tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

menu.js (6762B)


      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 
      5 "use strict";
      6 
      7 const DevToolsUtils = require("resource://devtools/shared/DevToolsUtils.js");
      8 const EventEmitter = require("resource://devtools/shared/event-emitter.js");
      9 
     10 /**
     11 * A partial implementation of the Menu API provided by electron:
     12 * https://github.com/electron/electron/blob/master/docs/api/menu.md.
     13 *
     14 * Extra features:
     15 *  - Emits an 'open' and 'close' event when the menu is opened/closed
     16 
     17 * @param String id (non standard)
     18 *        Needed so tests can confirm the XUL implementation is working
     19 */
     20 function Menu({ id = null } = {}) {
     21  this.menuitems = [];
     22  this.id = id;
     23 
     24  Object.defineProperty(this, "items", {
     25    get() {
     26      return this.menuitems;
     27    },
     28  });
     29 
     30  EventEmitter.decorate(this);
     31 }
     32 
     33 /**
     34 * Add an item to the end of the Menu
     35 *
     36 * @param {MenuItem} menuItem
     37 */
     38 Menu.prototype.append = function (menuItem) {
     39  this.menuitems.push(menuItem);
     40 };
     41 
     42 /**
     43 * Remove all items from the Menu
     44 */
     45 Menu.prototype.clear = function () {
     46  this.menuitems = [];
     47 };
     48 
     49 /**
     50 * Add an item to a specified position in the menu
     51 *
     52 * @param {int} _pos
     53 * @param {MenuItem} _menuItem
     54 */
     55 Menu.prototype.insert = function (_pos, _menuItem) {
     56  throw Error("Not implemented");
     57 };
     58 
     59 /**
     60 * Show the Menu next to the provided target. Anchor point is bottom-left.
     61 *
     62 * @param {Element} target
     63 *        The element to use as anchor.
     64 */
     65 Menu.prototype.popupAtTarget = function (target) {
     66  const rect = target.getBoundingClientRect();
     67  const doc = target.ownerDocument;
     68  const defaultView = doc.defaultView;
     69  const x = rect.left + defaultView.mozInnerScreenX;
     70  const y = rect.bottom + defaultView.mozInnerScreenY;
     71 
     72  this.popup(x, y, doc);
     73 };
     74 
     75 /**
     76 * Hide an existing menu, if there's any.
     77 *
     78 * @param {Document} doc
     79 *        The document that should own the context menu.
     80 */
     81 Menu.prototype.hide = function (doc) {
     82  const win = doc.defaultView;
     83  doc = DevToolsUtils.getTopWindow(win).document;
     84  const popup = doc.querySelector('popupset menupopup[menu-api="true"]');
     85  if (!popup) {
     86    return;
     87  }
     88  popup.hidePopup();
     89 };
     90 
     91 /**
     92 * Show the Menu at a specified location on the screen
     93 *
     94 * Missing features:
     95 *   - browserWindow - BrowserWindow (optional) - Default is null.
     96 *   - positioningItem Number - (optional) OS X
     97 *
     98 * @param {int} screenX
     99 * @param {int} screenY
    100 * @param {Document} doc
    101 *        The document that should own the context menu.
    102 */
    103 Menu.prototype.popup = function (screenX, screenY, doc) {
    104  // See bug 1285229, on Windows, opening the same popup multiple times in a
    105  // row ends up duplicating the popup. The newly inserted popup doesn't
    106  // dismiss the old one. So remove any previously displayed popup before
    107  // opening a new one.
    108  this.hide(doc);
    109 
    110  // The context-menu will be created in the topmost window to preserve keyboard
    111  // navigation (see Bug 1543940).
    112  // Keep a reference on the window owning the menu to hide the popup on unload.
    113  const win = doc.defaultView;
    114  const topWin = DevToolsUtils.getTopWindow(win);
    115 
    116  // Convert coordinates from win's CSS coordinate space to topWin's
    117  const winToTopWinCssScale = win.devicePixelRatio / topWin.devicePixelRatio;
    118  screenX = screenX * winToTopWinCssScale;
    119  screenY = screenY * winToTopWinCssScale;
    120 
    121  doc = topWin.document;
    122 
    123  let popupset = doc.querySelector("popupset");
    124  if (!popupset) {
    125    popupset = doc.createXULElement("popupset");
    126    doc.documentElement.appendChild(popupset);
    127  }
    128 
    129  const popup = doc.createXULElement("menupopup");
    130  popup.setAttribute("menu-api", "true");
    131  popup.setAttribute("consumeoutsideclicks", "false");
    132  popup.setAttribute("incontentshell", "false");
    133 
    134  if (this.id) {
    135    popup.id = this.id;
    136  }
    137  this._createMenuItems(popup);
    138 
    139  // The context menu will be created in the topmost chrome window. Hide it manually when
    140  // the owner document is unloaded.
    141  const onWindowUnload = () => popup.hidePopup();
    142  win.addEventListener("unload", onWindowUnload);
    143 
    144  // Remove the menu from the DOM once it's hidden.
    145  popup.addEventListener("popuphidden", e => {
    146    if (e.target === popup) {
    147      win.removeEventListener("unload", onWindowUnload);
    148      popup.remove();
    149      this.emit("close");
    150    }
    151  });
    152 
    153  popup.addEventListener("popupshown", e => {
    154    if (e.target === popup) {
    155      this.emit("open");
    156    }
    157  });
    158 
    159  popupset.appendChild(popup);
    160  popup.openPopupAtScreen(screenX, screenY, true);
    161 };
    162 
    163 Menu.prototype._createMenuItems = function (parent) {
    164  const doc = parent.ownerDocument;
    165  this.menuitems.forEach(item => {
    166    if (!item.visible) {
    167      return;
    168    }
    169 
    170    if (item.submenu) {
    171      const menupopup = doc.createXULElement("menupopup");
    172      menupopup.setAttribute("incontentshell", "false");
    173 
    174      item.submenu._createMenuItems(menupopup);
    175 
    176      const menu = doc.createXULElement("menu");
    177      menu.appendChild(menupopup);
    178      applyItemAttributesToNode(item, menu);
    179      parent.appendChild(menu);
    180    } else if (item.type === "separator") {
    181      const menusep = doc.createXULElement("menuseparator");
    182      parent.appendChild(menusep);
    183    } else {
    184      const menuitem = doc.createXULElement("menuitem");
    185      applyItemAttributesToNode(item, menuitem);
    186 
    187      menuitem.addEventListener("command", () => {
    188        item.click();
    189      });
    190      menuitem.addEventListener("DOMMenuItemActive", () => {
    191        item.hover();
    192      });
    193 
    194      parent.appendChild(menuitem);
    195    }
    196  });
    197 };
    198 
    199 Menu.getMenuElementById = function (id, doc) {
    200  const menuDoc = DevToolsUtils.getTopWindow(doc.defaultView).document;
    201  return menuDoc.getElementById(id);
    202 };
    203 
    204 Menu.setApplicationMenu = () => {
    205  throw Error("Not implemented");
    206 };
    207 
    208 Menu.sendActionToFirstResponder = () => {
    209  throw Error("Not implemented");
    210 };
    211 
    212 Menu.buildFromTemplate = () => {
    213  throw Error("Not implemented");
    214 };
    215 
    216 function applyItemAttributesToNode(item, node) {
    217  if (item.l10nID) {
    218    node.ownerDocument.l10n.setAttributes(node, item.l10nID);
    219  } else {
    220    node.setAttribute("label", item.label);
    221    if (item.accelerator) {
    222      node.setAttribute("acceltext", item.accelerator);
    223    }
    224    if (item.accesskey) {
    225      node.setAttribute("accesskey", item.accesskey);
    226    }
    227  }
    228  if (item.type === "checkbox") {
    229    node.setAttribute("type", "checkbox");
    230  }
    231  if (item.type === "radio") {
    232    node.setAttribute("type", "radio");
    233  }
    234  if (item.disabled) {
    235    node.setAttribute("disabled", "true");
    236  }
    237  if (item.checked) {
    238    node.setAttribute("checked", "true");
    239  }
    240  if (item.image) {
    241    node.setAttribute("image", item.image);
    242  }
    243  if (item.id) {
    244    node.id = item.id;
    245  }
    246 }
    247 
    248 module.exports = Menu;