forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			248 lines
		
	
	
	
		
			6.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			248 lines
		
	
	
	
		
			6.6 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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/. */
 | |
| 
 | |
| "use strict";
 | |
| 
 | |
| const DevToolsUtils = require("devtools/shared/DevToolsUtils");
 | |
| const EventEmitter = require("devtools/shared/event-emitter");
 | |
| 
 | |
| /**
 | |
|  * A partial implementation of the Menu API provided by electron:
 | |
|  * https://github.com/electron/electron/blob/master/docs/api/menu.md.
 | |
|  *
 | |
|  * Extra features:
 | |
|  *  - Emits an 'open' and 'close' event when the menu is opened/closed
 | |
| 
 | |
|  * @param String id (non standard)
 | |
|  *        Needed so tests can confirm the XUL implementation is working
 | |
|  */
 | |
| function Menu({ id = null } = {}) {
 | |
|   this.menuitems = [];
 | |
|   this.id = id;
 | |
| 
 | |
|   Object.defineProperty(this, "items", {
 | |
|     get() {
 | |
|       return this.menuitems;
 | |
|     },
 | |
|   });
 | |
| 
 | |
|   EventEmitter.decorate(this);
 | |
| }
 | |
| 
 | |
| /**
 | |
|  * Add an item to the end of the Menu
 | |
|  *
 | |
|  * @param {MenuItem} menuItem
 | |
|  */
 | |
| Menu.prototype.append = function(menuItem) {
 | |
|   this.menuitems.push(menuItem);
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Remove all items from the Menu
 | |
|  */
 | |
| Menu.prototype.clear = function() {
 | |
|   this.menuitems = [];
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Add an item to a specified position in the menu
 | |
|  *
 | |
|  * @param {int} pos
 | |
|  * @param {MenuItem} menuItem
 | |
|  */
 | |
| Menu.prototype.insert = function(pos, menuItem) {
 | |
|   throw Error("Not implemented");
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Show the Menu next to the provided target. Anchor point is bottom-left.
 | |
|  *
 | |
|  * @param {Element} target
 | |
|  *        The element to use as anchor.
 | |
|  */
 | |
| Menu.prototype.popupAtTarget = function(target) {
 | |
|   const rect = target.getBoundingClientRect();
 | |
|   const doc = target.ownerDocument;
 | |
|   const defaultView = doc.defaultView;
 | |
|   const x = rect.left + defaultView.mozInnerScreenX;
 | |
|   const y = rect.bottom + defaultView.mozInnerScreenY;
 | |
| 
 | |
|   this.popup(x, y, doc);
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Hide an existing menu, if there's any.
 | |
|  *
 | |
|  * @param {Document} doc
 | |
|  *        The document that should own the context menu.
 | |
|  */
 | |
| Menu.prototype.hide = function(doc) {
 | |
|   const win = doc.defaultView;
 | |
|   doc = DevToolsUtils.getTopWindow(win).document;
 | |
|   const popup = doc.querySelector('popupset menupopup[menu-api="true"]');
 | |
|   if (!popup) {
 | |
|     return;
 | |
|   }
 | |
|   popup.hidePopup();
 | |
| };
 | |
| 
 | |
| /**
 | |
|  * Show the Menu at a specified location on the screen
 | |
|  *
 | |
|  * Missing features:
 | |
|  *   - browserWindow - BrowserWindow (optional) - Default is null.
 | |
|  *   - positioningItem Number - (optional) OS X
 | |
|  *
 | |
|  * @param {int} screenX
 | |
|  * @param {int} screenY
 | |
|  * @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.setAttribute("data-l10n-id", 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;
 | 
