зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
630efc5cec
Коммит
f94ac37cc8
|
@ -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 => {
|
||||
|
|
Загрузка…
Ссылка в новой задаче