1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
5 var EXPORTED_SYMBOLS = ["PageMenuParent", "PageMenuChild"];
10 PAGEMENU_ATTR: "pagemenu",
11 GENERATEDITEMID_ATTR: "generateditemid",
15 // Only one of builder or browser will end up getting set.
19 // Given a target node, get the context menu for it or its ancestor.
20 getContextMenu(aTarget) {
23 let contextMenu = target.contextMenu;
27 target = target.parentNode;
33 // Given a target node, generate a JSON object for any context menu
34 // associated with it, or null if there is no context menu.
36 let pageMenu = this.getContextMenu(aTarget);
41 pageMenu.sendShowEvent();
42 // the show event is not cancelable, so no need to check a result here
44 this._builder = pageMenu.createBuilder();
49 pageMenu.build(this._builder);
51 // This serializes then parses again, however this could be avoided in
52 // the single-process case with further improvement.
53 let menuString = this._builder.toJSONString();
58 return JSON.parse(menuString);
61 // Given a JSON menu object and popup, add the context menu to the popup.
62 buildAndAttachMenuWithObject(aMenu, aBrowser, aPopup) {
67 let insertionPoint = this.getInsertionPoint(aPopup);
68 if (!insertionPoint) {
72 let fragment = aPopup.ownerDocument.createDocumentFragment();
73 this.buildXULMenu(aMenu, fragment);
75 let pos = insertionPoint.getAttribute(this.PAGEMENU_ATTR);
77 insertionPoint.insertBefore(fragment, insertionPoint.firstElementChild);
78 } else if (pos.startsWith("#")) {
79 insertionPoint.insertBefore(fragment, insertionPoint.querySelector(pos));
81 insertionPoint.appendChild(fragment);
84 this._browser = aBrowser;
87 this._popup.addEventListener("command", this);
88 this._popup.addEventListener("popuphidden", this);
93 // Construct the XUL menu structure for a given JSON object.
94 buildXULMenu(aNode, aElementForAppending) {
95 let document = aElementForAppending.ownerDocument;
97 let children = aNode.children;
98 for (let child of children) {
100 switch (child.type) {
103 continue; // Ignore children without ids
106 menuitem = document.createXULElement("menuitem");
107 if (child.checkbox) {
108 menuitem.setAttribute("type", "checkbox");
110 menuitem.setAttribute("checked", "true");
115 menuitem.setAttribute("label", child.label);
118 menuitem.setAttribute("image", child.icon);
119 menuitem.className = "menuitem-iconic";
121 if (child.disabled) {
122 menuitem.setAttribute("disabled", true);
128 menuitem = document.createXULElement("menuseparator");
132 menuitem = document.createXULElement("menu");
134 menuitem.setAttribute("label", child.label);
137 let menupopup = document.createXULElement("menupopup");
138 menuitem.appendChild(menupopup);
140 this.buildXULMenu(child, menupopup);
144 menuitem.setAttribute(this.GENERATEDITEMID_ATTR, child.id ? child.id : 0);
145 aElementForAppending.appendChild(menuitem);
149 // Called when the generated menuitem is executed.
151 let type = event.type;
152 let target = event.target;
153 if (type == "command" && target.hasAttribute(this.GENERATEDITEMID_ATTR)) {
154 // If a builder is assigned, call click on it directly. Otherwise, this is
155 // likely a menu with data from another process, so send a message to the
156 // browser to execute the menuitem.
158 this._builder.click(target.getAttribute(this.GENERATEDITEMID_ATTR));
159 } else if (this._browser) {
160 let win = target.ownerGlobal;
161 let windowUtils = win.windowUtils;
162 win.gContextMenu.doCustomCommand(
163 target.getAttribute(this.GENERATEDITEMID_ATTR),
164 windowUtils.isHandlingUserInput
167 } else if (type == "popuphidden" && this._popup == target) {
168 this.removeGeneratedContent(this._popup);
170 this._popup.removeEventListener("popuphidden", this);
171 this._popup.removeEventListener("command", this);
174 this._builder = null;
175 this._browser = null;
179 // Get the first child of the given element with the given tag name.
180 getImmediateChild(element, tag) {
181 let child = element.firstElementChild;
183 if (child.localName == tag) {
186 child = child.nextElementSibling;
191 // Return the location where the generated items should be inserted into the
192 // given popup. They should be inserted as the next sibling of the returned
194 getInsertionPoint(aPopup) {
195 if (aPopup.hasAttribute(this.PAGEMENU_ATTR)) {
199 let element = aPopup.firstElementChild;
201 if (element.localName == "menu") {
202 let popup = this.getImmediateChild(element, "menupopup");
204 let result = this.getInsertionPoint(popup);
210 element = element.nextElementSibling;
216 // Remove the generated content from the given popup.
217 removeGeneratedContent(aPopup) {
218 let ungenerated = [];
219 ungenerated.push(aPopup);
222 while (0 != (count = ungenerated.length)) {
223 let last = count - 1;
224 let element = ungenerated[last];
225 ungenerated.splice(last, 1);
227 let i = element.children.length;
229 let child = element.children[i];
230 if (!child.hasAttribute(this.GENERATEDITEMID_ATTR)) {
231 ungenerated.push(child);
234 element.removeChild(child);
240 // This object is expected to be used from a parent process.
241 function PageMenuParent() {}
243 PageMenuParent.prototype = {
244 __proto__: PageMenu.prototype,
246 * Given a JSON menu object and popup, add the context menu to the popup.
247 * aBrowser should be the browser containing the page the context menu is
248 * displayed for, which may be null.
250 * Returns true if custom menu items were present.
252 addToPopup(aMenu, aBrowser, aPopup) {
253 return this.buildAndAttachMenuWithObject(aMenu, aBrowser, aPopup);
257 // This object is expected to be used from a child process.
258 function PageMenuChild() {}
260 PageMenuChild.prototype = {
261 __proto__: PageMenu.prototype,
264 * Given a target node, return a JSON object for the custom menu commands. The
265 * object will consist of a hierarchical structure of menus, menuitems or
266 * separators. Supported properties of each are:
267 * Menu: children, label, type="menu"
268 * Menuitems: checkbox, checked, disabled, icon, label, type="menuitem"
269 * Separators: type="separator"
271 * In addition, the id of each item will be used to identify the item
272 * when it is executed. The type will either be 'menu', 'menuitem' or
273 * 'separator'. The toplevel node will be a menu with a children property. The
274 * children property of a menu is an array of zero or more other items.
276 * If there is no menu associated with aTarget, null will be returned.
279 return this.maybeBuild(aTarget);
283 * Given the id of a menu, execute the command associated with that menu. It
284 * is assumed that only one command will be executed so the builder is
285 * cleared afterwards.
289 this._builder.click(aId);
290 this._builder = null;