Y * @param {Document} doc * The document that should own the context menu. */ Menu.prototype.popup = function (screenX, screenY, doc) { // See bug 1285229, on Windows, opening the same popup multiple times in a // row ends up duplicating the popup. The newly inserted popup doesn't // dismiss the old one. So remove any previously displayed popup before // opening a new one. this.hide(doc); // The context-menu will be created in the topmost window to preserve keyboard // navigation (see Bug 1543940). // Keep a reference on the window owning the menu to hide the popup on unload. const win = doc.defaultView; const topWin = DevToolsUtils.getTopWindow(win); // Convert coordinates from win's CSS coordinate space to topWin's const winToTopWinCssScale = win.devicePixelRatio / topWin.devicePixelRatio; screenX = screenX * winToTopWinCssScale; screenY = screenY * winToTopWinCssScale; doc = topWin.document; let popupset = doc.querySelector("popupset"); if (!popupset) { popupset = doc.createXULElement("popupset"); doc.documentElement.appendChild(popupset); } const popup = doc.createXULElement("menupopup"); popup.setAttribute("menu-api", "true"); popup.setAttribute("consumeoutsideclicks", "false"); popup.setAttribute("incontentshell", "false"); if (this.id) { popup.id = this.id; } this._createMenuItems(popup); // The context menu will be created in the topmost chrome window. Hide it manually when // the owner document is unloaded. const onWindowUnload = () => popup.hidePopup(); win.addEventListener("unload", onWindowUnload); // Remove the menu from the DOM once it's hidden. popup.addEventListener("popuphidden", e => { if (e.target === popup) { win.removeEventListener("unload", onWindowUnload); popup.remove(); this.emit("close"); } }); popup.addEventListener("popupshown", e => { if (e.target === popup) { this.emit("open"); } }); popupset.appendChild(popup); popup.openPopupAtScreen(screenX, screenY, true); }; Menu.prototype._createMenuItems = function (parent) { const doc = parent.ownerDocument; this.menuitems.forEach(item => { if (!item.visible) { return; } if (item.submenu) { const menupopup = doc.createXULElement("menupopup"); menupopup.setAttribute("incontentshell", "false"); item.submenu._createMenuItems(menupopup); const menu = doc.createXULElement("menu"); menu.appendChild(menupopup); applyItemAttributesToNode(item, menu); parent.appendChild(menu); } else if (item.type === "separator") { const menusep = doc.createXULElement("menuseparator"); parent.appendChild(menusep); } else { const menuitem = doc.createXULElement("menuitem"); applyItemAttributesToNode(item, menuitem); menuitem.addEventListener("command", () => { item.click(); }); menuitem.addEventListener("DOMMenuItemActive", () => { item.hover(); }); parent.appendChild(menuitem); } }); }; Menu.getMenuElementById = function (id, doc) { const menuDoc = DevToolsUtils.getTopWindow(doc.defaultView).document; return menuDoc.getElementById(id); }; Menu.setApplicationMenu = () => { throw Error("Not implemented"); }; Menu.sendActionToFirstResponder = () => { throw Error("Not implemented"); }; Menu.buildFromTemplate = () => { throw Error("Not implemented"); }; function applyItemAttributesToNode(item, node) { if (item.l10nID) { node.ownerDocument.l10n.setAttributes(node, item.l10nID); } else { node.setAttribute("label", item.label); if (item.accelerator) { node.setAttribute("acceltext", item.accelerator); } if (item.accesskey) { node.setAttribute("accesskey", item.accesskey); } } if (item.type === "checkbox") { node.setAttribute("type", "checkbox"); } if (item.type === "radio") { node.setAttribute("type", "radio"); } if (item.disabled) { node.setAttribute("disabled", "true"); } if (item.checked) { node.setAttribute("checked", "true"); } if (item.image) { node.setAttribute("image", item.image); } if (item.id) { node.id = item.id; } } module.exports = Menu; PK