Bug 1257613 - Add an API to open context menus from an HTML document; f=jdescottes draft
authorBrian Grinstead <bgrinstead@mozilla.com>
Wed, 27 Apr 2016 17:49:43 -0700
changeset 357106 85a3df619a3eb2ecbd3157ca78724b7dc6b7e66f
parent 357090 2a3eb80dc9653ee0edc9f4a8b657b80f817b000e
child 519570 c1b02d436b9b2b958e3c66816c24cd98504f2b1c
push id16700
push userbgrinstead@mozilla.com
push dateThu, 28 Apr 2016 00:49:50 +0000
bugs1257613
milestone49.0a1
Bug 1257613 - Add an API to open context menus from an HTML document; f=jdescottes MozReview-Commit-ID: 4j9d5k3Ut1f
devtools/client/framework/menu-item.js
devtools/client/framework/menu.js
devtools/client/framework/moz.build
devtools/client/framework/test/browser.ini
devtools/client/framework/test/browser_menu_api.js
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/menu-item.js
@@ -0,0 +1,25 @@
+/* -*- 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/. */
+
+function MenuItem({
+    label = "not set",
+    id = null,
+    submenu = null,
+    checked = false,
+    type = "normal",
+    disabled = false,
+    click = () => {}
+} = { }) {
+  this.checked = checked;
+  this.click = click;
+  this.disabled = disabled;
+  this.id = id;
+  this.label = label;
+  this.submenu = submenu;
+  this.type = type;
+}
+
+module.exports = MenuItem;
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/menu.js
@@ -0,0 +1,90 @@
+/* -*- 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");
+
+function Menu({id=null} = {}) {
+  this.menuitems = [];
+  this.id = id;
+}
+
+Menu.prototype.append = function(menuitem) {
+  // TODO: do we want to support appending a plain object?
+  // if (!(menuitem instanceof MenuItem)) {
+  //   menuitem = new MenuItem(menuitem);
+  // }
+
+  this.menuitems.push(menuitem);
+};
+
+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.disabled) {
+        menuitem.setAttribute("disabled", "true");
+      }
+      if (item.accesskey) {
+        menuitem.setAttribute("accesskey", item.accesskey);
+      }
+      if (item.selection) {
+        menuitem.setAttribute("selection", item.selection);
+      }
+      if (item.selectionType) {
+        menuitem.setAttribute("selectionType", item.selectionType);
+      }
+      if (item.id) {
+        menuitem.id = item.id;
+      }
+
+      parent.appendChild(menuitem);
+    }
+  });
+};
+
+Menu.prototype.popup = function(win, x, y) {
+  let doc = win.document;
+  let popup = doc.createElement("menupopup");
+  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();
+    }
+  }, true);
+
+  doc.querySelector("popupset").appendChild(popup);
+  popup.openPopupAtScreen(x, y, true);
+  popup.focus();
+
+  return popup;
+};
+
+module.exports = Menu;
--- a/devtools/client/framework/moz.build
+++ b/devtools/client/framework/moz.build
@@ -11,16 +11,18 @@ TEST_HARNESS_FILES.xpcshell.devtools.cli
 
 DevToolsModules(
     'about-devtools-toolbox.js',
     'attach-thread.js',
     'browser-menus.js',
     'devtools-browser.js',
     'devtools.js',
     'gDevTools.jsm',
+    'menu-item.js',
+    'menu.js',
     'selection.js',
     'sidebar.js',
     'source-location.js',
     'target-from-url.js',
     'target.js',
     'toolbox-highlighter-utils.js',
     'toolbox-hosts.js',
     'toolbox-options.js',
--- a/devtools/client/framework/test/browser.ini
+++ b/devtools/client/framework/test/browser.ini
@@ -24,16 +24,17 @@ support-files =
 [browser_browser_toolbox_debugger.js]
 [browser_devtools_api.js]
 [browser_devtools_api_destroy.js]
 [browser_dynamic_tool_enabling.js]
 [browser_ignore_toolbox_network_requests.js]
 [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]
 [browser_target_from_url.js]
 [browser_target_events.js]
 [browser_target_remote.js]
 [browser_target_support.js]
 [browser_toolbox_custom_host.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/framework/test/browser_menu_api.js
@@ -0,0 +1,48 @@
+/* -*- 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");
+
+
+  let menu = new Menu({
+    id: "menu-popup",
+  });
+  menu.append(new MenuItem({
+    id: "menu-item-1",
+    label: "Hi",
+    click: () => { },
+  }));
+
+  let popup = menu.popup(toolbox.doc.defaultView, 0, 0);
+
+  let menuPopup = toolbox.doc.querySelector("#menu-popup");
+  ok(menuPopup, "A popup is in the DOM");
+  is(popup, menuPopup, "Correct popup is in the DOM");
+
+  let menuItems = [...menuPopup.querySelectorAll("menuitem")];
+  is(menuItems.length, 1, "Correct number of menuitems");
+  is(menuItems[0].getAttribute("label"), "Hi", "Correct text for menuitem");
+
+  // TODO: Test all the possible options for menu items
+
+  yield once(popup, "popupshown");
+
+  // TODO: Test that click event fires
+
+  yield toolbox.destroy();
+  gBrowser.removeCurrentTab();
+});