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/. */ 4 5var EXPORTED_SYMBOLS = ["PageMenuParent", "PageMenuChild"]; 6 7function PageMenu() {} 8 9PageMenu.prototype = { 10 PAGEMENU_ATTR: "pagemenu", 11 GENERATEDITEMID_ATTR: "generateditemid", 12 13 _popup: null, 14 15 // Only one of builder or browser will end up getting set. 16 _builder: null, 17 _browser: null, 18 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 } 29 30 return null; 31 }, 32 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 } 40 41 pageMenu.sendShowEvent(); 42 // the show event is not cancelable, so no need to check a result here 43 44 this._builder = pageMenu.createBuilder(); 45 if (!this._builder) { 46 return null; 47 } 48 49 pageMenu.build(this._builder); 50 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 } 57 58 return JSON.parse(menuString); 59 }, 60 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 } 66 67 let insertionPoint = this.getInsertionPoint(aPopup); 68 if (!insertionPoint) { 69 return false; 70 } 71 72 let fragment = aPopup.ownerDocument.createDocumentFragment(); 73 this.buildXULMenu(aMenu, fragment); 74 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 } 83 84 this._browser = aBrowser; 85 this._popup = aPopup; 86 87 this._popup.addEventListener("command", this); 88 this._popup.addEventListener("popuphidden", this); 89 90 return true; 91 }, 92 93 // Construct the XUL menu structure for a given JSON object. 94 buildXULMenu(aNode, aElementForAppending) { 95 let document = aElementForAppending.ownerDocument; 96 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 } 105 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 } 113 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 } 124 125 break; 126 127 case "separator": 128 menuitem = document.createXULElement("menuseparator"); 129 break; 130 131 case "menu": 132 menuitem = document.createXULElement("menu"); 133 if (child.label) { 134 menuitem.setAttribute("label", child.label); 135 } 136 137 let menupopup = document.createXULElement("menupopup"); 138 menuitem.appendChild(menupopup); 139 140 this.buildXULMenu(child, menupopup); 141 break; 142 } 143 144 menuitem.setAttribute(this.GENERATEDITEMID_ATTR, child.id ? child.id : 0); 145 aElementForAppending.appendChild(menuitem); 146 } 147 }, 148 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); 169 170 this._popup.removeEventListener("popuphidden", this); 171 this._popup.removeEventListener("command", this); 172 173 this._popup = null; 174 this._builder = null; 175 this._browser = null; 176 } 177 }, 178 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 }, 190 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 } 198 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 } 212 213 return null; 214 }, 215 216 // Remove the generated content from the given popup. 217 removeGeneratedContent(aPopup) { 218 let ungenerated = []; 219 ungenerated.push(aPopup); 220 221 let count; 222 while (0 != (count = ungenerated.length)) { 223 let last = count - 1; 224 let element = ungenerated[last]; 225 ungenerated.splice(last, 1); 226 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 }, 238}; 239 240// This object is expected to be used from a parent process. 241function PageMenuParent() {} 242 243PageMenuParent.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 }, 255}; 256 257// This object is expected to be used from a child process. 258function PageMenuChild() {} 259 260PageMenuChild.prototype = { 261 __proto__: PageMenu.prototype, 262 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 }, 281 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 }, 293}; 294