You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

238 lines
6.7 KiB

/* 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);
}
}
}
}