Bug 1456852 - Programatically create the edit menu in the browser console input;r=nchevobbe

This allows us to open the context menu directly from webconsole.html when it's
running as a top level window. Ultimately, I'd like to not have to use special
handling in the console - all top-level windows should get the edit menu working
automatically for HTML inputs - but this lets us prove it out as a first consumer.

MozReview-Commit-ID: BYisQDtXWe4

--HG--
extra : rebase_source : 8000147713000a30af48f1da17d50356a4cd4a04
This commit is contained in:
Brian Grinstead 2018-07-17 11:55:49 -07:00
Родитель 630efc5cec
Коммит f94ac37cc8
10 изменённых файлов: 175 добавлений и 39 удалений

Просмотреть файл

@ -51,6 +51,7 @@
function MenuItem({
accelerator = null,
accesskey = null,
l10nID = null,
checked = false,
click = () => {},
disabled = false,
@ -63,6 +64,7 @@ function MenuItem({
} = { }) {
this.accelerator = accelerator;
this.accesskey = accesskey;
this.l10nID = l10nID;
this.checked = checked;
this.click = click;
this.disabled = disabled;

Просмотреть файл

@ -136,26 +136,15 @@ Menu.prototype._createMenuItems = function(parent) {
const menu = doc.createElementNS(XUL_NS, "menu");
menu.appendChild(menupopup);
menu.setAttribute("label", item.label);
if (item.disabled) {
menu.setAttribute("disabled", "true");
}
if (item.accelerator) {
menu.setAttribute("acceltext", item.accelerator);
}
if (item.accesskey) {
menu.setAttribute("accesskey", item.accesskey);
}
if (item.id) {
menu.id = item.id;
}
applyItemAttributesToNode(item, menu);
parent.appendChild(menu);
} else if (item.type === "separator") {
const menusep = doc.createElementNS(XUL_NS, "menuseparator");
parent.appendChild(menusep);
} else {
const menuitem = doc.createElementNS(XUL_NS, "menuitem");
menuitem.setAttribute("label", item.label);
applyItemAttributesToNode(item, menuitem);
menuitem.addEventListener("command", () => {
item.click();
});
@ -163,28 +152,6 @@ Menu.prototype._createMenuItems = function(parent) {
item.hover();
});
if (item.type === "checkbox") {
menuitem.setAttribute("type", "checkbox");
}
if (item.type === "radio") {
menuitem.setAttribute("type", "radio");
}
if (item.disabled) {
menuitem.setAttribute("disabled", "true");
}
if (item.checked) {
menuitem.setAttribute("checked", "true");
}
if (item.accelerator) {
menuitem.setAttribute("acceltext", item.accelerator);
}
if (item.accesskey) {
menuitem.setAttribute("accesskey", item.accesskey);
}
if (item.id) {
menuitem.id = item.id;
}
parent.appendChild(menuitem);
}
});
@ -202,4 +169,33 @@ Menu.buildFromTemplate = () => {
throw Error("Not implemented");
};
function applyItemAttributesToNode(item, node) {
if (item.l10nID) {
node.setAttribute("data-l10n-id", item.l10nID);
} else {
node.setAttribute("label", item.label);
if (item.accelerator) {
node.setAttribute("acceltext", item.accelerator);
}
if (item.accesskey) {
node.setAttribute("accesskey", item.accesskey);
}
}
if (item.type === "checkbox") {
node.setAttribute("type", "checkbox");
}
if (item.type === "radio") {
node.setAttribute("type", "radio");
}
if (item.disabled) {
node.setAttribute("disabled", "true");
}
if (item.checked) {
node.setAttribute("checked", "true");
}
if (item.id) {
node.id = item.id;
}
}
module.exports = Menu;

Просмотреть файл

