summaryrefslogtreecommitdiff
path: root/modules/PageMenu.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'modules/PageMenu.jsm')
-rw-r--r--modules/PageMenu.jsm320
1 files changed, 320 insertions, 0 deletions
diff --git a/modules/PageMenu.jsm b/modules/PageMenu.jsm
new file mode 100644
index 000000000..30dfbea42
--- /dev/null
+++ b/modules/PageMenu.jsm
@@ -0,0 +1,320 @@
+/* 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/. */
+
+this.EXPORTED_SYMBOLS = ["PageMenuParent", "PageMenuChild"];
+
+var {interfaces: Ci} = Components;
+
+this.PageMenu = function PageMenu() {
+}
+
+PageMenu.prototype = {
+ PAGEMENU_ATTR: "pagemenu",
+ GENERATEDITEMID_ATTR: "generateditemid",
+
+ _popup: null,
+
+ // Only one of builder or browser will end up getting set.
+ _builder: null,
+ _browser: null,
+
+ // Given a target node, get the context menu for it or its ancestor.
+ getContextMenu: function(aTarget) {
+ let pageMenu = null;
+ let target = aTarget;
+ while (target) {
+ let contextMenu = target.contextMenu;
+ if (contextMenu) {
+ return contextMenu;
+ }
+ target = target.parentNode;
+ }
+
+ return null;
+ },
+
+ // Given a target node, generate a JSON object for any context menu
+ // associated with it, or null if there is no context menu.
+ maybeBuild: function(aTarget) {
+ let pageMenu = this.getContextMenu(aTarget);
+ if (!pageMenu) {
+ return null;
+ }
+
+ pageMenu.QueryInterface(Components.interfaces.nsIHTMLMenu);
+ pageMenu.sendShowEvent();
+ // the show event is not cancelable, so no need to check a result here
+
+ this._builder = pageMenu.createBuilder();
+ if (!this._builder) {
+ return null;
+ }
+
+ pageMenu.build(this._builder);
+
+ // This serializes then parses again, however this could be avoided in
+ // the single-process case with further improvement.
+ let menuString = this._builder.toJSONString();
+ if (!menuString) {
+ return null;
+ }
+
+ return JSON.parse(menuString);
+ },
+
+ // Given a JSON menu object and popup, add the context menu to the popup.
+ buildAndAttachMenuWithObject: function(aMenu, aBrowser, aPopup) {
+ if (!aMenu) {
+ return false;
+ }
+
+ let insertionPoint = this.getInsertionPoint(aPopup);
+ if (!insertionPoint) {
+ return false;
+ }
+
+ let fragment = aPopup.ownerDocument.createDocumentFragment();
+ this.buildXULMenu(aMenu, fragment);
+
+ let pos = insertionPoint.getAttribute(this.PAGEMENU_ATTR);
+ if (pos == "start") {
+ insertionPoint.insertBefore(fragment,
+ insertionPoint.firstChild);
+ } else if (pos.startsWith("#")) {
+ insertionPoint.insertBefore(fragment, insertionPoint.querySelector(pos));
+ } else {
+ insertionPoint.appendChild(fragment);
+ }
+
+ this._browser = aBrowser;
+ this._popup = aPopup;
+
+ this._popup.addEventListener("command", this);
+ this._popup.addEventListener("popuphidden", this);
+
+ return true;
+ },
+
+ // Construct the XUL menu structure for a given JSON object.
+ buildXULMenu: function(aNode, aElementForAppending) {
+ let document = aElementForAppending.ownerDocument;
+
+ let children = aNode.children;
+ for (let child of children) {
+ let menuitem;
+ switch (child.type) {
+ case "menuitem":
+ if (!child.id) {
+ continue; // Ignore children without ids
+ }
+
+ menuitem = document.createElement("menuitem");
+ if (child.checkbox) {
+ menuitem.setAttribute("type", "checkbox");
+ if (child.checked) {
+ menuitem.setAttribute("checked", "true");
+ }
+ }
+
+ if (child.label) {
+ menuitem.setAttribute("label", child.label);
+ }
+ if (child.icon) {
+ menuitem.setAttribute("image", child.icon);
+ menuitem.className = "menuitem-iconic";
+ }
+ if (child.disabled) {
+ menuitem.setAttribute("disabled", true);
+ }
+
+ break;
+
+ case "separator":
+ menuitem = document.createElement("menuseparator");
+ break;
+
+ case "menu":
+ menuitem = document.createElement("menu");
+ if (child.label) {
+ menuitem.setAttribute("label", child.label);
+ }
+
+ let menupopup = document.createElement("menupopup");
+ menuitem.appendChild(menupopup);
+
+ this.buildXULMenu(child, menupopup);
+ break;
+ }
+
+ menuitem.setAttribute(this.GENERATEDITEMID_ATTR, child.id ? child.id : 0);
+ aElementForAppending.appendChild(menuitem);
+ }
+ },
+
+ // Called when the generated menuitem is executed.
+ handleEvent: function(event) {
+ let type = event.type;
+ let target = event.target;
+ if (type == "command" && target.hasAttribute(this.GENERATEDITEMID_ATTR)) {
+ // If a builder is assigned, call click on it directly. Otherwise, this is
+ // likely a menu with data from another process, so send a message to the
+ // browser to execute the menuitem.
+ if (this._builder) {
+ this._builder.click(target.getAttribute(this.GENERATEDITEMID_ATTR));
+ }
+ else if (this._browser) {
+ let win = target.ownerDocument.defaultView;
+ let windowUtils = win.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+ this._browser.messageManager.sendAsyncMessage("ContextMenu:DoCustomCommand", {
+ generatedItemId: target.getAttribute(this.GENERATEDITEMID_ATTR),
+ handlingUserInput: windowUtils.isHandlingUserInput
+ });
+ }
+ } else if (type == "popuphidden" && this._popup == target) {
+ this.removeGeneratedContent(this._popup);
+
+ this._popup.removeEventListener("popuphidden", this);
+ this._popup.removeEventListener("command", this);
+
+ this._popup = null;
+ this._builder = null;
+ this._browser = null;
+ }
+ },
+
+ // Get the first child of the given element with the given tag name.
+ getImmediateChild: function(element, tag) {
+ let child = element.firstChild;
+ while (child) {
+ if (child.localName == tag) {
+ return child;
+ }
+ child = child.nextSibling;
+ }
+ return null;
+ },
+
+ // Return the location where the generated items should be inserted into the
+ // given popup. They should be inserted as the next sibling of the returned
+ // element.
+ getInsertionPoint: function(aPopup) {
+ if (aPopup.hasAttribute(this.PAGEMENU_ATTR))
+ return aPopup;
+
+ let element = aPopup.firstChild;
+ while (element) {
+ if (element.localName == "menu") {
+ let popup = this.getImmediateChild(element, "menupopup");
+ if (popup) {
+ let result = this.getInsertionPoint(popup);
+ if (result) {
+ return result;
+ }
+ }
+ }
+ element = element.nextSibling;
+ }
+
+ return null;
+ },
+
+ // Remove the generated content from the given popup.
+ removeGeneratedContent: function(aPopup) {
+ let ungenerated = [];
+ ungenerated.push(aPopup);
+
+ let count;
+ while (0 != (count = ungenerated.length)) {
+ let last = count - 1;
+ let element = ungenerated[last];
+ ungenerated.splice(last, 1);
+
+ let i = element.childNodes.length;
+ while (i-- > 0) {
+ let child = element.childNodes[i];
+ if (!child.hasAttribute(this.GENERATEDITEMID_ATTR)) {
+ ungenerated.push(child);
+ continue;
+ }
+ element.removeChild(child);
+ }
+ }
+ }
+}
+
+// This object is expected to be used from a parent process.
+this.PageMenuParent = function PageMenuParent() {
+}
+
+PageMenuParent.prototype = {
+ __proto__ : PageMenu.prototype,
+
+ /*
+ * Given a target node and popup, add the context menu to the popup. This is
+ * intended to be called when a single process is used. This is equivalent to
+ * calling PageMenuChild.build and PageMenuParent.addToPopup in sequence.
+ *
+ * Returns true if custom menu items were present.
+ */
+ buildAndAddToPopup: function(aTarget, aPopup) {
+ let menuObject = this.maybeBuild(aTarget);
+ if (!menuObject) {
+ return false;
+ }
+
+ return this.buildAndAttachMenuWithObject(menuObject, null, aPopup);
+ },
+
+ /*
+ * Given a JSON menu object and popup, add the context menu to the popup. This
+ * is intended to be called when the child page is in a different process.
+ * aBrowser should be the browser containing the page the context menu is
+ * displayed for, which may be null.
+ *
+ * Returns true if custom menu items were present.
+ */
+ addToPopup: function(aMenu, aBrowser, aPopup) {
+ return this.buildAndAttachMenuWithObject(aMenu, aBrowser, aPopup);
+ }
+}
+
+// This object is expected to be used from a child process.
+this.PageMenuChild = function PageMenuChild() {
+}
+
+PageMenuChild.prototype = {
+ __proto__ : PageMenu.prototype,
+
+ /*
+ * Given a target node, return a JSON object for the custom menu commands. The
+ * object will consist of a hierarchical structure of menus, menuitems or
+ * separators. Supported properties of each are:
+ * Menu: children, label, type="menu"
+ * Menuitems: checkbox, checked, disabled, icon, label, type="menuitem"
+ * Separators: type="separator"
+ *
+ * In addition, the id of each item will be used to identify the item
+ * when it is executed. The type will either be 'menu', 'menuitem' or
+ * 'separator'. The toplevel node will be a menu with a children property. The
+ * children property of a menu is an array of zero or more other items.
+ *
+ * If there is no menu associated with aTarget, null will be returned.
+ */
+ build: function(aTarget) {
+ return this.maybeBuild(aTarget);
+ },
+
+ /*
+ * Given the id of a menu, execute the command associated with that menu. It
+ * is assumed that only one command will be executed so the builder is
+ * cleared afterwards.
+ */
+ executeMenu: function(aId) {
+ if (this._builder) {
+ this._builder.click(aId);
+ this._builder = null;
+ }
+ }
+}