Bug 1625482 [wpt PR 22496] - [ScrollTimeline] Do not show scrollbar to bypass flakine...
[gecko.git] / toolkit / modules / PageMenu.jsm
blob2c96600973093e2c3daf47272786e90b08c55ff2
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"];
7 function PageMenu() {}
9 PageMenu.prototype = {
10   PAGEMENU_ATTR: "pagemenu",
11   GENERATEDITEMID_ATTR: "generateditemid",
13   _popup: null,
15   // Only one of builder or browser will end up getting set.
16   _builder: null,
17   _browser: null,
19   // Given a target node, get the context menu for it or its ancestor.
20   getContextMenu(aTarget) {
21     let target = aTarget;
22     while (target) {
23       let contextMenu = target.contextMenu;
24       if (contextMenu) {
25         return contextMenu;
26       }
27       target = target.parentNode;
28     }
30     return null;
31   },
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.
35   maybeBuild(aTarget) {
36     let pageMenu = this.getContextMenu(aTarget);
37     if (!pageMenu) {
38       return null;
39     }
41     pageMenu.sendShowEvent();
42     // the show event is not cancelable, so no need to check a result here
44     this._builder = pageMenu.createBuilder();
45     if (!this._builder) {
46       return null;
47     }
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();
54     if (!menuString) {
55       return null;
56     }
58     return JSON.parse(menuString);
59   },
61   // Given a JSON menu object and popup, add the context menu to the popup.
62   buildAndAttachMenuWithObject(aMenu, aBrowser, aPopup) {
63     if (!aMenu) {
64       return false;
65     }
67     let insertionPoint = this.getInsertionPoint(aPopup);
68     if (!insertionPoint) {
69       return false;
70     }
72     let fragment = aPopup.ownerDocument.createDocumentFragment();
73     this.buildXULMenu(aMenu, fragment);
75     let pos = insertionPoint.getAttribute(this.PAGEMENU_ATTR);
76     if (pos == "start") {
77       insertionPoint.insertBefore(fragment, insertionPoint.firstElementChild);
78     } else if (pos.startsWith("#")) {
79       insertionPoint.insertBefore(fragment, insertionPoint.querySelector(pos));
80     } else {
81       insertionPoint.appendChild(fragment);
82     }
84     this._browser = aBrowser;
85     this._popup = aPopup;
87     this._popup.addEventListener("command", this);
88     this._popup.addEventListener("popuphidden", this);
90     return true;
91   },
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) {
99       let menuitem;
100       switch (child.type) {
101         case "menuitem":
102           if (!child.id) {
103             continue; // Ignore children without ids
104           }
106           menuitem = document.createXULElement("menuitem");
107           if (child.checkbox) {
108             menuitem.setAttribute("type", "checkbox");
109             if (child.checked) {
110               menuitem.setAttribute("checked", "true");
111             }
112           }
114           if (child.label) {
115             menuitem.setAttribute("label", child.label);
116           }
117           if (child.icon) {
118             menuitem.setAttribute("image", child.icon);
119             menuitem.className = "menuitem-iconic";
120           }
121           if (child.disabled) {
122             menuitem.setAttribute("disabled", true);
123           }
125           break;
127         case "separator":
128           menuitem = document.createXULElement("menuseparator");
129           break;
131         case "menu":
132           menuitem = document.createXULElement("menu");
133           if (child.label) {
134             menuitem.setAttribute("label", child.label);
135           }
137           let menupopup = document.createXULElement("menupopup");
138           menuitem.appendChild(menupopup);
140           this.buildXULMenu(child, menupopup);
141           break;
142       }
144       menuitem.setAttribute(this.GENERATEDITEMID_ATTR, child.id ? child.id : 0);
145       aElementForAppending.appendChild(menuitem);
146     }
147   },
149   // Called when the generated menuitem is executed.
150   handleEvent(event) {
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.
157       if (this._builder) {
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
165         );
166       }
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);
173       this._popup = null;
174       this._builder = null;
175       this._browser = null;
176     }
177   },
179   // Get the first child of the given element with the given tag name.
180   getImmediateChild(element, tag) {
181     let child = element.firstElementChild;
182     while (child) {
183       if (child.localName == tag) {
184         return child;
185       }
186       child = child.nextElementSibling;
187     }
188     return null;
189   },
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
193   // element.
194   getInsertionPoint(aPopup) {
195     if (aPopup.hasAttribute(this.PAGEMENU_ATTR)) {
196       return aPopup;
197     }
199     let element = aPopup.firstElementChild;
200     while (element) {
201       if (element.localName == "menu") {
202         let popup = this.getImmediateChild(element, "menupopup");
203         if (popup) {
204           let result = this.getInsertionPoint(popup);
205           if (result) {
206             return result;
207           }
208         }
209       }
210       element = element.nextElementSibling;
211     }
213     return null;
214   },
216   // Remove the generated content from the given popup.
217   removeGeneratedContent(aPopup) {
218     let ungenerated = [];
219     ungenerated.push(aPopup);
221     let count;
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;
228       while (i-- > 0) {
229         let child = element.children[i];
230         if (!child.hasAttribute(this.GENERATEDITEMID_ATTR)) {
231           ungenerated.push(child);
232           continue;
233         }
234         element.removeChild(child);
235       }
236     }
237   },
240 // This object is expected to be used from a parent process.
241 function PageMenuParent() {}
243 PageMenuParent.prototype = {
244   __proto__: PageMenu.prototype,
245   /*
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.
249    *
250    * Returns true if custom menu items were present.
251    */
252   addToPopup(aMenu, aBrowser, aPopup) {
253     return this.buildAndAttachMenuWithObject(aMenu, aBrowser, aPopup);
254   },
257 // This object is expected to be used from a child process.
258 function PageMenuChild() {}
260 PageMenuChild.prototype = {
261   __proto__: PageMenu.prototype,
263   /*
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"
270    *
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.
275    *
276    * If there is no menu associated with aTarget, null will be returned.
277    */
278   build(aTarget) {
279     return this.maybeBuild(aTarget);
280   },
282   /*
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.
286    */
287   executeMenu(aId) {
288     if (this._builder) {
289       this._builder.click(aId);
290       this._builder = null;
291     }
292   },