зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1257613 - Add an API to open context menus from an HTML document;r=jdescottes
MozReview-Commit-ID: 4j9d5k3Ut1f
This commit is contained in:
Родитель
7c5c08d787
Коммит
3642cda1f9
|
@ -0,0 +1,58 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* A partial implementation of the MenuItem API provided by electron:
|
||||
* https://github.com/electron/electron/blob/master/docs/api/menu-item.md.
|
||||
*
|
||||
* Missing features:
|
||||
* - id String - Unique within a single menu. If defined then it can be used
|
||||
* as a reference to this item by the position attribute.
|
||||
* - role String - Define the action of the menu item; when specified the
|
||||
* click property will be ignored
|
||||
* - sublabel String
|
||||
* - accelerator Accelerator
|
||||
* - icon NativeImage
|
||||
* - visible Boolean - If false, the menu item will be entirely hidden.
|
||||
* - position String - This field allows fine-grained definition of the
|
||||
* specific location within a given menu.
|
||||
*
|
||||
* Implemented features:
|
||||
* @param Object options
|
||||
* Function click
|
||||
* Will be called with click(menuItem, browserWindow) when the menu item is clicked
|
||||
* String type
|
||||
* Can be normal, separator, submenu, checkbox or radio
|
||||
* String label
|
||||
* Boolean enabled
|
||||
* If false, the menu item will be greyed out and unclickable.
|
||||
* Boolean checked
|
||||
* Should only be specified for checkbox or radio type menu items.
|
||||
* Menu submenu
|
||||
* Should be specified for submenu type menu items. If submenu is specified, the type: 'submenu' can be omitted. If the value is not a Menu then it will be automatically converted to one using Menu.buildFromTemplate.
|
||||
*
|
||||
*/
|
||||
function MenuItem({
|
||||
accesskey = null,
|
||||
checked = false,
|
||||
click = () => {},
|
||||
disabled = false,
|
||||
label = "",
|
||||
id = null,
|
||||
submenu = null,
|
||||
type = "normal",
|
||||
} = { }) {
|
||||
this.accesskey = accesskey;
|
||||
this.checked = checked;
|
||||
this.click = click;
|
||||
this.disabled = disabled;
|
||||
this.id = id;
|
||||
this.label = label;
|
||||
this.submenu = submenu;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
module.exports = MenuItem;
|
|
@ -0,0 +1,149 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
const MenuItem = require("./menu-item");
|
||||
const EventEmitter = require("devtools/shared/event-emitter");
|
||||
|
||||
/**
|
||||
* A partial implementation of the Menu API provided by electron:
|
||||
* https://github.com/electron/electron/blob/master/docs/api/menu.md.
|
||||
*
|
||||
* Extra features:
|
||||
* - Emits an 'open' and 'close' event when the menu is opened/closed
|
||||
|
||||
* @param String id (non standard)
|
||||
* Needed so tests can confirm the XUL implementation is working
|
||||
*/
|
||||
function Menu({id=null} = {}) {
|
||||
this.menuitems = [];
|
||||
this.id = id;
|
||||
|
||||
Object.defineProperty(this, "items", {
|
||||
get() {
|
||||
return this.menuitems;
|
||||
}
|
||||
});
|
||||
|
||||
EventEmitter.decorate(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an item to the end of the Menu
|
||||
*
|
||||
* @param {MenuItem} menuItem
|
||||
*/
|
||||
Menu.prototype.append = function(menuItem) {
|
||||
this.menuitems.push(menuItem);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add an item to a specified position in the menu
|
||||
*
|
||||
* @param {int} pos
|
||||
* @param {MenuItem} menuItem
|
||||
*/
|
||||
Menu.prototype.insert = function(pos, menuItem) {
|
||||
throw "Not implemented";
|
||||
};
|
||||
|
||||
/**
|
||||
* Show the Menu at a specified location on the screen
|
||||
*
|
||||
* Missing features:
|
||||
* - browserWindow - BrowserWindow (optional) - Default is null.
|
||||
* - positioningItem Number - (optional) OS X
|
||||
*
|
||||
* @param {int} screenX
|
||||
* @param {int} screenY
|
||||
* @param Toolbox toolbox (non standard)
|
||||
* Needed so we in which window to inject XUL
|
||||
*/
|
||||
Menu.prototype.popup = function(screenX, screenY, toolbox) {
|
||||
let doc = toolbox.doc;
|
||||
let popup = doc.createElement("menupopup");
|
||||
popup.setAttribute("menu-api", "true");
|
||||
|
||||
if (this.id) {
|
||||
popup.id = this.id;
|
||||
}
|
||||
this._createMenuItems(popup);
|
||||
|
||||
// Remove the menu from the DOM once it's hidden.
|
||||
popup.addEventListener("popuphidden", (e) => {
|
||||
if (e.target === popup) {
|
||||
popup.remove();
|
||||
this.emit("close");
|
||||
}
|
||||
});
|
||||
|
||||
popup.addEventListener("popupshown", (e) => {
|
||||
if (e.target === popup) {
|
||||
this.emit("open");
|
||||
}
|
||||
});
|
||||
|
||||
doc.querySelector("popupset").appendChild(popup);
|
||||
popup.openPopupAtScreen(screenX, screenY, true);
|
||||
};
|
||||
|
||||
Menu.prototype._createMenuItems = function(parent) {
|
||||
let doc = parent.ownerDocument;
|
||||
this.menuitems.forEach(item => {
|
||||
if (item.submenu) {
|
||||
let menupopup = doc.createElement("menupopup");
|
||||
item.submenu._createMenuItems(menupopup);
|
||||
|
||||
let menu = doc.createElement("menu");
|
||||
menu.appendChild(menupopup);
|
||||
menu.setAttribute("label", item.label);
|
||||
parent.appendChild(menu);
|
||||
} else if (item.type === "separator") {
|
||||
let menusep = doc.createElement("menuseparator");
|
||||
parent.appendChild(menusep);
|
||||
} else {
|
||||
let menuitem = doc.createElement("menuitem");
|
||||
menuitem.setAttribute("label", item.label);
|
||||
menuitem.addEventListener("command", () => {
|
||||
item.click();
|
||||
});
|
||||
|
||||
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.accesskey) {
|
||||
menuitem.setAttribute("accesskey", item.accesskey);
|
||||
}
|
||||
if (item.id) {
|
||||
menuitem.id = item.id;
|
||||
}
|
||||
|
||||
parent.appendChild(menuitem);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
Menu.setApplicationMenu = () => {
|
||||
throw "Not implemented";
|
||||
};
|
||||
|
||||
Menu.sendActionToFirstResponder = () => {
|
||||
throw "Not implemented";
|
||||
};
|
||||
|
||||
Menu.buildFromTemplate = () => {
|
||||
throw "Not implemented";
|
||||
};
|
||||
|
||||
module.exports = Menu;
|
|
@ -16,6 +16,8 @@ DevToolsModules(
|
|||
'devtools-browser.js',
|
||||
'devtools.js',
|
||||
'gDevTools.jsm',
|
||||
'menu-item.js',
|
||||
'menu.js',
|
||||
'selection.js',
|
||||
'sidebar.js',
|
||||
'source-location.js',
|
||||
|
|
|
@ -29,6 +29,7 @@ support-files =
|
|||
[browser_keybindings_01.js]
|
||||
[browser_keybindings_02.js]
|
||||
[browser_keybindings_03.js]
|
||||
[browser_menu_api.js]
|
||||
[browser_new_activation_workflow.js]
|
||||
[browser_source-location-01.js]
|
||||
[browser_source-location-02.js]
|
||||
|
|
|
@ -0,0 +1,161 @@
|
|||
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
|
||||
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test that the Menu API works
|
||||
|
||||
const URL = "data:text/html;charset=utf8,test page for menu api";
|
||||
const Menu = require("devtools/client/framework/menu");
|
||||
const MenuItem = require("devtools/client/framework/menu-item");
|
||||
|
||||
add_task(function*() {
|
||||
info("Create a test tab and open the toolbox");
|
||||
let tab = yield addTab(URL);
|
||||
let target = TargetFactory.forTab(tab);
|
||||
let toolbox = yield gDevTools.showToolbox(target, "webconsole");
|
||||
|
||||
yield testMenuItems();
|
||||
yield testMenuPopup(toolbox);
|
||||
yield testSubmenu(toolbox);
|
||||
});
|
||||
|
||||
function* testMenuItems() {
|
||||
let menu = new Menu();
|
||||
let menuItem1 = new MenuItem();
|
||||
let menuItem2 = new MenuItem();
|
||||
|
||||
menu.append(menuItem1);
|
||||
menu.append(menuItem2);
|
||||
|
||||
is(menu.items.length, 2, "Correct number of 'items'");
|
||||
is(menu.items[0], menuItem1, "Correct reference to MenuItem");
|
||||
is(menu.items[1], menuItem2, "Correct reference to MenuItem");
|
||||
}
|
||||
|
||||
function* testMenuPopup(toolbox) {
|
||||
let clickFired = false;
|
||||
|
||||
let menu = new Menu({
|
||||
id: "menu-popup",
|
||||
});
|
||||
menu.append(new MenuItem({ type: "separator" }));
|
||||
|
||||
let MENU_ITEMS = [
|
||||
new MenuItem({
|
||||
id: "menu-item-1",
|
||||
label: "Normal Item",
|
||||
click: () => {
|
||||
info("Click callback has fired for menu item");
|
||||
clickFired = true;
|
||||
},
|
||||
}),
|
||||
new MenuItem({
|
||||
label: "Checked Item",
|
||||
type: "checkbox",
|
||||
checked: true,
|
||||
}),
|
||||
new MenuItem({
|
||||
label: "Radio Item",
|
||||
type: "radio",
|
||||
}),
|
||||
new MenuItem({
|
||||
label: "Disabled Item",
|
||||
disabled: true,
|
||||
}),
|
||||
];
|
||||
|
||||
for (let item of MENU_ITEMS) {
|
||||
menu.append(item);
|
||||
}
|
||||
|
||||
menu.popup(0, 0, toolbox);
|
||||
|
||||
ok(toolbox.doc.querySelector("#menu-popup"), "A popup is in the DOM");
|
||||
|
||||
let menuSeparators = toolbox.doc.querySelectorAll("#menu-popup > menuseparator");
|
||||
is(menuSeparators.length, 1, "A separator is in the menu");
|
||||
|
||||
let menuItems = toolbox.doc.querySelectorAll("#menu-popup > menuitem");
|
||||
is(menuItems.length, MENU_ITEMS.length, "Correct number of menuitems");
|
||||
|
||||
is(menuItems[0].id, MENU_ITEMS[0].id, "Correct id for menuitem");
|
||||
is(menuItems[0].getAttribute("label"), MENU_ITEMS[0].label, "Correct label");
|
||||
|
||||
is(menuItems[1].getAttribute("label"), MENU_ITEMS[1].label, "Correct label");
|
||||
is(menuItems[1].getAttribute("type"), "checkbox", "Correct type attribute");
|
||||
is(menuItems[1].getAttribute("checked"), "true", "Has checked attribute");
|
||||
|
||||
is(menuItems[2].getAttribute("label"), MENU_ITEMS[2].label, "Correct label");
|
||||
is(menuItems[2].getAttribute("type"), "radio", "Correct type attribute");
|
||||
ok(!menuItems[2].hasAttribute("checked"), "Doesn't have checked attribute");
|
||||
|
||||
is(menuItems[3].getAttribute("label"), MENU_ITEMS[3].label, "Correct label");
|
||||
is(menuItems[3].getAttribute("disabled"), "true", "disabled attribute menuitem");
|
||||
|
||||
yield once(menu, "open");
|
||||
let closed = once(menu, "close");
|
||||
EventUtils.synthesizeMouseAtCenter(menuItems[0], {}, toolbox.doc.defaultView);
|
||||
yield closed;
|
||||
ok(clickFired, "Click has fired");
|
||||
|
||||
ok(!toolbox.doc.querySelector("#menu-popup"), "The popup is removed from the DOM");
|
||||
}
|
||||
|
||||
function* testSubmenu(toolbox) {
|
||||
let clickFired = false;
|
||||
let menu = new Menu({
|
||||
id: "menu-popup",
|
||||
});
|
||||
let submenu = new Menu({
|
||||
id: "submenu-popup",
|
||||
});
|
||||
submenu.append(new MenuItem({
|
||||
label: "Submenu item",
|
||||
click: () => {
|
||||
info("Click callback has fired for submenu item");
|
||||
clickFired = true;
|
||||
},
|
||||
}));
|
||||
menu.append(new MenuItem({
|
||||
label: "Submenu parent",
|
||||
submenu: submenu,
|
||||
}));
|
||||
|
||||
menu.popup(0, 0, toolbox);
|
||||
ok(toolbox.doc.querySelector("#menu-popup"), "A popup is in the DOM");
|
||||
is(toolbox.doc.querySelectorAll("#menu-popup > menuitem").length, 0, "No menuitem children");
|
||||
|
||||
let menus = toolbox.doc.querySelectorAll("#menu-popup > menu");
|
||||
is(menus.length, 1, "Correct number of menus");
|
||||
is(menus[0].getAttribute("label"), "Submenu parent", "Correct label for menus");
|
||||
|
||||
let subMenuItems = menus[0].querySelectorAll("menupopup > menuitem");
|
||||
is(subMenuItems.length, 1, "Correct number of submenu items");
|
||||
is(subMenuItems[0].getAttribute("label"), "Submenu item", "Correct label");
|
||||
|
||||
yield once(menu, "open");
|
||||
let closed = once(menu, "close");
|
||||
|
||||
info("Using keyboard navigation to open, close, and reopen the submenu");
|
||||
let shown = once(menus[0], "popupshown");
|
||||
EventUtils.synthesizeKey("VK_DOWN", {});
|
||||
EventUtils.synthesizeKey("VK_RIGHT", {});
|
||||
yield shown;
|
||||
|
||||
let hidden = once(menus[0], "popuphidden");
|
||||
EventUtils.synthesizeKey("VK_LEFT", {});
|
||||
yield hidden;
|
||||
|
||||
shown = once(menus[0], "popupshown");
|
||||
EventUtils.synthesizeKey("VK_RIGHT", {});
|
||||
yield shown;
|
||||
|
||||
info("Clicking the submenu item");
|
||||
EventUtils.synthesizeMouseAtCenter(subMenuItems[0], {}, toolbox.doc.defaultView);
|
||||
|
||||
yield closed;
|
||||
ok(clickFired, "Click has fired");
|
||||
}
|
Загрузка…
Ссылка в новой задаче