mirror of https://github.com/roytam1/UXP
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
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); |
|
} |
|
} |
|
} |
|
}
|
|
|