summaryrefslogtreecommitdiffstats
path: root/modules/PageMenu.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'modules/PageMenu.jsm')
-rw-r--r--modules/PageMenu.jsm238
1 files changed, 238 insertions, 0 deletions
diff --git a/modules/PageMenu.jsm b/modules/PageMenu.jsm
new file mode 100644
index 0000000..d01f626
--- /dev/null
+++ b/modules/PageMenu.jsm
@@ -0,0 +1,238 @@
+/* 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 = ["PageMenu"];
+
+this.PageMenu = function PageMenu() {
+}
+
+PageMenu.prototype = {
+ PAGEMENU_ATTR: "pagemenu",
+ GENERATEDITEMID_ATTR: "generateditemid",
+
+ _popup: null,
+ _builder: null,
+
+ // Given a target node, get the context menu for it or its ancestor.
+ getContextMenu: function(aTarget) {
+ 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._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 (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;
+ }
+ },
+
+ // 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;
+ },
+
+ // Returns true if custom menu items were present.
+ maybeBuildAndAttachMenu: function(aTarget, aPopup) {
+ let menuObject = this.maybeBuild(aTarget);
+ if (!menuObject) {
+ return false;
+ }
+
+ return this.buildAndAttachMenuWithObject(menuObject, null, aPopup);
+ },
+
+ // 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);
+ }
+ }
+ }
+}