@ -65,6 +65,9 @@ async function testMenuPopup(toolbox) {
label: "Disabled Item",
disabled: true,
}),
new MenuItem({
l10nID: "foo",
}),
];
for (const item of MENU_ITEMS) {
@ -102,6 +105,8 @@ async function testMenuPopup(toolbox) {
is(menuItems[3].getAttribute("label"), MENU_ITEMS[3].label, "Correct label");
is(menuItems[3].getAttribute("disabled"), "true", "disabled attr menuitem");
is(menuItems[4].getAttribute("data-l10n-id"), MENU_ITEMS[4].l10nID, "Correct localization attribute");
await once(menu, "open");
const closed = once(menu, "close");
EventUtils.synthesizeMouseAtCenter(menuItems[0], {}, toolbox.win);
@ -127,7 +132,7 @@ async function testSubmenu(toolbox) {
},
}));
menu.append(new MenuItem({
label: "Submenu parent",
l10nID: "submenu-parent",
submenu: submenu,
}));
menu.append(new MenuItem({
@ -145,8 +150,9 @@ async function testSubmenu(toolbox) {
const menus = toolbox.doc.querySelectorAll("#menu-popup > menu");
is(menus.length, 2, "Correct number of menus");
is(menus[0].getAttribute("label"), "Submenu parent", "Correct label");
ok(!menus[0].hasAttribute("label"), "No label: should be set by localization");
ok(!menus[0].hasAttribute("disabled"), "Correct disabled state");
is(menus[0].getAttribute("data-l10n-id"), "submenu-parent", "Correct localization attribute");
is(menus[1].getAttribute("accesskey"), "A", "Correct accesskey");
ok(menus[1].hasAttribute("disabled"), "Correct disabled state");

Просмотреть файл

@ -11,6 +11,8 @@
windowtype="devtools:webconsole"
width="900" height="350"
persist="screenX screenY width height sizemode">
<link rel="localization" href="toolkit/main-window/editmenu.ftl"/>
<script type="text/javascript" src="chrome://global/content/l10n.js"></script>
<popupset></popupset>
<iframe src="index.html" flex="1"></iframe>
</window>

Просмотреть файл

@ -163,6 +163,7 @@ class App extends Component {
}),
JSTerm({
hud,
serviceContainer,
onPaste: this.onPaste,
codeMirrorEnabled: jstermCodeMirror,
}),

Просмотреть файл

@ -70,6 +70,8 @@ class JSTerm extends Component {
history: PropTypes.object.isRequired,
// Console object.
hud: PropTypes.object.isRequired,
// Needed for opening context menu
serviceContainer: PropTypes.object.isRequired,
// Handler for clipboard 'paste' event (also used for 'drop' event, callback).
onPaste: PropTypes.func,
codeMirrorEnabled: PropTypes.bool,
@ -97,6 +99,7 @@ class JSTerm extends Component {
this._keyPress = this._keyPress.bind(this);
this._inputEventHandler = this._inputEventHandler.bind(this);
this._blurEventHandler = this._blurEventHandler.bind(this);
this.onContextMenu = this.onContextMenu.bind(this);
this.SELECTED_FRAME = -1;
@ -1467,6 +1470,17 @@ class JSTerm extends Component {
.paddingLeft.replace(/[^0-9.]/g, "") - 4;
}
onContextMenu(e) {
// The toolbox does it's own edit menu handling with
// toolbox-textbox-context-popup and friends. For now, fall
// back to use that if running inside the toolbox, but use our
// own menu when running in the Browser Console (see Bug 1476097).
if (this.props.hud.isBrowserConsole &&
Services.prefs.getBoolPref("devtools.browserconsole.html")) {
this.props.serviceContainer.openEditContextMenu(e);
}
}
destroy() {
this.clearCompletion();
@ -1509,6 +1523,7 @@ class JSTerm extends Component {
key: "jsterm-container",
style: {direction: "ltr"},
"aria-live": "off",
onContextMenu: this.onContextMenu,
ref: node => {
this.node = node;
},
@ -1545,6 +1560,7 @@ class JSTerm extends Component {
},
onPaste: onPaste,
onDrop: onPaste,
onContextMenu: this.onContextMenu,
})
)
);

Просмотреть файл

@ -9,6 +9,8 @@
persist="screenX screenY width height sizemode">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<link rel="localization" href="toolkit/main-window/editmenu.ftl"/>
<link rel="stylesheet" href="chrome://global/skin/"/>
<link rel="stylesheet" href="chrome://devtools/skin/widgets.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/webconsole.css"/>
<link rel="stylesheet" href="chrome://devtools/skin/components-frame.css"/>
@ -18,6 +20,7 @@
<link rel="stylesheet" href="resource://devtools/client/shared/components/NotificationBox.css"/>
<link rel="stylesheet" href="chrome://devtools/content/netmonitor/src/assets/styles/httpi.css"/>
<script type="text/javascript" src="chrome://global/content/l10n.js"></script>
<script src="chrome://devtools/content/shared/theme-switching.js"></script>
<script type="application/javascript"
src="resource://devtools/client/webconsole/main.js"></script>

Просмотреть файл

@ -11,6 +11,9 @@ const TEST_URI = "http://example.com/browser/devtools/client/webconsole/" +
add_task(async function() {
// Enable net messages in the console for this test.
await pushPref("devtools.browserconsole.filter.net", true);
// These are required for testing the text input in the browser console:
await pushPref("devtools.browserconsole.html", true);
await pushPref("devtools.chrome.enabled", true);
await addTab(TEST_URI);
const hud = await HUDService.toggleBrowserConsole();
@ -56,6 +59,19 @@ add_task(async function() {
is(getSimplifiedContextMenu(menuPopup).join("\n"), expectedContextMenu.join("\n"),
"The context menu has the expected entries for a simple log message");
menuPopup = await openContextMenu(hud, hud.jsterm.inputNode);
expectedContextMenu = [
"#editmenu-undo (editmenu-undo) [disabled]",
"#editmenu-cut (editmenu-cut)",
"#editmenu-copy (editmenu-copy)",
"#editmenu-paste (editmenu-paste)",
"#editmenu-delete (editmenu-delete) [disabled]",
"#editmenu-selectAll (editmenu-select-all) [disabled]",
];
is(getL10NContextMenu(menuPopup).join("\n"), expectedContextMenu.join("\n"),
"The context menu has the correct edit menu items");
await hideContextMenu(hud);
});
@ -67,6 +83,15 @@ function addPrefBasedEntries(expectedEntries) {
return expectedEntries;
}
function getL10NContextMenu(popupElement) {
return [...popupElement.querySelectorAll("menuitem")]
.map(entry => {
const l10nID = entry.getAttribute("data-l10n-id");
const disabled = entry.hasAttribute("disabled");
return `#${entry.id} (${l10nID})${disabled ? " [disabled]" : ""}`;
});
}
function getSimplifiedContextMenu(popupElement) {
return [...popupElement.querySelectorAll("menuitem")]
.map(entry => {

Просмотреть файл

@ -181,3 +181,78 @@ function createContextMenu(hud, parentNode, {
}
exports.createContextMenu = createContextMenu;
/**
* Return an 'edit' menu for a input field. This integrates directly
* with docshell commands to provide the right enabled state and editor
* functionality.
*
* You'll need to call menu.popup() yourself, this just returns the Menu instance.
*
* @returns {Menu}
*/
function createEditContextMenu() {
const docshell = window.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
const menu = new Menu({
id: "webconsole-menu"
});
menu.append(new MenuItem({
id: "editmenu-undo",
l10nID: "editmenu-undo",
disabled: !docshell.isCommandEnabled("cmd_undo"),
click: () => {
docshell.doCommand("cmd_undo");
},
}));
menu.append(new MenuItem({
type: "separator"
}));
menu.append(new MenuItem({
id: "editmenu-cut",
l10nID: "editmenu-cut",
disabled: !docshell.isCommandEnabled("cmd_cut"),
click: () => {
docshell.doCommand("cmd_cut");
},
}));
menu.append(new MenuItem({
id: "editmenu-copy",
l10nID: "editmenu-copy",
disabled: !docshell.isCommandEnabled("cmd_copy"),
click: () => {
docshell.doCommand("cmd_copy");
},
}));
menu.append(new MenuItem({
id: "editmenu-paste",
l10nID: "editmenu-paste",
disabled: !docshell.isCommandEnabled("cmd_paste"),
click: () => {
docshell.doCommand("cmd_paste");
},
}));
menu.append(new MenuItem({
id: "editmenu-delete",
l10nID: "editmenu-delete",
disabled: !docshell.isCommandEnabled("cmd_delete"),
click: () => {
docshell.doCommand("cmd_delete");
},
}));
menu.append(new MenuItem({
type: "separator"
}));
menu.append(new MenuItem({
id: "editmenu-selectAll",
l10nID: "editmenu-select-all",
disabled: !docshell.isCommandEnabled("cmd_selectAll"),
click: () => {
docshell.doCommand("cmd_selectAll");
},
}));
return menu;
}
exports.createEditContextMenu = createEditContextMenu;

Просмотреть файл

@ -8,7 +8,7 @@ const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const { Provider } = require("devtools/client/shared/vendor/react-redux");
const actions = require("devtools/client/webconsole/actions/index");
const { createContextMenu } = require("devtools/client/webconsole/utils/context-menu");
const { createContextMenu, createEditContextMenu } = require("devtools/client/webconsole/utils/context-menu");
const { configureStore } = require("devtools/client/webconsole/store");
const { isPacketPrivate } = require("devtools/client/webconsole/utils/messages");
const { getAllMessagesById, getMessage } = require("devtools/client/webconsole/selectors/messages");
@ -160,6 +160,16 @@ WebConsoleOutputWrapper.prototype = {
return menu;
};
serviceContainer.openEditContextMenu = (e) => {
const { screenX, screenY } = e;
const menu = createEditContextMenu();
// Emit the "menu-open" event for testing.
menu.once("open", () => this.emit("menu-open"));
menu.popup(screenX, screenY, { doc: this.owner.chromeWindow.document });
return menu;
};
if (this.toolbox) {
Object.assign(serviceContainer, {
onViewSourceInDebugger: frame => {