tor-browser

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

browserPlacesViews.js (71670B)


      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 file,
      3 * You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 /**
      6 * The base view implements everything that's common to all the views.
      7 * It should not be instanced directly, use a derived class instead.
      8 */
      9 class PlacesViewBase {
     10  /**
     11   * @param {string} placesUrl
     12   *   The query string associated with the view.
     13   * @param {DOMElement} rootElt
     14   *   The root element for the view.
     15   * @param {DOMElement} viewElt
     16   *   The view element.
     17   */
     18  constructor(placesUrl, rootElt, viewElt) {
     19    this._rootElt = rootElt;
     20    this._viewElt = viewElt;
     21    // Do initialization in subclass now that `this` exists.
     22    this._init?.();
     23    this._controller = new PlacesController(this);
     24    this.place = placesUrl;
     25    this._viewElt.controllers.appendController(this._controller);
     26  }
     27 
     28  // The xul element that holds the entire view.
     29  _viewElt = null;
     30 
     31  get associatedElement() {
     32    return this._viewElt;
     33  }
     34 
     35  get controllers() {
     36    return this._viewElt.controllers;
     37  }
     38 
     39  // The xul element that represents the root container.
     40  _rootElt = null;
     41 
     42  get rootElement() {
     43    return this._rootElt;
     44  }
     45 
     46  // Set to true for views that are represented by native widgets (i.e.
     47  // the native mac menu).
     48  _nativeView = false;
     49 
     50  static interfaces = [
     51    Ci.nsINavHistoryResultObserver,
     52    Ci.nsISupportsWeakReference,
     53  ];
     54 
     55  QueryInterface = ChromeUtils.generateQI(PlacesViewBase.interfaces);
     56 
     57  _place = "";
     58  get place() {
     59    return this._place;
     60  }
     61  set place(val) {
     62    this._place = val;
     63 
     64    let history = PlacesUtils.history;
     65    let query = {},
     66      options = {};
     67    history.queryStringToQuery(val, query, options);
     68    let result = history.executeQuery(query.value, options.value);
     69    result.addObserver(this);
     70  }
     71 
     72  _result = null;
     73  get result() {
     74    return this._result;
     75  }
     76  set result(val) {
     77    if (this._result == val) {
     78      return;
     79    }
     80 
     81    if (this._result) {
     82      this._result.removeObserver(this);
     83      this._resultNode.containerOpen = false;
     84    }
     85 
     86    if (this._rootElt.localName == "menupopup") {
     87      this._rootElt._built = false;
     88    }
     89 
     90    this._result = val;
     91    if (val) {
     92      this._resultNode = val.root;
     93      this._rootElt._placesNode = this._resultNode;
     94      this._domNodes = new Map();
     95      this._domNodes.set(this._resultNode, this._rootElt);
     96 
     97      // This calls _rebuild through invalidateContainer.
     98      this._resultNode.containerOpen = true;
     99    } else {
    100      this._resultNode = null;
    101      delete this._domNodes;
    102    }
    103  }
    104 
    105  /**
    106   * Gets the DOM node used for the given places node.
    107   *
    108   * @param {object} aPlacesNode
    109   *        a places result node.
    110   * @param {boolean} aAllowMissing
    111   *        whether the node may be missing
    112   * @returns {object|null} The associated DOM node.
    113   * @throws if there is no DOM node set for aPlacesNode.
    114   */
    115  _getDOMNodeForPlacesNode(aPlacesNode, aAllowMissing = false) {
    116    let node = this._domNodes.get(aPlacesNode, null);
    117    if (!node && !aAllowMissing) {
    118      throw new Error(
    119        "No DOM node set for aPlacesNode.\nnode.type: " +
    120          aPlacesNode.type +
    121          ". node.parent: " +
    122          aPlacesNode
    123      );
    124    }
    125    return node;
    126  }
    127 
    128  get controller() {
    129    return this._controller;
    130  }
    131 
    132  get selType() {
    133    return "single";
    134  }
    135  selectItems() {}
    136  selectAll() {}
    137 
    138  get selectedNode() {
    139    if (this._contextMenuShown) {
    140      let anchor = this._contextMenuShown.triggerNode;
    141      if (!anchor) {
    142        return null;
    143      }
    144 
    145      if (anchor._placesNode) {
    146        return this._rootElt == anchor ? null : anchor._placesNode;
    147      }
    148 
    149      anchor = anchor.parentNode;
    150      return this._rootElt == anchor ? null : anchor._placesNode || null;
    151    }
    152    return null;
    153  }
    154 
    155  get hasSelection() {
    156    return this.selectedNode != null;
    157  }
    158 
    159  get selectedNodes() {
    160    let selectedNode = this.selectedNode;
    161    return selectedNode ? [selectedNode] : [];
    162  }
    163 
    164  get singleClickOpens() {
    165    return true;
    166  }
    167 
    168  get removableSelectionRanges() {
    169    // On static content the current selectedNode would be the selection's
    170    // parent node. We don't want to allow removing a node when the
    171    // selection is not explicit.
    172    let popupNode = PlacesUIUtils.lastContextMenuTriggerNode;
    173    if (popupNode && (popupNode == "menupopup" || !popupNode._placesNode)) {
    174      return [];
    175    }
    176 
    177    return [this.selectedNodes];
    178  }
    179 
    180  get draggableSelection() {
    181    return [this._draggedElt];
    182  }
    183 
    184  get insertionPoint() {
    185    // There is no insertion point for history queries, so bail out now and
    186    // save a lot of work when updating commands.
    187    let resultNode = this._resultNode;
    188    if (
    189      PlacesUtils.nodeIsQuery(resultNode) &&
    190      PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
    191        Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY
    192    ) {
    193      return null;
    194    }
    195 
    196    // By default, the insertion point is at the top level, at the end.
    197    let index = PlacesUtils.bookmarks.DEFAULT_INDEX;
    198    let container = this._resultNode;
    199    let orientation = Ci.nsITreeView.DROP_BEFORE;
    200    let tagName = null;
    201 
    202    let selectedNode = this.selectedNode;
    203    if (selectedNode) {
    204      let popupNode = PlacesUIUtils.lastContextMenuTriggerNode;
    205      if (
    206        !popupNode._placesNode ||
    207        popupNode._placesNode == this._resultNode ||
    208        popupNode._placesNode.itemId == -1 ||
    209        !selectedNode.parent
    210      ) {
    211        // If a static menuitem is selected, or if the root node is selected,
    212        // the insertion point is inside the folder, at the end.
    213        container = selectedNode;
    214        orientation = Ci.nsITreeView.DROP_ON;
    215      } else {
    216        // In all other cases the insertion point is before that node.
    217        container = selectedNode.parent;
    218        index = container.getChildIndex(selectedNode);
    219        if (PlacesUtils.nodeIsTagQuery(container)) {
    220          tagName = PlacesUtils.asQuery(container).query.tags[0];
    221        }
    222      }
    223    }
    224 
    225    if (this.controller.disallowInsertion(container)) {
    226      return null;
    227    }
    228 
    229    return new PlacesInsertionPoint({
    230      parentGuid: PlacesUtils.getConcreteItemGuid(container),
    231      index,
    232      orientation,
    233      tagName,
    234    });
    235  }
    236 
    237  buildContextMenu(aPopup) {
    238    this._contextMenuShown = aPopup;
    239    window.updateCommands("places");
    240 
    241    // Ensure that an existing "Show Other Bookmarks" item is removed before adding it
    242    // again.
    243    let existingOtherBookmarksItem = aPopup.querySelector(
    244      "#show-other-bookmarks_PersonalToolbar"
    245    );
    246    existingOtherBookmarksItem?.remove();
    247 
    248    let manageBookmarksMenu = aPopup.querySelector(
    249      "#placesContext_showAllBookmarks"
    250    );
    251    // Add the View menu for the Bookmarks Toolbar and "Show Other Bookmarks" menu item
    252    // if the click originated from the Bookmarks Toolbar.
    253    let existingSubmenu = aPopup.querySelector("#toggle_PersonalToolbar");
    254    existingSubmenu?.remove();
    255    let bookmarksToolbar = document.getElementById("PersonalToolbar");
    256    if (bookmarksToolbar?.contains(aPopup.triggerNode)) {
    257      manageBookmarksMenu.removeAttribute("hidden");
    258 
    259      let menu = BookmarkingUI.buildBookmarksToolbarSubmenu(bookmarksToolbar);
    260      aPopup.insertBefore(menu, manageBookmarksMenu);
    261 
    262      if (
    263        aPopup.triggerNode.id === "OtherBookmarks" ||
    264        aPopup.triggerNode.id === "PlacesChevron" ||
    265        aPopup.triggerNode.id === "PlacesToolbarItems" ||
    266        aPopup.triggerNode.parentNode.id === "PlacesToolbarItems"
    267      ) {
    268        let otherBookmarksMenuItem =
    269          BookmarkingUI.buildShowOtherBookmarksMenuItem();
    270 
    271        if (otherBookmarksMenuItem) {
    272          aPopup.insertBefore(otherBookmarksMenuItem, menu.nextElementSibling);
    273        }
    274      }
    275    } else {
    276      manageBookmarksMenu.setAttribute("hidden", "true");
    277    }
    278 
    279    return this.controller.buildContextMenu(aPopup);
    280  }
    281 
    282  destroyContextMenu() {
    283    this._contextMenuShown = null;
    284  }
    285 
    286  clearAllContents(aPopup) {
    287    let kid = aPopup.firstElementChild;
    288    while (kid) {
    289      let next = kid.nextElementSibling;
    290      if (!kid.classList.contains("panel-header")) {
    291        kid.remove();
    292      }
    293      kid = next;
    294    }
    295    aPopup._emptyMenuitem = aPopup._startMarker = aPopup._endMarker = null;
    296  }
    297 
    298  _cleanPopup(aPopup, aDelay) {
    299    // Ensure markers are here when `invalidateContainer` is called before the
    300    // popup is shown, which may the case for panelviews, for example.
    301    this._ensureMarkers(aPopup);
    302    // Remove Places nodes from the popup.
    303    let child = aPopup._startMarker;
    304    while (child.nextElementSibling != aPopup._endMarker) {
    305      let sibling = child.nextElementSibling;
    306      if (sibling._placesNode && !aDelay) {
    307        aPopup.removeChild(sibling);
    308      } else if (sibling._placesNode && aDelay) {
    309        // HACK (bug 733419): the popups originating from the OS X native
    310        // menubar don't live-update while open, thus we don't clean it
    311        // until the next popupshowing, to avoid zombie menuitems.
    312        if (!aPopup._delayedRemovals) {
    313          aPopup._delayedRemovals = [];
    314        }
    315        aPopup._delayedRemovals.push(sibling);
    316        child = child.nextElementSibling;
    317      } else {
    318        child = child.nextElementSibling;
    319      }
    320    }
    321  }
    322 
    323  _rebuildPopup(aPopup) {
    324    let resultNode = aPopup._placesNode;
    325    if (!resultNode.containerOpen) {
    326      return;
    327    }
    328 
    329    this._cleanPopup(aPopup);
    330 
    331    let cc = resultNode.childCount;
    332    if (cc > 0) {
    333      this._setEmptyPopupStatus(aPopup, false);
    334      let fragment = document.createDocumentFragment();
    335      for (let i = 0; i < cc; ++i) {
    336        let child = resultNode.getChild(i);
    337        this._insertNewItemToPopup(child, fragment);
    338      }
    339      aPopup.insertBefore(fragment, aPopup._endMarker);
    340    } else {
    341      this._setEmptyPopupStatus(aPopup, true);
    342    }
    343    aPopup._built = true;
    344  }
    345 
    346  _removeChild(aChild) {
    347    aChild.remove();
    348  }
    349 
    350  _setEmptyPopupStatus(aPopup, aEmpty) {
    351    if (!aPopup._emptyMenuitem) {
    352      aPopup._emptyMenuitem = document.createXULElement("menuitem");
    353      aPopup._emptyMenuitem.setAttribute("disabled", true);
    354      aPopup._emptyMenuitem.className = "bookmark-item";
    355      document.l10n.setAttributes(
    356        aPopup._emptyMenuitem,
    357        "places-empty-bookmarks-folder"
    358      );
    359    }
    360 
    361    if (aEmpty) {
    362      aPopup.setAttribute("emptyplacesresult", "true");
    363      // Don't add the menuitem if there is static content.
    364      if (
    365        !aPopup._startMarker.previousElementSibling &&
    366        !aPopup._endMarker.nextElementSibling
    367      ) {
    368        aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker);
    369      }
    370    } else {
    371      aPopup.removeAttribute("emptyplacesresult");
    372      try {
    373        aPopup.removeChild(aPopup._emptyMenuitem);
    374      } catch (ex) {}
    375    }
    376  }
    377 
    378  _createDOMNodeForPlacesNode(aPlacesNode) {
    379    this._domNodes.delete(aPlacesNode);
    380 
    381    let element;
    382    let type = aPlacesNode.type;
    383    if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
    384      element = document.createXULElement("menuseparator");
    385    } else {
    386      if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
    387        element = document.createXULElement("menuitem");
    388        element.className =
    389          "menuitem-iconic bookmark-item menuitem-with-favicon";
    390        element.setAttribute(
    391          "scheme",
    392          PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri)
    393        );
    394      } else if (PlacesUtils.containerTypes.includes(type)) {
    395        element = document.createXULElement("menu");
    396        element.setAttribute("container", "true");
    397 
    398        if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
    399          element.setAttribute("query", "true");
    400          if (PlacesUtils.nodeIsTagQuery(aPlacesNode)) {
    401            element.setAttribute("tagContainer", "true");
    402          } else if (PlacesUtils.nodeIsDay(aPlacesNode)) {
    403            element.setAttribute("dayContainer", "true");
    404          } else if (PlacesUtils.nodeIsHost(aPlacesNode)) {
    405            element.setAttribute("hostContainer", "true");
    406          }
    407        }
    408 
    409        let popup = document.createXULElement("menupopup", {
    410          is: "places-popup",
    411        });
    412        popup._placesNode = PlacesUtils.asContainer(aPlacesNode);
    413 
    414        if (!this._nativeView) {
    415          popup.setAttribute("placespopup", "true");
    416        }
    417 
    418        element.appendChild(popup);
    419        element.className = "menu-iconic bookmark-item";
    420 
    421        this._domNodes.set(aPlacesNode, popup);
    422      } else {
    423        throw new Error("Unexpected node");
    424      }
    425 
    426      element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
    427 
    428      let icon = aPlacesNode.icon;
    429      if (icon) {
    430        element.setAttribute("image", ChromeUtils.encodeURIForSrcset(icon));
    431      }
    432    }
    433 
    434    element._placesNode = aPlacesNode;
    435    if (!this._domNodes.has(aPlacesNode)) {
    436      this._domNodes.set(aPlacesNode, element);
    437    }
    438 
    439    return element;
    440  }
    441 
    442  _insertNewItemToPopup(aNewChild, aInsertionNode, aBefore = null) {
    443    let element = this._createDOMNodeForPlacesNode(aNewChild);
    444 
    445    aInsertionNode.insertBefore(element, aBefore);
    446    return element;
    447  }
    448 
    449  toggleCutNode(aPlacesNode, aValue) {
    450    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
    451 
    452    // We may get the popup for menus, but we need the menu itself.
    453    if (elt.localName == "menupopup") {
    454      elt = elt.parentNode;
    455    }
    456    if (aValue) {
    457      elt.setAttribute("cutting", "true");
    458    } else {
    459      elt.removeAttribute("cutting");
    460    }
    461  }
    462 
    463  nodeURIChanged(aPlacesNode) {
    464    let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
    465 
    466    // There's no DOM node, thus there's nothing to be done when the URI changes.
    467    if (!elt) {
    468      return;
    469    }
    470 
    471    // Here we need the <menu>.
    472    if (elt.localName == "menupopup") {
    473      elt = elt.parentNode;
    474    }
    475 
    476    elt.setAttribute(
    477      "scheme",
    478      PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri)
    479    );
    480  }
    481 
    482  nodeIconChanged(aPlacesNode) {
    483    let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
    484 
    485    // There's no UI representation for the root node, or there's no DOM node,
    486    // thus there's nothing to be done when the icon changes.
    487    if (!elt || elt == this._rootElt) {
    488      return;
    489    }
    490 
    491    // Here we need the <menu>.
    492    if (elt.localName == "menupopup") {
    493      elt = elt.parentNode;
    494    }
    495    // We must remove and reset the attribute to force an update.
    496    elt.removeAttribute("image");
    497    elt.setAttribute("image", ChromeUtils.encodeURIForSrcset(aPlacesNode.icon));
    498  }
    499 
    500  nodeTitleChanged(aPlacesNode, aNewTitle) {
    501    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
    502 
    503    // There's no UI representation for the root node, thus there's
    504    // nothing to be done when the title changes.
    505    if (elt == this._rootElt) {
    506      return;
    507    }
    508 
    509    // Here we need the <menu>.
    510    if (elt.localName == "menupopup") {
    511      elt = elt.parentNode;
    512    }
    513 
    514    if (!aNewTitle && elt.localName != "toolbarbutton") {
    515      // Many users consider toolbars as shortcuts containers, so explicitly
    516      // allow empty labels on toolbarbuttons.  For any other element try to be
    517      // smarter, guessing a title from the uri.
    518      elt.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
    519    } else {
    520      elt.setAttribute("label", aNewTitle);
    521    }
    522  }
    523 
    524  nodeRemoved(aParentPlacesNode, aPlacesNode) {
    525    let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
    526    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
    527 
    528    // Here we need the <menu>.
    529    if (elt.localName == "menupopup") {
    530      elt = elt.parentNode;
    531    }
    532 
    533    if (parentElt._built) {
    534      parentElt.removeChild(elt);
    535 
    536      // Figure out if we need to show the "<Empty>" menu-item.
    537      // TODO Bug 517701: This doesn't seem to handle the case of an empty
    538      // root.
    539      if (parentElt._startMarker.nextElementSibling == parentElt._endMarker) {
    540        this._setEmptyPopupStatus(parentElt, true);
    541      }
    542    }
    543  }
    544 
    545  // Opt-out of history details updates, since all the views derived from this
    546  // are not showing them.
    547  skipHistoryDetailsNotifications = true;
    548  nodeHistoryDetailsChanged() {}
    549  nodeTagsChanged() {}
    550  nodeDateAddedChanged() {}
    551  nodeLastModifiedChanged() {}
    552  nodeKeywordChanged() {}
    553  sortingChanged() {}
    554  batching() {}
    555 
    556  nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
    557    let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
    558    if (!parentElt._built) {
    559      return;
    560    }
    561 
    562    let index =
    563      Array.prototype.indexOf.call(parentElt.children, parentElt._startMarker) +
    564      aIndex +
    565      1;
    566    this._insertNewItemToPopup(
    567      aPlacesNode,
    568      parentElt,
    569      parentElt.children[index] || parentElt._endMarker
    570    );
    571    this._setEmptyPopupStatus(parentElt, false);
    572  }
    573 
    574  nodeMoved(
    575    aPlacesNode,
    576    aOldParentPlacesNode,
    577    aOldIndex,
    578    aNewParentPlacesNode,
    579    aNewIndex
    580  ) {
    581    // Note: the current implementation of moveItem does not actually
    582    // use this notification when the item in question is moved from one
    583    // folder to another.  Instead, it calls nodeRemoved and nodeInserted
    584    // for the two folders.  Thus, we can assume old-parent == new-parent.
    585    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
    586 
    587    // Here we need the <menu>.
    588    if (elt.localName == "menupopup") {
    589      elt = elt.parentNode;
    590    }
    591 
    592    // If our root node is a folder, it might be moved. There's nothing
    593    // we need to do in that case.
    594    if (elt == this._rootElt) {
    595      return;
    596    }
    597 
    598    let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
    599    if (parentElt._built) {
    600      // Move the node.
    601      parentElt.removeChild(elt);
    602      let index =
    603        Array.prototype.indexOf.call(
    604          parentElt.children,
    605          parentElt._startMarker
    606        ) +
    607        aNewIndex +
    608        1;
    609      parentElt.insertBefore(elt, parentElt.children[index]);
    610    }
    611  }
    612 
    613  containerStateChanged(aPlacesNode, aOldState, aNewState) {
    614    if (
    615      aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED ||
    616      aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED
    617    ) {
    618      this.invalidateContainer(aPlacesNode);
    619    }
    620  }
    621 
    622  /**
    623   * Checks whether the popup associated with the provided element is open.
    624   * This method may be overridden by classes that extend this base class.
    625   *
    626   * @param  {Element} elt
    627   *   The element to check.
    628   * @returns {boolean}
    629   */
    630  _isPopupOpen(elt) {
    631    return !!elt.parentNode.open;
    632  }
    633 
    634  invalidateContainer(aPlacesNode) {
    635    let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
    636    elt._built = false;
    637 
    638    // If the menupopup is open we should live-update it.
    639    if (this._isPopupOpen(elt)) {
    640      this._rebuildPopup(elt);
    641    }
    642  }
    643 
    644  uninit() {
    645    if (this._result) {
    646      this._result.removeObserver(this);
    647      this._resultNode.containerOpen = false;
    648      this._resultNode = null;
    649      this._result = null;
    650    }
    651 
    652    if (this._controller) {
    653      this._controller.terminate();
    654      // Removing the controller will fail if it is already no longer there.
    655      // This can happen if the view element was removed/reinserted without
    656      // our knowledge. There is no way to check for that having happened
    657      // without the possibility of an exception. :-(
    658      try {
    659        this._viewElt.controllers.removeController(this._controller);
    660      } catch (ex) {
    661      } finally {
    662        this._controller = null;
    663      }
    664    }
    665 
    666    delete this._viewElt._placesView;
    667  }
    668 
    669  get isRTL() {
    670    if ("_isRTL" in this) {
    671      return this._isRTL;
    672    }
    673 
    674    return (this._isRTL =
    675      document.defaultView.getComputedStyle(this._viewElt).direction == "rtl");
    676  }
    677 
    678  get ownerWindow() {
    679    return window;
    680  }
    681 
    682  /**
    683   * Adds an "Open All in Tabs" menuitem to the bottom of the popup.
    684   *
    685   * @param {object} aPopup
    686   *        a Places popup.
    687   */
    688  _mayAddCommandsItems(aPopup) {
    689    // The command items are never added to the root popup.
    690    if (aPopup == this._rootElt) {
    691      return;
    692    }
    693 
    694    let hasMultipleURIs = false;
    695 
    696    // Check if the popup contains at least 2 menuitems with places nodes.
    697    // We don't currently support opening multiple uri nodes when they are not
    698    // populated by the result.
    699    if (aPopup._placesNode.childCount > 0) {
    700      let currentChild = aPopup.firstElementChild;
    701      let numURINodes = 0;
    702      while (currentChild) {
    703        if (currentChild.localName == "menuitem" && currentChild._placesNode) {
    704          if (++numURINodes == 2) {
    705            break;
    706          }
    707        }
    708        currentChild = currentChild.nextElementSibling;
    709      }
    710      hasMultipleURIs = numURINodes > 1;
    711    }
    712 
    713    if (!hasMultipleURIs) {
    714      // We don't have to show any option.
    715      if (aPopup._endOptOpenAllInTabs) {
    716        aPopup.removeChild(aPopup._endOptOpenAllInTabs);
    717        aPopup._endOptOpenAllInTabs = null;
    718 
    719        aPopup.removeChild(aPopup._endOptSeparator);
    720        aPopup._endOptSeparator = null;
    721      }
    722    } else if (!aPopup._endOptOpenAllInTabs) {
    723      // Create a separator before options.
    724      aPopup._endOptSeparator = document.createXULElement("menuseparator");
    725      aPopup._endOptSeparator.className = "bookmarks-actions-menuseparator";
    726      aPopup.appendChild(aPopup._endOptSeparator);
    727 
    728      // Add the "Open All in Tabs" menuitem.
    729      aPopup._endOptOpenAllInTabs = document.createXULElement("menuitem");
    730      aPopup._endOptOpenAllInTabs.className = "openintabs-menuitem";
    731      aPopup._endOptOpenAllInTabs.setAttribute(
    732        "label",
    733        gNavigatorBundle.getString("menuOpenAllInTabs.label")
    734      );
    735      aPopup._endOptOpenAllInTabs.addEventListener("command", event => {
    736        PlacesUIUtils.openMultipleLinksInTabs(
    737          event.currentTarget.parentNode._placesNode,
    738          event,
    739          PlacesUIUtils.getViewForNode(event.currentTarget)
    740        );
    741      });
    742      aPopup.appendChild(aPopup._endOptOpenAllInTabs);
    743    }
    744  }
    745 
    746  _ensureMarkers(aPopup) {
    747    if (aPopup._startMarker) {
    748      return;
    749    }
    750 
    751    // Places nodes are appended between _startMarker and _endMarker, that
    752    // are hidden menuseparators. By default they take the whole panel...
    753    aPopup._startMarker = document.createXULElement("menuseparator");
    754    aPopup._startMarker.hidden = true;
    755    aPopup.insertBefore(aPopup._startMarker, aPopup.firstElementChild);
    756    aPopup._endMarker = document.createXULElement("menuseparator");
    757    aPopup._endMarker.hidden = true;
    758    aPopup.appendChild(aPopup._endMarker);
    759 
    760    // ...but there can be static content before or after the places nodes, thus
    761    // we move the markers to the right position, by checking for static content
    762    // at the beginning of the view, and for an element with "afterplacescontent"
    763    // attribute.
    764    // TODO: In the future we should just use a container element.
    765    let firstNonStaticNodeFound = false;
    766    for (let child of aPopup.children) {
    767      if (child.hasAttribute("afterplacescontent")) {
    768        aPopup.insertBefore(aPopup._endMarker, child);
    769        break;
    770      }
    771 
    772      // Check for the first Places node that is not a view.
    773      if (child._placesNode && !child._placesView && !firstNonStaticNodeFound) {
    774        firstNonStaticNodeFound = true;
    775        aPopup.insertBefore(aPopup._startMarker, child);
    776      }
    777    }
    778    if (!firstNonStaticNodeFound) {
    779      // Just put the start marker before the end marker.
    780      aPopup.insertBefore(aPopup._startMarker, aPopup._endMarker);
    781    }
    782  }
    783 
    784  _onPopupShowing(aEvent) {
    785    // Avoid handling popupshowing of inner views.
    786    let popup = aEvent.originalTarget;
    787 
    788    this._ensureMarkers(popup);
    789 
    790    // Remove any delayed element, see _cleanPopup for details.
    791    if ("_delayedRemovals" in popup) {
    792      while (popup._delayedRemovals.length) {
    793        popup.removeChild(popup._delayedRemovals.shift());
    794      }
    795    }
    796 
    797    if (popup._placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
    798      if (this.#isPopupForRecursiveFolderShortcut(popup)) {
    799        // Show as an empty container for now. We may want to show a better
    800        // message in the future, but since we are likely to remove recursive
    801        // shortcuts in maintenance at a certain point, this should be enough.
    802        this._setEmptyPopupStatus(popup, true);
    803        popup._built = true;
    804        return;
    805      }
    806 
    807      if (!popup._placesNode.containerOpen) {
    808        popup._placesNode.containerOpen = true;
    809      }
    810      if (!popup._built) {
    811        this._rebuildPopup(popup);
    812      }
    813 
    814      this._mayAddCommandsItems(popup);
    815    }
    816  }
    817 
    818  _addEventListeners(aObject, aEventNames, aCapturing = false) {
    819    for (let i = 0; i < aEventNames.length; i++) {
    820      aObject.addEventListener(aEventNames[i], this, aCapturing);
    821    }
    822  }
    823 
    824  _removeEventListeners(aObject, aEventNames, aCapturing = false) {
    825    for (let i = 0; i < aEventNames.length; i++) {
    826      aObject.removeEventListener(aEventNames[i], this, aCapturing);
    827    }
    828  }
    829 
    830  /**
    831   * Walks up the parent chain to detect whether a folder shortcut resolves to
    832   * a folder already present in the ancestry.
    833   *
    834   * @param {DOMElement} popup
    835   * @returns {boolean} Whether this popup is for a recursive folder shortcut.
    836   */
    837  #isPopupForRecursiveFolderShortcut(popup) {
    838    if (
    839      !popup._placesNode ||
    840      !PlacesUtils.nodeIsFolderOrShortcut(popup._placesNode)
    841    ) {
    842      return false;
    843    }
    844    let guid = PlacesUtils.getConcreteItemGuid(popup._placesNode);
    845    for (
    846      let parentView = popup.parentNode?.parentNode;
    847      parentView?._placesNode;
    848      parentView = parentView.parentNode?.parentNode
    849    ) {
    850      if (PlacesUtils.getConcreteItemGuid(parentView._placesNode) == guid) {
    851        return true;
    852      }
    853    }
    854    return false;
    855  }
    856 }
    857 
    858 /**
    859 * Toolbar View implementation.
    860 */
    861 class PlacesToolbar extends PlacesViewBase {
    862  constructor(placesUrl, rootElt, viewElt) {
    863    let timerId = Glean.bookmarksToolbar.init.start();
    864    super(placesUrl, rootElt, viewElt);
    865    this._addEventListeners(this._dragRoot, this._cbEvents, false);
    866    this._addEventListeners(
    867      this._rootElt,
    868      ["popupshowing", "popuphidden"],
    869      true
    870    );
    871    this._addEventListeners(this._rootElt, ["overflow", "underflow"], true);
    872    this._addEventListeners(window, ["resize", "unload"], false);
    873 
    874    // If personal-bookmarks has been dragged to the tabs toolbar,
    875    // we have to track addition and removals of tabs, to properly
    876    // recalculate the available space for bookmarks.
    877    // TODO (bug 734730): Use a performant mutation listener when available.
    878    if (
    879      this._viewElt.parentNode.parentNode ==
    880      document.getElementById("TabsToolbar")
    881    ) {
    882      this._addEventListeners(
    883        gBrowser.tabContainer,
    884        ["TabOpen", "TabClose"],
    885        false
    886      );
    887    }
    888 
    889    Glean.bookmarksToolbar.init.stopAndAccumulate(timerId);
    890  }
    891 
    892  // Called by PlacesViewBase so we can init properties that class
    893  // initialization depends on. PlacesViewBase will assign this.place which
    894  // calls which sets `this.result` through its places observer, which changes
    895  // containerOpen, which calls invalidateContainer(), which calls rebuild(),
    896  // which needs `_overFolder`, `_chevronPopup` and various other things to
    897  // exist.
    898  _init() {
    899    this._overFolder = {
    900      elt: null,
    901      openTimer: null,
    902      hoverTime: 350,
    903      closeTimer: null,
    904    };
    905 
    906    // Add some smart getters for our elements.
    907    let thisView = this;
    908    [
    909      ["_dropIndicator", "PlacesToolbarDropIndicator"],
    910      ["_chevron", "PlacesChevron"],
    911      ["_chevronPopup", "PlacesChevronPopup"],
    912    ].forEach(function (elementGlobal) {
    913      let [name, id] = elementGlobal;
    914      thisView.__defineGetter__(name, function () {
    915        let element = document.getElementById(id);
    916        if (!element) {
    917          return null;
    918        }
    919 
    920        delete thisView[name];
    921        return (thisView[name] = element);
    922      });
    923    });
    924 
    925    this._viewElt._placesView = this;
    926 
    927    this._dragRoot = BookmarkingUI.toolbar.contains(this._viewElt)
    928      ? BookmarkingUI.toolbar
    929      : this._viewElt;
    930 
    931    this._updatingNodesVisibility = false;
    932  }
    933 
    934  _cbEvents = [
    935    "dragstart",
    936    "dragover",
    937    "dragleave",
    938    "dragend",
    939    "drop",
    940    "mousemove",
    941    "mouseover",
    942    "mouseout",
    943    "mousedown",
    944  ];
    945 
    946  QueryInterface = ChromeUtils.generateQI([
    947    "nsINamed",
    948    "nsITimerCallback",
    949    ...PlacesViewBase.interfaces,
    950  ]);
    951 
    952  uninit() {
    953    if (this._dragRoot) {
    954      this._removeEventListeners(this._dragRoot, this._cbEvents, false);
    955    }
    956    this._removeEventListeners(
    957      this._rootElt,
    958      ["popupshowing", "popuphidden"],
    959      true
    960    );
    961    this._removeEventListeners(this._rootElt, ["overflow", "underflow"], true);
    962    this._removeEventListeners(window, ["resize", "unload"], false);
    963    this._removeEventListeners(
    964      gBrowser.tabContainer,
    965      ["TabOpen", "TabClose"],
    966      false
    967    );
    968 
    969    if (this._chevron._placesView) {
    970      this._chevron._placesView.uninit();
    971    }
    972 
    973    this._chevronPopup.uninit();
    974 
    975    if (this._otherBookmarks?._placesView) {
    976      this._otherBookmarks._placesView.uninit();
    977    }
    978 
    979    super.uninit();
    980  }
    981 
    982  _openedMenuButton = null;
    983  _allowPopupShowing = true;
    984 
    985  promiseRebuilt() {
    986    return this._rebuilding?.promise;
    987  }
    988 
    989  get _isAlive() {
    990    return this._resultNode && this._rootElt;
    991  }
    992 
    993  _runBeforeFrameRender(callback) {
    994    return new Promise((resolve, reject) => {
    995      window.requestAnimationFrame(() => {
    996        try {
    997          resolve(callback());
    998        } catch (err) {
    999          reject(err);
   1000        }
   1001      });
   1002    });
   1003  }
   1004 
   1005  async _rebuild() {
   1006    // Clear out references to existing nodes, since they will be removed
   1007    // and re-added.
   1008    if (this._overFolder.elt) {
   1009      this._clearOverFolder();
   1010    }
   1011 
   1012    this._openedMenuButton = null;
   1013    while (this._rootElt.hasChildNodes()) {
   1014      this._rootElt.firstChild.remove();
   1015    }
   1016 
   1017    let cc = this._resultNode.childCount;
   1018    if (cc > 0) {
   1019      // There could be a lot of nodes, but we only want to build the ones that
   1020      // are more likely to be shown, not all of them.
   1021      // We also don't want to wait for reflows at every node insertion, to
   1022      // calculate a precise number of visible items, thus we guess a size from
   1023      // the first non-separator node (because separators have flexible size).
   1024      let startIndex = 0;
   1025      let limit = await this._runBeforeFrameRender(() => {
   1026        if (!this._isAlive) {
   1027          return cc;
   1028        }
   1029 
   1030        // Look for the first non-separator node.
   1031        let elt;
   1032        while (startIndex < cc) {
   1033          elt = this._insertNewItem(
   1034            this._resultNode.getChild(startIndex),
   1035            this._rootElt
   1036          );
   1037          ++startIndex;
   1038          if (elt.localName != "toolbarseparator") {
   1039            break;
   1040          }
   1041        }
   1042        if (!elt) {
   1043          return cc;
   1044        }
   1045 
   1046        return window.promiseDocumentFlushed(() => {
   1047          // We assume a button with just the icon will be more or less a square,
   1048          // then compensate the measurement error by considering a larger screen
   1049          // width. Moreover the window could be bigger than the screen.
   1050          let size = elt.clientHeight || 1; // Sanity fallback.
   1051          return Math.min(cc, parseInt((window.screen.width * 1.5) / size));
   1052        });
   1053      });
   1054 
   1055      if (!this._isAlive) {
   1056        return;
   1057      }
   1058 
   1059      let fragment = document.createDocumentFragment();
   1060      for (let i = startIndex; i < limit; ++i) {
   1061        this._insertNewItem(this._resultNode.getChild(i), fragment);
   1062      }
   1063      await new Promise(resolve => window.requestAnimationFrame(resolve));
   1064      if (!this._isAlive) {
   1065        return;
   1066      }
   1067      this._rootElt.appendChild(fragment);
   1068      this.updateNodesVisibility();
   1069    }
   1070 
   1071    if (this._chevronPopup.hasAttribute("type")) {
   1072      // Chevron has already been initialized, but since we are forcing
   1073      // a rebuild of the toolbar, it has to be rebuilt.
   1074      // Otherwise, it will be initialized when the toolbar overflows.
   1075      this._chevronPopup.place = this.place;
   1076    }
   1077 
   1078    // Rebuild the "Other Bookmarks" folder if it already exists.
   1079    let otherBookmarks = document.getElementById("OtherBookmarks");
   1080    otherBookmarks?.remove();
   1081 
   1082    BookmarkingUI.maybeShowOtherBookmarksFolder().catch(console.error);
   1083  }
   1084 
   1085  _insertNewItem(aChild, aInsertionNode, aBefore = null) {
   1086    this._domNodes.delete(aChild);
   1087 
   1088    let type = aChild.type;
   1089    let button;
   1090    if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
   1091      button = document.createXULElement("toolbarseparator");
   1092    } else {
   1093      button = document.createXULElement("toolbarbutton");
   1094      button.className = "bookmark-item";
   1095      button.setAttribute("label", aChild.title || "");
   1096 
   1097      if (PlacesUtils.containerTypes.includes(type)) {
   1098        button.setAttribute("type", "menu");
   1099        button.setAttribute("container", "true");
   1100 
   1101        if (PlacesUtils.nodeIsQuery(aChild)) {
   1102          button.setAttribute("query", "true");
   1103          if (PlacesUtils.nodeIsTagQuery(aChild)) {
   1104            button.setAttribute("tagContainer", "true");
   1105          }
   1106        }
   1107 
   1108        let popup = document.createXULElement("menupopup", {
   1109          is: "places-popup",
   1110        });
   1111        popup.setAttribute("placespopup", "true");
   1112        popup.classList.add("toolbar-menupopup");
   1113        button.appendChild(popup);
   1114        popup._placesNode = PlacesUtils.asContainer(aChild);
   1115        popup.setAttribute("context", "placesContext");
   1116 
   1117        this._domNodes.set(aChild, popup);
   1118      } else if (PlacesUtils.nodeIsURI(aChild)) {
   1119        button.setAttribute(
   1120          "scheme",
   1121          PlacesUIUtils.guessUrlSchemeForUI(aChild.uri)
   1122        );
   1123      }
   1124    }
   1125 
   1126    button._placesNode = aChild;
   1127    let { icon } = button._placesNode;
   1128    if (icon) {
   1129      button.setAttribute("image", icon);
   1130    }
   1131    if (!this._domNodes.has(aChild)) {
   1132      this._domNodes.set(aChild, button);
   1133    }
   1134 
   1135    if (aBefore) {
   1136      aInsertionNode.insertBefore(button, aBefore);
   1137    } else {
   1138      aInsertionNode.appendChild(button);
   1139    }
   1140    return button;
   1141  }
   1142 
   1143  _updateChevronPopupNodesVisibility() {
   1144    // Note the toolbar by default builds less nodes than the chevron popup.
   1145    for (
   1146      let toolbarNode = this._rootElt.firstElementChild,
   1147        node = this._chevronPopup._startMarker.nextElementSibling;
   1148      toolbarNode && node;
   1149      toolbarNode = toolbarNode.nextElementSibling,
   1150        node = node.nextElementSibling
   1151    ) {
   1152      node.hidden = toolbarNode.style.visibility != "hidden";
   1153    }
   1154  }
   1155 
   1156  _onChevronPopupShowing(aEvent) {
   1157    // Handle popupshowing only for the chevron popup, not for nested ones.
   1158    if (aEvent.target != this._chevronPopup) {
   1159      return;
   1160    }
   1161 
   1162    if (!this._chevron._placesView) {
   1163      this._chevron._placesView = new PlacesMenu(aEvent, this.place);
   1164    }
   1165 
   1166    this._updateChevronPopupNodesVisibility();
   1167  }
   1168 
   1169  _onOtherBookmarksPopupShowing(aEvent) {
   1170    if (aEvent.target != this._otherBookmarksPopup) {
   1171      return;
   1172    }
   1173 
   1174    if (!this._otherBookmarks._placesView) {
   1175      this._otherBookmarks._placesView = new PlacesMenu(
   1176        aEvent,
   1177        "place:parent=" + PlacesUtils.bookmarks.unfiledGuid
   1178      );
   1179    }
   1180  }
   1181 
   1182  handleEvent(aEvent) {
   1183    switch (aEvent.type) {
   1184      case "unload":
   1185        this.uninit();
   1186        break;
   1187      case "resize":
   1188        // This handler updates nodes visibility in both the toolbar
   1189        // and the chevron popup when a window resize does not change
   1190        // the overflow status of the toolbar.
   1191        if (aEvent.target == aEvent.currentTarget) {
   1192          this.updateNodesVisibility();
   1193        }
   1194        break;
   1195      case "overflow":
   1196        if (!this._isOverflowStateEventRelevant(aEvent)) {
   1197          return;
   1198        }
   1199        // Avoid triggering overflow in containers if possible
   1200        aEvent.stopPropagation();
   1201        this._onOverflow();
   1202        break;
   1203      case "underflow":
   1204        if (!this._isOverflowStateEventRelevant(aEvent)) {
   1205          return;
   1206        }
   1207        // Avoid triggering underflow in containers if possible
   1208        aEvent.stopPropagation();
   1209        this._onUnderflow();
   1210        break;
   1211      case "TabOpen":
   1212      case "TabClose":
   1213        this.updateNodesVisibility();
   1214        break;
   1215      case "dragstart":
   1216        this._onDragStart(aEvent);
   1217        break;
   1218      case "dragover":
   1219        this._onDragOver(aEvent);
   1220        break;
   1221      case "dragleave":
   1222        this._onDragLeave(aEvent);
   1223        break;
   1224      case "dragend":
   1225        this._onDragEnd(aEvent);
   1226        break;
   1227      case "drop":
   1228        this._onDrop(aEvent);
   1229        break;
   1230      case "mouseover":
   1231        this._onMouseOver(aEvent);
   1232        break;
   1233      case "mousemove":
   1234        this._onMouseMove(aEvent);
   1235        break;
   1236      case "mouseout":
   1237        this._onMouseOut(aEvent);
   1238        break;
   1239      case "mousedown":
   1240        this._onMouseDown(aEvent);
   1241        break;
   1242      case "popupshowing":
   1243        this._onPopupShowing(aEvent);
   1244        break;
   1245      case "popuphidden":
   1246        this._onPopupHidden(aEvent);
   1247        break;
   1248      default:
   1249        throw new Error("Trying to handle unexpected event.");
   1250    }
   1251  }
   1252 
   1253  _isOverflowStateEventRelevant(aEvent) {
   1254    // Ignore events not aimed at ourselves, as well as purely vertical ones:
   1255    return aEvent.target == aEvent.currentTarget && aEvent.detail > 0;
   1256  }
   1257 
   1258  _onOverflow() {
   1259    // Attach the popup binding to the chevron popup if it has not yet
   1260    // been initialized.
   1261    if (!this._chevronPopup.hasAttribute("type")) {
   1262      this._chevronPopup.setAttribute("place", this.place);
   1263      this._chevronPopup.setAttribute("type", "places");
   1264    }
   1265    this._chevron.collapsed = false;
   1266    this.updateNodesVisibility();
   1267  }
   1268 
   1269  _onUnderflow() {
   1270    this.updateNodesVisibility();
   1271    this._chevron.collapsed = true;
   1272  }
   1273 
   1274  updateNodesVisibility() {
   1275    // Update the chevron on a timer.  This will avoid repeated work when
   1276    // lot of changes happen in a small timeframe.
   1277    if (this._updateNodesVisibilityTimer) {
   1278      this._updateNodesVisibilityTimer.cancel();
   1279    }
   1280 
   1281    this._updateNodesVisibilityTimer = this._setTimer(100);
   1282  }
   1283 
   1284  async _updateNodesVisibilityTimerCallback() {
   1285    if (this._updatingNodesVisibility || window.closed) {
   1286      return;
   1287    }
   1288    this._updatingNodesVisibility = true;
   1289 
   1290    let dwu = window.windowUtils;
   1291 
   1292    let scrollRect = await window.promiseDocumentFlushed(() =>
   1293      dwu.getBoundsWithoutFlushing(this._rootElt)
   1294    );
   1295 
   1296    let childOverflowed = false;
   1297 
   1298    // We're about to potentially update a bunch of nodes, so we do it
   1299    // in a requestAnimationFrame so that other JS that's might execute
   1300    // in the same tick can avoid flushing styles and layout for these
   1301    // changes.
   1302    window.requestAnimationFrame(() => {
   1303      for (let child of this._rootElt.children) {
   1304        // Once a child overflows, all the next ones will.
   1305        if (!childOverflowed) {
   1306          let childRect = dwu.getBoundsWithoutFlushing(child);
   1307          childOverflowed = this.isRTL
   1308            ? childRect.left < scrollRect.left
   1309            : childRect.right > scrollRect.right;
   1310        }
   1311 
   1312        if (childOverflowed) {
   1313          child.removeAttribute("image");
   1314          child.style.visibility = "hidden";
   1315        } else {
   1316          let icon = child._placesNode.icon;
   1317          if (icon) {
   1318            child.setAttribute("image", icon);
   1319          }
   1320          child.style.removeProperty("visibility");
   1321        }
   1322      }
   1323 
   1324      // We rebuild the chevron on popupShowing, so if it is open
   1325      // we must update it.
   1326      if (!this._chevron.collapsed && this._chevron.open) {
   1327        this._updateChevronPopupNodesVisibility();
   1328      }
   1329 
   1330      let event = new CustomEvent("BookmarksToolbarVisibilityUpdated", {
   1331        bubbles: true,
   1332      });
   1333      this._viewElt.dispatchEvent(event);
   1334      this._updatingNodesVisibility = false;
   1335    });
   1336  }
   1337 
   1338  nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) {
   1339    let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
   1340    if (parentElt == this._rootElt) {
   1341      // Node is on the toolbar.
   1342      let children = this._rootElt.children;
   1343      // Nothing to do if it's a never-visible node, but note it's possible
   1344      // we are appending.
   1345      if (aIndex > children.length) {
   1346        return;
   1347      }
   1348 
   1349      // Note that childCount is already accounting for the node being added,
   1350      // thus we must subtract one node from it.
   1351      if (this._resultNode.childCount - 1 > children.length) {
   1352        if (aIndex == children.length) {
   1353          // If we didn't build all the nodes and new node is being appended,
   1354          // we can skip it as well.
   1355          return;
   1356        }
   1357        // Keep the number of built nodes consistent.
   1358        this._rootElt.removeChild(this._rootElt.lastElementChild);
   1359      }
   1360 
   1361      let button = this._insertNewItem(
   1362        aPlacesNode,
   1363        this._rootElt,
   1364        children[aIndex] || null
   1365      );
   1366      let prevSiblingOverflowed =
   1367        aIndex > 0 &&
   1368        aIndex <= children.length &&
   1369        children[aIndex - 1].style.visibility == "hidden";
   1370      if (prevSiblingOverflowed) {
   1371        button.style.visibility = "hidden";
   1372      } else {
   1373        let icon = aPlacesNode.icon;
   1374        if (icon) {
   1375          button.setAttribute("image", ChromeUtils.encodeURIForSrcset(icon));
   1376        }
   1377        this.updateNodesVisibility();
   1378      }
   1379      return;
   1380    }
   1381 
   1382    super.nodeInserted(aParentPlacesNode, aPlacesNode, aIndex);
   1383  }
   1384 
   1385  nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
   1386    let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
   1387    if (parentElt == this._rootElt) {
   1388      // Node is on the toolbar.
   1389      let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
   1390      // Nothing to do if it's a never-visible node.
   1391      if (!elt) {
   1392        return;
   1393      }
   1394 
   1395      // Here we need the <menu>.
   1396      if (elt.localName == "menupopup") {
   1397        elt = elt.parentNode;
   1398      }
   1399 
   1400      let overflowed = elt.style.visibility == "hidden";
   1401      this._removeChild(elt);
   1402      if (this._resultNode.childCount > this._rootElt.children.length) {
   1403        // A new node should be built to keep a coherent number of children.
   1404        this._insertNewItem(
   1405          this._resultNode.getChild(this._rootElt.children.length),
   1406          this._rootElt
   1407        );
   1408      }
   1409      if (!overflowed) {
   1410        this.updateNodesVisibility();
   1411      }
   1412      return;
   1413    }
   1414 
   1415    super.nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex);
   1416  }
   1417 
   1418  nodeMoved(
   1419    aPlacesNode,
   1420    aOldParentPlacesNode,
   1421    aOldIndex,
   1422    aNewParentPlacesNode,
   1423    aNewIndex
   1424  ) {
   1425    let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
   1426    if (parentElt == this._rootElt) {
   1427      // Node is on the toolbar.
   1428      // Do nothing if the node will never be visible.
   1429      let lastBuiltIndex = this._rootElt.children.length - 1;
   1430      if (aOldIndex > lastBuiltIndex && aNewIndex > lastBuiltIndex + 1) {
   1431        return;
   1432      }
   1433 
   1434      let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
   1435      if (elt) {
   1436        // Here we need the <menu>.
   1437        if (elt.localName == "menupopup") {
   1438          elt = elt.parentNode;
   1439        }
   1440        this._removeChild(elt);
   1441      }
   1442 
   1443      if (aNewIndex > lastBuiltIndex + 1) {
   1444        if (this._resultNode.childCount > this._rootElt.children.length) {
   1445          // If the element was built and becomes non built, another node should
   1446          // be built to keep a coherent number of children.
   1447          this._insertNewItem(
   1448            this._resultNode.getChild(this._rootElt.children.length),
   1449            this._rootElt
   1450          );
   1451        }
   1452        return;
   1453      }
   1454 
   1455      if (!elt) {
   1456        // The node has not been inserted yet, so we must create it.
   1457        elt = this._insertNewItem(
   1458          aPlacesNode,
   1459          this._rootElt,
   1460          this._rootElt.children[aNewIndex]
   1461        );
   1462        let icon = aPlacesNode.icon;
   1463        if (icon) {
   1464          elt.setAttribute("image", ChromeUtils.encodeURIForSrcset(icon));
   1465        }
   1466      } else {
   1467        this._rootElt.insertBefore(elt, this._rootElt.children[aNewIndex]);
   1468      }
   1469 
   1470      // The chevron view may get nodeMoved after the toolbar.  In such a case,
   1471      // we should ensure (by manually swapping menuitems) that the actual nodes
   1472      // are in the final position before updateNodesVisibility tries to update
   1473      // their visibility, or the chevron may go out of sync.
   1474      // Luckily updateNodesVisibility runs on a timer, so, by the time it updates
   1475      // nodes, the menu has already handled the notification.
   1476 
   1477      this.updateNodesVisibility();
   1478      return;
   1479    }
   1480 
   1481    super.nodeMoved(
   1482      aPlacesNode,
   1483      aOldParentPlacesNode,
   1484      aOldIndex,
   1485      aNewParentPlacesNode,
   1486      aNewIndex
   1487    );
   1488  }
   1489 
   1490  nodeTitleChanged(aPlacesNode, aNewTitle) {
   1491    let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
   1492 
   1493    // Nothing to do if it's a never-visible node.
   1494    if (!elt || elt == this._rootElt) {
   1495      return;
   1496    }
   1497 
   1498    super.nodeTitleChanged(aPlacesNode, aNewTitle);
   1499 
   1500    // Here we need the <menu>.
   1501    if (elt.localName == "menupopup") {
   1502      elt = elt.parentNode;
   1503    }
   1504 
   1505    if (elt.parentNode == this._rootElt) {
   1506      // Node is on the toolbar.
   1507      if (elt.style.visibility != "hidden") {
   1508        this.updateNodesVisibility();
   1509      }
   1510    }
   1511  }
   1512 
   1513  invalidateContainer(aPlacesNode) {
   1514    let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
   1515    // Nothing to do if it's a never-visible node.
   1516    if (!elt) {
   1517      return;
   1518    }
   1519 
   1520    if (elt == this._rootElt) {
   1521      // Container is the toolbar itself.
   1522      let instance = (this._rebuildingInstance = {});
   1523      if (!this._rebuilding) {
   1524        this._rebuilding = Promise.withResolvers();
   1525      }
   1526      this._rebuild()
   1527        .catch(console.error)
   1528        .finally(() => {
   1529          if (instance == this._rebuildingInstance) {
   1530            this._rebuilding.resolve();
   1531            this._rebuilding = null;
   1532          }
   1533        });
   1534      return;
   1535    }
   1536 
   1537    super.invalidateContainer(aPlacesNode);
   1538  }
   1539 
   1540  _clearOverFolder() {
   1541    // The mouse is no longer dragging over the stored menubutton.
   1542    // Close the menubutton, clear out drag styles, and clear all
   1543    // timers for opening/closing it.
   1544    if (this._overFolder.elt && this._overFolder.elt.menupopup) {
   1545      if (!this._overFolder.elt.menupopup.hasAttribute("dragover")) {
   1546        this._overFolder.elt.menupopup.hidePopup();
   1547      }
   1548      this._overFolder.elt.removeAttribute("dragover");
   1549      this._overFolder.elt = null;
   1550    }
   1551    if (this._overFolder.openTimer) {
   1552      this._overFolder.openTimer.cancel();
   1553      this._overFolder.openTimer = null;
   1554    }
   1555    if (this._overFolder.closeTimer) {
   1556      this._overFolder.closeTimer.cancel();
   1557      this._overFolder.closeTimer = null;
   1558    }
   1559  }
   1560 
   1561  /**
   1562   * This function returns information about where to drop when dragging over
   1563   * the toolbar.
   1564   *
   1565   * @param {object} aEvent
   1566   *   The associated event.
   1567   * @returns {object}
   1568   *   - ip: the insertion point for the bookmarks service.
   1569   *   - beforeIndex: child index to drop before, for the drop indicator.
   1570   *   - folderElt: the folder to drop into, if applicable.
   1571   */
   1572  _getDropPoint(aEvent) {
   1573    if (!PlacesUtils.nodeIsFolderOrShortcut(this._resultNode)) {
   1574      return null;
   1575    }
   1576 
   1577    let dropPoint = { ip: null, beforeIndex: null, folderElt: null };
   1578    let elt = aEvent.target;
   1579    if (
   1580      elt._placesNode &&
   1581      elt != this._rootElt &&
   1582      elt.localName != "menupopup"
   1583    ) {
   1584      let eltRect = elt.getBoundingClientRect();
   1585      let eltIndex = Array.prototype.indexOf.call(this._rootElt.children, elt);
   1586      if (
   1587        PlacesUtils.nodeIsFolderOrShortcut(elt._placesNode) &&
   1588        !PlacesUIUtils.isFolderReadOnly(elt._placesNode)
   1589      ) {
   1590        // This is a folder.
   1591        // If we are in the middle of it, drop inside it.
   1592        // Otherwise, drop before it, with regards to RTL mode.
   1593        let threshold = eltRect.width * 0.25;
   1594        if (
   1595          this.isRTL
   1596            ? aEvent.clientX > eltRect.right - threshold
   1597            : aEvent.clientX < eltRect.left + threshold
   1598        ) {
   1599          // Drop before this folder.
   1600          dropPoint.ip = new PlacesInsertionPoint({
   1601            parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
   1602            index: eltIndex,
   1603            orientation: Ci.nsITreeView.DROP_BEFORE,
   1604          });
   1605          dropPoint.beforeIndex = eltIndex;
   1606        } else if (
   1607          this.isRTL
   1608            ? aEvent.clientX > eltRect.left + threshold
   1609            : aEvent.clientX < eltRect.right - threshold
   1610        ) {
   1611          // Drop inside this folder.
   1612          let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode)
   1613            ? elt._placesNode.title
   1614            : null;
   1615          dropPoint.ip = new PlacesInsertionPoint({
   1616            parentGuid: PlacesUtils.getConcreteItemGuid(elt._placesNode),
   1617            tagName,
   1618          });
   1619          dropPoint.beforeIndex = eltIndex;
   1620          dropPoint.folderElt = elt;
   1621        } else {
   1622          // Drop after this folder.
   1623          let beforeIndex =
   1624            eltIndex == this._rootElt.children.length - 1 ? -1 : eltIndex + 1;
   1625 
   1626          dropPoint.ip = new PlacesInsertionPoint({
   1627            parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
   1628            index: beforeIndex,
   1629            orientation: Ci.nsITreeView.DROP_BEFORE,
   1630          });
   1631          dropPoint.beforeIndex = beforeIndex;
   1632        }
   1633      } else {
   1634        // This is a non-folder node or a read-only folder.
   1635        // Drop before it with regards to RTL mode.
   1636        let threshold = eltRect.width * 0.5;
   1637        if (
   1638          this.isRTL
   1639            ? aEvent.clientX > eltRect.left + threshold
   1640            : aEvent.clientX < eltRect.left + threshold
   1641        ) {
   1642          // Drop before this bookmark.
   1643          dropPoint.ip = new PlacesInsertionPoint({
   1644            parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
   1645            index: eltIndex,
   1646            orientation: Ci.nsITreeView.DROP_BEFORE,
   1647          });
   1648          dropPoint.beforeIndex = eltIndex;
   1649        } else {
   1650          // Drop after this bookmark.
   1651          let beforeIndex =
   1652            eltIndex == this._rootElt.children.length - 1 ? -1 : eltIndex + 1;
   1653          dropPoint.ip = new PlacesInsertionPoint({
   1654            parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
   1655            index: beforeIndex,
   1656            orientation: Ci.nsITreeView.DROP_BEFORE,
   1657          });
   1658          dropPoint.beforeIndex = beforeIndex;
   1659        }
   1660      }
   1661    } else if (elt == this._chevron) {
   1662      // If drop on the chevron, insert after the last bookmark.
   1663      dropPoint.ip = new PlacesInsertionPoint({
   1664        parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
   1665        orientation: Ci.nsITreeView.DROP_BEFORE,
   1666      });
   1667      dropPoint.beforeIndex = -1;
   1668    } else {
   1669      dropPoint.ip = new PlacesInsertionPoint({
   1670        parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
   1671        orientation: Ci.nsITreeView.DROP_BEFORE,
   1672      });
   1673 
   1674      // If could not find an insertion point before bookmark items or empty,
   1675      // drop after the last bookmark.
   1676      dropPoint.beforeIndex = -1;
   1677 
   1678      let canInsertHere = this.isRTL
   1679        ? (x, rect) => x >= Math.round(rect.right)
   1680        : (x, rect) => x <= Math.round(rect.left);
   1681 
   1682      // Find the bookmark placed just after the mouse point as the insertion
   1683      // point.
   1684      for (let i = 0; i < this._rootElt.children.length; i++) {
   1685        let childRect = window.windowUtils.getBoundsWithoutFlushing(
   1686          this._rootElt.children[i]
   1687        );
   1688        if (canInsertHere(aEvent.clientX, childRect)) {
   1689          dropPoint.beforeIndex = i;
   1690          dropPoint.ip.index = i;
   1691          break;
   1692        }
   1693      }
   1694    }
   1695 
   1696    return dropPoint;
   1697  }
   1698 
   1699  _setTimer(aTime) {
   1700    let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
   1701    timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
   1702    return timer;
   1703  }
   1704 
   1705  get name() {
   1706    return "PlacesToolbar";
   1707  }
   1708 
   1709  notify(aTimer) {
   1710    if (aTimer == this._updateNodesVisibilityTimer) {
   1711      this._updateNodesVisibilityTimer = null;
   1712      this._updateNodesVisibilityTimerCallback();
   1713    } else if (aTimer == this._overFolder.openTimer) {
   1714      // * Timer to open a menubutton that's being dragged over.
   1715      // Set the autoopen attribute on the folder's menupopup so that
   1716      // the menu will automatically close when the mouse drags off of it.
   1717      this._overFolder.elt.menupopup.setAttribute("autoopened", "true");
   1718      this._overFolder.elt.open = true;
   1719      this._overFolder.openTimer = null;
   1720    } else if (aTimer == this._overFolder.closeTimer) {
   1721      // * Timer to close a menubutton that's been dragged off of.
   1722      // Close the menubutton if we are not dragging over it or one of
   1723      // its children.  The autoopened attribute will let the menu know to
   1724      // close later if the menu is still being dragged over.
   1725      let currentPlacesNode = PlacesControllerDragHelper.currentDropTarget;
   1726      let inHierarchy = false;
   1727      while (currentPlacesNode) {
   1728        if (currentPlacesNode == this._rootElt) {
   1729          inHierarchy = true;
   1730          break;
   1731        }
   1732        currentPlacesNode = currentPlacesNode.parentNode;
   1733      }
   1734      // The _clearOverFolder() function will close the menu for
   1735      // _overFolder.elt.  So null it out if we don't want to close it.
   1736      if (inHierarchy) {
   1737        this._overFolder.elt = null;
   1738      }
   1739 
   1740      // Clear out the folder and all associated timers.
   1741      this._clearOverFolder();
   1742    }
   1743  }
   1744 
   1745  _onMouseOver(aEvent) {
   1746    let button = aEvent.target;
   1747    if (
   1748      button.parentNode == this._rootElt &&
   1749      button._placesNode &&
   1750      PlacesUtils.nodeIsURI(button._placesNode)
   1751    ) {
   1752      window.XULBrowserWindow.setOverLink(aEvent.target._placesNode.uri);
   1753    }
   1754  }
   1755 
   1756  _onMouseOut() {
   1757    window.XULBrowserWindow.setOverLink("");
   1758  }
   1759 
   1760  _onMouseDown(aEvent) {
   1761    let target = aEvent.target;
   1762    if (
   1763      aEvent.button == 0 &&
   1764      target.localName == "toolbarbutton" &&
   1765      target.getAttribute("type") == "menu"
   1766    ) {
   1767      let modifKey = aEvent.shiftKey || aEvent.getModifierState("Accel");
   1768      if (modifKey) {
   1769        // Do not open the popup since BEH_onClick is about to
   1770        // open all child uri nodes in tabs.
   1771        this._allowPopupShowing = false;
   1772      }
   1773    }
   1774    PlacesUIUtils.maybeSpeculativeConnectOnMouseDown(aEvent);
   1775  }
   1776 
   1777  _cleanupDragDetails() {
   1778    // Called on dragend and drop.
   1779    PlacesControllerDragHelper.currentDropTarget = null;
   1780    this._draggedElt = null;
   1781    this._dropIndicator.collapsed = true;
   1782  }
   1783 
   1784  _onDragStart(aEvent) {
   1785    // Sub menus have their own d&d handlers.
   1786    let draggedElt = aEvent.target;
   1787    if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode) {
   1788      return;
   1789    }
   1790 
   1791    if (
   1792      draggedElt.localName == "toolbarbutton" &&
   1793      draggedElt.getAttribute("type") == "menu"
   1794    ) {
   1795      // If the drag gesture on a container is toward down we open instead
   1796      // of dragging.
   1797      let translateY = this._cachedMouseMoveEvent.clientY - aEvent.clientY;
   1798      let translateX = this._cachedMouseMoveEvent.clientX - aEvent.clientX;
   1799      if (translateY >= Math.abs(translateX / 2)) {
   1800        // Don't start the drag.
   1801        aEvent.preventDefault();
   1802        // Open the menu.
   1803        draggedElt.open = true;
   1804        return;
   1805      }
   1806 
   1807      // If the menu is open, close it.
   1808      if (draggedElt.open) {
   1809        draggedElt.menupopup.hidePopup();
   1810        draggedElt.open = false;
   1811      }
   1812    }
   1813 
   1814    // Activate the view and cache the dragged element.
   1815    this._draggedElt = draggedElt._placesNode;
   1816    this._rootElt.focus();
   1817 
   1818    this._controller.setDataTransfer(aEvent);
   1819    aEvent.stopPropagation();
   1820  }
   1821 
   1822  _onDragOver(aEvent) {
   1823    // Cache the dataTransfer
   1824    PlacesControllerDragHelper.currentDropTarget = aEvent.target;
   1825    let dt = aEvent.dataTransfer;
   1826 
   1827    let dropPoint = this._getDropPoint(aEvent);
   1828    if (
   1829      !dropPoint ||
   1830      !dropPoint.ip ||
   1831      !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)
   1832    ) {
   1833      this._dropIndicator.collapsed = true;
   1834      aEvent.stopPropagation();
   1835      return;
   1836    }
   1837 
   1838    if (dropPoint.folderElt || aEvent.originalTarget == this._chevron) {
   1839      // Dropping over a menubutton or chevron button.
   1840      // Set styles and timer to open relative menupopup.
   1841      let overElt = dropPoint.folderElt || this._chevron;
   1842      if (this._overFolder.elt != overElt) {
   1843        this._clearOverFolder();
   1844        this._overFolder.elt = overElt;
   1845        this._overFolder.openTimer = this._setTimer(this._overFolder.hoverTime);
   1846      }
   1847      if (!this._overFolder.elt.hasAttribute("dragover")) {
   1848        this._overFolder.elt.setAttribute("dragover", "true");
   1849      }
   1850 
   1851      this._dropIndicator.collapsed = true;
   1852    } else {
   1853      // Dragging over a normal toolbarbutton,
   1854      // show indicator bar and move it to the appropriate drop point.
   1855      let ind = this._dropIndicator;
   1856      ind.parentNode.collapsed = false;
   1857      let halfInd = ind.clientWidth / 2;
   1858      let translateX;
   1859      if (this.isRTL) {
   1860        halfInd = Math.ceil(halfInd);
   1861        translateX = 0 - this._rootElt.getBoundingClientRect().right - halfInd;
   1862        if (this._rootElt.firstElementChild) {
   1863          if (dropPoint.beforeIndex == -1) {
   1864            translateX +=
   1865              this._rootElt.lastElementChild.getBoundingClientRect().left;
   1866          } else {
   1867            translateX +=
   1868              this._rootElt.children[
   1869                dropPoint.beforeIndex
   1870              ].getBoundingClientRect().right;
   1871          }
   1872        }
   1873      } else {
   1874        halfInd = Math.floor(halfInd);
   1875        translateX = 0 - this._rootElt.getBoundingClientRect().left + halfInd;
   1876        if (this._rootElt.firstElementChild) {
   1877          if (dropPoint.beforeIndex == -1) {
   1878            translateX +=
   1879              this._rootElt.lastElementChild.getBoundingClientRect().right;
   1880          } else {
   1881            translateX +=
   1882              this._rootElt.children[
   1883                dropPoint.beforeIndex
   1884              ].getBoundingClientRect().left;
   1885          }
   1886        }
   1887      }
   1888 
   1889      ind.style.transform = "translate(" + Math.round(translateX) + "px)";
   1890      ind.style.marginInlineStart = -ind.clientWidth + "px";
   1891      ind.collapsed = false;
   1892 
   1893      // Clear out old folder information.
   1894      this._clearOverFolder();
   1895    }
   1896 
   1897    aEvent.preventDefault();
   1898    aEvent.stopPropagation();
   1899  }
   1900 
   1901  _onDrop(aEvent) {
   1902    PlacesControllerDragHelper.currentDropTarget = aEvent.target;
   1903 
   1904    let dropPoint = this._getDropPoint(aEvent);
   1905    if (dropPoint && dropPoint.ip) {
   1906      PlacesControllerDragHelper.onDrop(
   1907        dropPoint.ip,
   1908        aEvent.dataTransfer
   1909      ).catch(console.error);
   1910      aEvent.preventDefault();
   1911    }
   1912 
   1913    this._cleanupDragDetails();
   1914    aEvent.stopPropagation();
   1915  }
   1916 
   1917  _onDragLeave() {
   1918    PlacesControllerDragHelper.currentDropTarget = null;
   1919 
   1920    this._dropIndicator.collapsed = true;
   1921 
   1922    // If we hovered over a folder, close it now.
   1923    if (this._overFolder.elt) {
   1924      this._overFolder.closeTimer = this._setTimer(this._overFolder.hoverTime);
   1925    }
   1926  }
   1927 
   1928  _onDragEnd() {
   1929    this._cleanupDragDetails();
   1930  }
   1931 
   1932  _onPopupShowing(aEvent) {
   1933    if (!this._allowPopupShowing) {
   1934      this._allowPopupShowing = true;
   1935      aEvent.preventDefault();
   1936      return;
   1937    }
   1938 
   1939    let parent = aEvent.target.parentNode;
   1940    if (parent.localName == "toolbarbutton") {
   1941      this._openedMenuButton = parent;
   1942    }
   1943 
   1944    super._onPopupShowing(aEvent);
   1945  }
   1946 
   1947  _onPopupHidden(aEvent) {
   1948    let popup = aEvent.target;
   1949    let placesNode = popup._placesNode;
   1950    // Avoid handling popuphidden of inner views
   1951    if (
   1952      placesNode &&
   1953      PlacesUIUtils.getViewForNode(popup) == this &&
   1954      // UI performance: folder queries are cheap, keep the resultnode open
   1955      // so we don't rebuild its contents whenever the popup is reopened.
   1956      !PlacesUtils.nodeIsFolderOrShortcut(placesNode)
   1957    ) {
   1958      placesNode.containerOpen = false;
   1959    }
   1960 
   1961    let parent = popup.parentNode;
   1962    if (parent.localName == "toolbarbutton") {
   1963      this._openedMenuButton = null;
   1964      // Clear the dragover attribute if present, if we are dragging into a
   1965      // folder in the hierachy of current opened popup we don't clear
   1966      // this attribute on clearOverFolder.  See Notify for closeTimer.
   1967      if (parent.hasAttribute("dragover")) {
   1968        parent.removeAttribute("dragover");
   1969      }
   1970    }
   1971  }
   1972 
   1973  _onMouseMove(aEvent) {
   1974    // Used in dragStart to prevent dragging folders when dragging down.
   1975    this._cachedMouseMoveEvent = aEvent;
   1976 
   1977    if (
   1978      this._openedMenuButton == null ||
   1979      PlacesControllerDragHelper.getSession()
   1980    ) {
   1981      return;
   1982    }
   1983 
   1984    let target = aEvent.originalTarget;
   1985    if (
   1986      this._openedMenuButton != target &&
   1987      target.localName == "toolbarbutton" &&
   1988      target.type == "menu"
   1989    ) {
   1990      this._openedMenuButton.open = false;
   1991      target.open = true;
   1992    }
   1993  }
   1994 }
   1995 
   1996 /**
   1997 * View for Places menus.  This object should be created during the first
   1998 * popupshowing that's dispatched on the menu.
   1999 *
   2000 */
   2001 class PlacesMenu extends PlacesViewBase {
   2002  /**
   2003   *
   2004   * @param {Event} popupShowingEvent
   2005   *   The event associated with opening the menu.
   2006   * @param {string} placesUrl
   2007   *   The query associated with the view on the menu.
   2008   */
   2009  constructor(popupShowingEvent, placesUrl) {
   2010    super(
   2011      placesUrl,
   2012      popupShowingEvent.target, // <menupopup>
   2013      popupShowingEvent.target.parentNode // <menu>
   2014    );
   2015 
   2016    this._addEventListeners(
   2017      this._rootElt,
   2018      ["popupshowing", "popuphidden"],
   2019      true
   2020    );
   2021    this._addEventListeners(window, ["unload"], false);
   2022    this._addEventListeners(this._rootElt, ["mousedown"], false);
   2023    if (AppConstants.platform === "macosx") {
   2024      // Must walk up to support views in sub-menus, like Bookmarks Toolbar menu.
   2025      for (let elt = this._viewElt.parentNode; elt; elt = elt.parentNode) {
   2026        if (elt.localName == "menubar") {
   2027          this._nativeView = true;
   2028          break;
   2029        }
   2030      }
   2031    }
   2032 
   2033    this._onPopupShowing(popupShowingEvent);
   2034  }
   2035 
   2036  _init() {
   2037    this._viewElt._placesView = this;
   2038  }
   2039 
   2040  _removeChild(aChild) {
   2041    super._removeChild(aChild);
   2042  }
   2043 
   2044  uninit() {
   2045    this._removeEventListeners(
   2046      this._rootElt,
   2047      ["popupshowing", "popuphidden"],
   2048      true
   2049    );
   2050    this._removeEventListeners(window, ["unload"], false);
   2051    this._removeEventListeners(this._rootElt, ["mousedown"], false);
   2052 
   2053    super.uninit();
   2054  }
   2055 
   2056  handleEvent(aEvent) {
   2057    switch (aEvent.type) {
   2058      case "unload":
   2059        this.uninit();
   2060        break;
   2061      case "popupshowing":
   2062        this._onPopupShowing(aEvent);
   2063        break;
   2064      case "popuphidden":
   2065        this._onPopupHidden(aEvent);
   2066        break;
   2067      case "mousedown":
   2068        this._onMouseDown(aEvent);
   2069        break;
   2070    }
   2071  }
   2072 
   2073  _onPopupHidden(aEvent) {
   2074    // Avoid handling popuphidden of inner views.
   2075    let popup = aEvent.originalTarget;
   2076    let placesNode = popup._placesNode;
   2077    if (!placesNode || PlacesUIUtils.getViewForNode(popup) != this) {
   2078      return;
   2079    }
   2080 
   2081    // UI performance: folder queries are cheap, keep the resultnode open
   2082    // so we don't rebuild its contents whenever the popup is reopened.
   2083    if (!PlacesUtils.nodeIsFolderOrShortcut(placesNode)) {
   2084      placesNode.containerOpen = false;
   2085    }
   2086 
   2087    // The autoopened attribute is set for folders which have been
   2088    // automatically opened when dragged over.  Turn off this attribute
   2089    // when the folder closes because it is no longer applicable.
   2090    popup.removeAttribute("autoopened");
   2091    popup.removeAttribute("dragstart");
   2092  }
   2093 
   2094  // We don't have a facility for catch "mousedown" events on the native
   2095  // Mac menus because Mac doesn't expose it
   2096  _onMouseDown(aEvent) {
   2097    PlacesUIUtils.maybeSpeculativeConnectOnMouseDown(aEvent);
   2098  }
   2099 }
   2100 
   2101 // This is used from CustomizableWidgets.sys.mjs using a `window` reference,
   2102 // so we have to expose this on the global.
   2103 this.PlacesPanelview = class PlacesPanelview extends PlacesViewBase {
   2104  constructor(placeUrl, rootElt, viewElt) {
   2105    super(placeUrl, rootElt, viewElt);
   2106    this._viewElt._placesView = this;
   2107    // We're simulating a popup show, because a panelview may only be shown when
   2108    // its containing popup is already shown.
   2109    this._onPopupShowing({ originalTarget: this._rootElt });
   2110    this._addEventListeners(window, ["unload"]);
   2111    this._rootElt.setAttribute("context", "placesContext");
   2112  }
   2113 
   2114  get events() {
   2115    if (this._events) {
   2116      return this._events;
   2117    }
   2118    return (this._events = [
   2119      "click",
   2120      "command",
   2121      "dragend",
   2122      "dragstart",
   2123      "ViewHiding",
   2124      "ViewShown",
   2125      "mousedown",
   2126    ]);
   2127  }
   2128 
   2129  handleEvent(event) {
   2130    switch (event.type) {
   2131      case "click":
   2132        // For middle clicks, fall through to the command handler.
   2133        if (event.button != 1) {
   2134          break;
   2135        }
   2136      // fall through
   2137      case "command":
   2138        this._onCommand(event);
   2139        break;
   2140      case "dragend":
   2141        this._onDragEnd(event);
   2142        break;
   2143      case "dragstart":
   2144        this._onDragStart(event);
   2145        break;
   2146      case "unload":
   2147        this.uninit(event);
   2148        break;
   2149      case "ViewHiding":
   2150        this._onPopupHidden(event);
   2151        break;
   2152      case "ViewShown":
   2153        this._onViewShown(event);
   2154        break;
   2155      case "mousedown":
   2156        this._onMouseDown(event);
   2157        break;
   2158    }
   2159  }
   2160 
   2161  _onCommand(event) {
   2162    event = BrowserUtils.getRootEvent(event);
   2163    let button = event.originalTarget;
   2164    if (!button._placesNode) {
   2165      return;
   2166    }
   2167 
   2168    let modifKey =
   2169      AppConstants.platform === "macosx" ? event.metaKey : event.ctrlKey;
   2170    if (!PlacesUIUtils.openInTabClosesMenu && modifKey) {
   2171      // If 'Recent Bookmarks' in Bookmarks Panel.
   2172      if (button.parentNode.id == "panelMenu_bookmarksMenu") {
   2173        button.setAttribute("closemenu", "none");
   2174      }
   2175    } else {
   2176      button.removeAttribute("closemenu");
   2177    }
   2178    PlacesUIUtils.openNodeWithEvent(button._placesNode, event);
   2179    // Unlike left-click, middle-click requires manual menu closing.
   2180    if (
   2181      button.parentNode.id != "panelMenu_bookmarksMenu" ||
   2182      (event.type == "click" &&
   2183        event.button == 1 &&
   2184        PlacesUIUtils.openInTabClosesMenu)
   2185    ) {
   2186      this.panelMultiView.closest("panel").hidePopup();
   2187    }
   2188  }
   2189 
   2190  destroyContextMenu() {
   2191    super.destroyContextMenu();
   2192    this.maybeClosePanel(PlacesUIUtils.lastContextMenuCommand);
   2193  }
   2194 
   2195  /**
   2196   * Closes the view depending on the command.
   2197   *
   2198   * This is necessary because PlacesPanelview's buttons are not
   2199   * XUL menuitems and are not affected by the closemenu attribute.
   2200   *
   2201   * @param {string} command the placesCommands command
   2202   */
   2203  maybeClosePanel(command) {
   2204    switch (command) {
   2205      // placesCmd_open:newcontainertab is not a placesCommand but it
   2206      // is set by PlacesUIUtils.openInContainerTab to close the panel.
   2207      case "placesCmd_open:newcontainertab":
   2208      case "placesCmd_open:tab":
   2209        if (
   2210          this._viewElt.id != "PanelUI-bookmarks" ||
   2211          PlacesUIUtils.openInTabClosesMenu
   2212        ) {
   2213          this.panelMultiView.closest("panel").hidePopup();
   2214        }
   2215        break;
   2216      case "placesCmd_createBookmark":
   2217      case "placesCmd_deleteDataHost":
   2218        this.panelMultiView.closest("panel").hidePopup();
   2219        break;
   2220    }
   2221  }
   2222 
   2223  _onDragEnd() {
   2224    this._draggedElt = null;
   2225  }
   2226 
   2227  _onDragStart(event) {
   2228    let draggedElt = event.originalTarget;
   2229    if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode) {
   2230      return;
   2231    }
   2232 
   2233    // Activate the view and cache the dragged element.
   2234    this._draggedElt = draggedElt._placesNode;
   2235    this._rootElt.focus();
   2236 
   2237    this._controller.setDataTransfer(event);
   2238    event.stopPropagation();
   2239  }
   2240 
   2241  uninit(event) {
   2242    this._removeEventListeners(this.panelMultiView, this.events);
   2243    this._removeEventListeners(window, ["unload"]);
   2244    delete this.panelMultiView;
   2245    super.uninit(event);
   2246  }
   2247 
   2248  _createDOMNodeForPlacesNode(placesNode) {
   2249    this._domNodes.delete(placesNode);
   2250 
   2251    let element;
   2252    let type = placesNode.type;
   2253    if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
   2254      element = document.createXULElement("toolbarseparator");
   2255    } else {
   2256      if (type != Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
   2257        throw new Error("Unexpected node");
   2258      }
   2259 
   2260      element = document.createXULElement("toolbarbutton");
   2261      element.classList.add(
   2262        "subviewbutton",
   2263        "subviewbutton-iconic",
   2264        "bookmark-item"
   2265      );
   2266      element.setAttribute(
   2267        "scheme",
   2268        PlacesUIUtils.guessUrlSchemeForUI(placesNode.uri)
   2269      );
   2270      element.setAttribute("label", PlacesUIUtils.getBestTitle(placesNode));
   2271 
   2272      let icon = placesNode.icon;
   2273      if (icon) {
   2274        element.setAttribute("image", icon);
   2275      }
   2276    }
   2277 
   2278    element._placesNode = placesNode;
   2279    if (!this._domNodes.has(placesNode)) {
   2280      this._domNodes.set(placesNode, element);
   2281    }
   2282 
   2283    return element;
   2284  }
   2285 
   2286  _setEmptyPopupStatus(panelview, empty = false) {
   2287    if (!panelview._emptyMenuitem) {
   2288      panelview._emptyMenuitem = document.createXULElement("toolbarbutton");
   2289      panelview._emptyMenuitem.setAttribute("disabled", true);
   2290      panelview._emptyMenuitem.className = "subviewbutton";
   2291      document.l10n.setAttributes(
   2292        panelview._emptyMenuitem,
   2293        "places-empty-bookmarks-folder"
   2294      );
   2295    }
   2296 
   2297    if (empty) {
   2298      panelview.setAttribute("emptyplacesresult", "true");
   2299      // Don't add the menuitem if there is static content.
   2300      // We also support external usage for custom crafted panels - which'll have
   2301      // no markers present.
   2302      if (
   2303        !panelview._startMarker ||
   2304        (!panelview._startMarker.previousElementSibling &&
   2305          !panelview._endMarker.nextElementSibling)
   2306      ) {
   2307        panelview.insertBefore(panelview._emptyMenuitem, panelview._endMarker);
   2308      }
   2309    } else {
   2310      panelview.removeAttribute("emptyplacesresult");
   2311      try {
   2312        panelview.removeChild(panelview._emptyMenuitem);
   2313      } catch (ex) {}
   2314    }
   2315  }
   2316 
   2317  _isPopupOpen() {
   2318    return PanelView.forNode(this._viewElt).active;
   2319  }
   2320 
   2321  _onPopupHidden(event) {
   2322    let panelview = event.originalTarget;
   2323    let placesNode = panelview._placesNode;
   2324    // Avoid handling ViewHiding of inner views
   2325    if (
   2326      placesNode &&
   2327      PlacesUIUtils.getViewForNode(panelview) == this &&
   2328      // UI performance: folder queries are cheap, keep the resultnode open
   2329      // so we don't rebuild its contents whenever the popup is reopened.
   2330      !PlacesUtils.nodeIsFolderOrShortcut(placesNode)
   2331    ) {
   2332      placesNode.containerOpen = false;
   2333    }
   2334  }
   2335 
   2336  _onPopupShowing(event) {
   2337    // If the event came from the root element, this is the first time
   2338    // we ever get here.
   2339    if (event.originalTarget == this._rootElt) {
   2340      // Start listening for events from all panels inside the panelmultiview.
   2341      this.panelMultiView = this._viewElt.panelMultiView;
   2342      this._addEventListeners(this.panelMultiView, this.events);
   2343    }
   2344    super._onPopupShowing(event);
   2345  }
   2346 
   2347  _onViewShown(event) {
   2348    if (event.originalTarget != this._viewElt) {
   2349      return;
   2350    }
   2351 
   2352    // Because PanelMultiView reparents the panelview internally, the controller
   2353    // may get lost. In that case we'll append it again, because we certainly
   2354    // need it later!
   2355    if (!this.controllers.getControllerCount() && this._controller) {
   2356      this.controllers.appendController(this._controller);
   2357    }
   2358  }
   2359 
   2360  _onMouseDown(aEvent) {
   2361    PlacesUIUtils.maybeSpeculativeConnectOnMouseDown(aEvent);
   2362  }
   2363 };