tor-browser

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

browser-places.js (72454B)


      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 XPCOMUtils.defineLazyPreferenceGetter(
      6  this,
      7  "NEWTAB_ENABLED",
      8  "browser.newtabpage.enabled",
      9  false
     10 );
     11 
     12 XPCOMUtils.defineLazyPreferenceGetter(
     13  this,
     14  "SHOW_OTHER_BOOKMARKS",
     15  "browser.toolbars.bookmarks.showOtherBookmarks",
     16  true,
     17  () => {
     18    BookmarkingUI.maybeShowOtherBookmarksFolder().then(() => {
     19      document
     20        .getElementById("PlacesToolbar")
     21        ?._placesView?.updateNodesVisibility();
     22    }, console.error);
     23  }
     24 );
     25 
     26 // Set by sync after syncing bookmarks successfully once.
     27 XPCOMUtils.defineLazyPreferenceGetter(
     28  this,
     29  "SHOW_MOBILE_BOOKMARKS",
     30  "browser.bookmarks.showMobileBookmarks",
     31  false
     32 );
     33 
     34 ChromeUtils.defineESModuleGetters(this, {
     35  PanelMultiView:
     36    "moz-src:///browser/components/customizableui/PanelMultiView.sys.mjs",
     37  RecentlyClosedTabsAndWindowsMenuUtils:
     38    "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs",
     39 });
     40 
     41 var StarUI = {
     42  userHasTags: undefined,
     43  _itemGuids: null,
     44  _isNewBookmark: false,
     45  _isComposing: false,
     46  _autoCloseTimer: 0,
     47  // The autoclose timer is diasbled if the user interacts with the
     48  // popup, such as making a change through typing or clicking on
     49  // the popup.
     50  _autoCloseTimerEnabled: true,
     51  // The autoclose timeout length.
     52  _autoCloseTimeout: 3500,
     53  _removeBookmarksOnPopupHidden: false,
     54 
     55  _element(aID) {
     56    return document.getElementById(aID);
     57  },
     58 
     59  // Edit-bookmark panel
     60  get panel() {
     61    delete this.panel;
     62    this._createPanelIfNeeded();
     63    var element = this._element("editBookmarkPanel");
     64    // initially the panel is hidden
     65    // to avoid impacting startup / new window performance
     66    element.hidden = false;
     67    element.addEventListener("keypress", this, { mozSystemGroup: true });
     68    element.addEventListener("mousedown", this);
     69    element.addEventListener("mouseout", this);
     70    element.addEventListener("mousemove", this);
     71    element.addEventListener("compositionstart", this);
     72    element.addEventListener("compositionend", this);
     73    element.addEventListener("input", this);
     74    element.addEventListener("popuphidden", this);
     75    element.addEventListener("popupshown", this);
     76    return (this.panel = element);
     77  },
     78 
     79  // nsIDOMEventListener
     80  handleEvent(aEvent) {
     81    switch (aEvent.type) {
     82      case "mousemove":
     83        clearTimeout(this._autoCloseTimer);
     84        // The autoclose timer is not disabled on generic mouseout
     85        // because the user may not have actually interacted with the popup.
     86        break;
     87      case "popuphidden": {
     88        clearTimeout(this._autoCloseTimer);
     89        if (aEvent.originalTarget == this.panel) {
     90          this._handlePopupHiddenEvent().catch(console.error);
     91        }
     92        break;
     93      }
     94      case "keypress":
     95        clearTimeout(this._autoCloseTimer);
     96        this._autoCloseTimerEnabled = false;
     97 
     98        if (aEvent.defaultPrevented) {
     99          // The event has already been consumed inside of the panel.
    100          break;
    101        }
    102 
    103        switch (aEvent.keyCode) {
    104          case KeyEvent.DOM_VK_ESCAPE:
    105            if (this._isNewBookmark) {
    106              this._removeBookmarksOnPopupHidden = true;
    107            }
    108            this.panel.hidePopup();
    109            break;
    110          case KeyEvent.DOM_VK_RETURN:
    111            if (
    112              aEvent.target.classList.contains("expander-up") ||
    113              aEvent.target.classList.contains("expander-down") ||
    114              aEvent.target.id == "editBMPanel_newFolderButton" ||
    115              aEvent.target.id == "editBookmarkPanelRemoveButton"
    116            ) {
    117              // XXX Why is this necessary? The defaultPrevented check should
    118              //    be enough.
    119              break;
    120            }
    121            this.panel.hidePopup();
    122            break;
    123          // This case is for catching character-generating keypresses
    124          case 0: {
    125            let accessKey = document.getElementById("key_close");
    126            if (eventMatchesKey(aEvent, accessKey)) {
    127              this.panel.hidePopup();
    128            }
    129            break;
    130          }
    131        }
    132        break;
    133      case "compositionend":
    134        // After composition is committed, "mouseout" or something can set
    135        // auto close timer.
    136        this._isComposing = false;
    137        break;
    138      case "compositionstart":
    139        if (aEvent.defaultPrevented) {
    140          // If the composition was canceled, nothing to do here.
    141          break;
    142        }
    143        this._isComposing = true;
    144      // Explicit fall-through, during composition, panel shouldn't be hidden automatically.
    145      case "input":
    146      // Might have edited some text without keyboard events nor composition
    147      // events. Fall-through to cancel auto close in such case.
    148      case "mousedown":
    149        clearTimeout(this._autoCloseTimer);
    150        this._autoCloseTimerEnabled = false;
    151        break;
    152      case "mouseout":
    153        if (!this._autoCloseTimerEnabled) {
    154          // Don't autoclose the popup if the user has made a selection
    155          // or keypress and then subsequently mouseout.
    156          break;
    157        }
    158      // Explicit fall-through
    159      case "popupshown":
    160        // Don't handle events for descendent elements.
    161        if (aEvent.target != aEvent.currentTarget) {
    162          break;
    163        }
    164        // auto-close if new and not interacted with
    165        if (this._isNewBookmark && !this._isComposing) {
    166          let delay = this._autoCloseTimeout;
    167          if (this._closePanelQuickForTesting) {
    168            delay /= 10;
    169          }
    170          clearTimeout(this._autoCloseTimer);
    171          this._autoCloseTimer = setTimeout(() => {
    172            if (!this.panel.matches(":hover")) {
    173              this.panel.hidePopup(true);
    174            }
    175          }, delay);
    176          this._autoCloseTimerEnabled = true;
    177        }
    178        break;
    179    }
    180  },
    181 
    182  /**
    183   * Handle popup hidden event.
    184   */
    185  async _handlePopupHiddenEvent() {
    186    const { bookmarkState, didChangeFolder, selectedFolderGuid } =
    187      gEditItemOverlay;
    188    gEditItemOverlay.uninitPanel(true);
    189 
    190    // Capture _removeBookmarksOnPopupHidden and _itemGuids values. Reset them
    191    // before we handle the next popup.
    192    const removeBookmarksOnPopupHidden = this._removeBookmarksOnPopupHidden;
    193    this._removeBookmarksOnPopupHidden = false;
    194    const guidsForRemoval = this._itemGuids;
    195    this._itemGuids = null;
    196 
    197    if (removeBookmarksOnPopupHidden && guidsForRemoval) {
    198      if (!this._isNewBookmark) {
    199        // Remove all bookmarks for the bookmark's url, this also removes
    200        // the tags for the url.
    201        await PlacesTransactions.Remove(guidsForRemoval).transact();
    202      } else {
    203        BookmarkingUI.star.removeAttribute("starred");
    204      }
    205      return;
    206    }
    207 
    208    await this._storeRecentlyUsedFolder(selectedFolderGuid, didChangeFolder);
    209    await bookmarkState.save();
    210    if (this._isNewBookmark) {
    211      this.showConfirmation();
    212    }
    213  },
    214 
    215  async showEditBookmarkPopup(aNode, aIsNewBookmark, aUrl) {
    216    // Slow double-clicks (not true double-clicks) shouldn't
    217    // cause the panel to flicker.
    218    if (this.panel.state != "closed") {
    219      return;
    220    }
    221 
    222    this._isNewBookmark = aIsNewBookmark;
    223    this._itemGuids = null;
    224 
    225    let titleL10nID = this._isNewBookmark
    226      ? "bookmarks-add-bookmark"
    227      : "bookmarks-edit-bookmark";
    228    document.l10n.setAttributes(
    229      this._element("editBookmarkPanelTitle"),
    230      titleL10nID
    231    );
    232 
    233    this._element("editBookmarkPanel_showForNewBookmarks").checked =
    234      this.showForNewBookmarks;
    235 
    236    this._itemGuids = [];
    237    await PlacesUtils.bookmarks.fetch({ url: aUrl }, bookmark =>
    238      this._itemGuids.push(bookmark.guid)
    239    );
    240 
    241    let removeButton = this._element("editBookmarkPanelRemoveButton");
    242    if (this._isNewBookmark) {
    243      document.l10n.setAttributes(removeButton, "bookmark-panel-cancel");
    244    } else {
    245      // The label of the remove button differs if the URI is bookmarked
    246      // multiple times.
    247      document.l10n.setAttributes(removeButton, "bookmark-panel-remove", {
    248        count: this._itemGuids.length,
    249      });
    250    }
    251 
    252    let onPanelReady = fn => {
    253      let target = this.panel;
    254      if (target.parentNode) {
    255        // By targeting the panel's parent and using a capturing listener, we
    256        // can have our listener called before others waiting for the panel to
    257        // be shown (which probably expect the panel to be fully initialized)
    258        target = target.parentNode;
    259      }
    260      target.addEventListener(
    261        "popupshown",
    262        function () {
    263          fn();
    264        },
    265        { capture: true, once: true }
    266      );
    267    };
    268 
    269    let hiddenRows = ["location", "keyword"];
    270 
    271    if (this.userHasTags === undefined) {
    272      // Cache must be initialized
    273      const fetchedTags = await PlacesUtils.bookmarks.fetchTags();
    274      this.userHasTags = !!fetchedTags.length;
    275    }
    276 
    277    if (!this.userHasTags) {
    278      // Hide tags ui because user has no tags defined
    279      hiddenRows.push("tags");
    280    }
    281 
    282    await gEditItemOverlay.initPanel({
    283      node: aNode,
    284      onPanelReady,
    285      hiddenRows,
    286      focusedElement: "preferred",
    287      isNewBookmark: this._isNewBookmark,
    288    });
    289 
    290    this.panel.openPopup(BookmarkingUI.anchor, "bottomright topright");
    291  },
    292 
    293  _createPanelIfNeeded() {
    294    // Lazy load the editBookmarkPanel the first time we need to display it.
    295    if (!this._element("editBookmarkPanel")) {
    296      MozXULElement.insertFTLIfNeeded("browser/editBookmarkOverlay.ftl");
    297      let template = this._element("editBookmarkPanelTemplate");
    298      let clone = template.content.cloneNode(true);
    299      template.replaceWith(clone);
    300    }
    301  },
    302 
    303  removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() {
    304    this._removeBookmarksOnPopupHidden = true;
    305    this.panel.hidePopup();
    306  },
    307 
    308  async _storeRecentlyUsedFolder(selectedFolderGuid, didChangeFolder) {
    309    if (!selectedFolderGuid) {
    310      return;
    311    }
    312 
    313    // If we're changing where a bookmark gets saved, persist that location.
    314    if (didChangeFolder) {
    315      Services.prefs.setCharPref(
    316        "browser.bookmarks.defaultLocation",
    317        selectedFolderGuid
    318      );
    319    }
    320 
    321    // Don't store folders that are always displayed in "Recent Folders".
    322    if (PlacesUtils.bookmarks.userContentRoots.includes(selectedFolderGuid)) {
    323      return;
    324    }
    325 
    326    // List of recently used folders:
    327    let lastUsedFolderGuids = await PlacesUtils.metadata.get(
    328      PlacesUIUtils.LAST_USED_FOLDERS_META_KEY,
    329      []
    330    );
    331 
    332    let index = lastUsedFolderGuids.indexOf(selectedFolderGuid);
    333    if (index > 1) {
    334      // The guid is in the array but not the most recent.
    335      lastUsedFolderGuids.splice(index, 1);
    336      lastUsedFolderGuids.unshift(selectedFolderGuid);
    337    } else if (index == -1) {
    338      lastUsedFolderGuids.unshift(selectedFolderGuid);
    339    }
    340    while (lastUsedFolderGuids.length > PlacesUIUtils.maxRecentFolders) {
    341      lastUsedFolderGuids.pop();
    342    }
    343 
    344    await PlacesUtils.metadata.set(
    345      PlacesUIUtils.LAST_USED_FOLDERS_META_KEY,
    346      lastUsedFolderGuids
    347    );
    348  },
    349 
    350  onShowForNewBookmarksCheckboxCommand() {
    351    Services.prefs.setBoolPref(
    352      "browser.bookmarks.editDialog.showForNewBookmarks",
    353      this._element("editBookmarkPanel_showForNewBookmarks").checked
    354    );
    355  },
    356 
    357  showConfirmation() {
    358    // Show the "Saved to bookmarks" hint for the first three times
    359    const HINT_COUNT_PREF =
    360      "browser.bookmarks.editDialog.confirmationHintShowCount";
    361    const HINT_COUNT = Services.prefs.getIntPref(HINT_COUNT_PREF, 0);
    362 
    363    if (HINT_COUNT >= 3) {
    364      return;
    365    }
    366    Services.prefs.setIntPref(HINT_COUNT_PREF, HINT_COUNT + 1);
    367 
    368    let anchor;
    369    if (window.toolbar.visible) {
    370      for (let id of ["library-button", "bookmarks-menu-button"]) {
    371        let element = document.getElementById(id);
    372        if (
    373          element &&
    374          element.getAttribute("cui-areatype") != "panel" &&
    375          element.getAttribute("overflowedItem") != "true"
    376        ) {
    377          anchor = element;
    378          break;
    379        }
    380      }
    381    }
    382    if (!anchor) {
    383      anchor = document.getElementById("PanelUI-menu-button");
    384    }
    385    ConfirmationHint.show(anchor, "confirmation-hint-page-bookmarked");
    386  },
    387 };
    388 
    389 XPCOMUtils.defineLazyPreferenceGetter(
    390  StarUI,
    391  "showForNewBookmarks",
    392  "browser.bookmarks.editDialog.showForNewBookmarks"
    393 );
    394 
    395 var PlacesCommandHook = {
    396  /**
    397   * Adds a bookmark to the page loaded in the current browser.
    398   */
    399  async bookmarkPage() {
    400    let browser = gBrowser.selectedBrowser;
    401    let url = URL.fromURI(Services.io.createExposableURI(browser.currentURI));
    402    let info = await PlacesUtils.bookmarks.fetch({ url });
    403    let isNewBookmark = !info;
    404    let showEditUI = !isNewBookmark || StarUI.showForNewBookmarks;
    405    if (isNewBookmark) {
    406      // This is async because we have to validate the guid
    407      // coming from prefs.
    408      let parentGuid = await PlacesUIUtils.defaultParentGuid;
    409      info = { url, parentGuid };
    410      // Bug 1148838 - Make this code work for full page plugins.
    411      let charset = null;
    412 
    413      let isErrorPage = false;
    414      if (browser.documentURI) {
    415        isErrorPage = /^about:(neterror|certerror|blocked)/.test(
    416          browser.documentURI.spec
    417        );
    418      }
    419 
    420      try {
    421        if (isErrorPage) {
    422          let entry = await PlacesUtils.history.fetch(browser.currentURI);
    423          if (entry) {
    424            info.title = entry.title;
    425          }
    426        } else {
    427          info.title = browser.contentTitle;
    428        }
    429        info.title = info.title || url.href;
    430        charset = browser.characterSet;
    431      } catch (e) {
    432        console.error(e);
    433      }
    434 
    435      if (!StarUI.showForNewBookmarks) {
    436        info.guid = await PlacesTransactions.NewBookmark(info).transact();
    437      } else {
    438        info.guid = PlacesUtils.bookmarks.unsavedGuid;
    439        BookmarkingUI.star.setAttribute("starred", "true");
    440      }
    441 
    442      if (charset) {
    443        PlacesUIUtils.setCharsetForPage(url, charset, window).catch(
    444          console.error
    445        );
    446      }
    447    }
    448 
    449    // Revert the contents of the location bar
    450    gURLBar.handleRevert();
    451 
    452    // If it was not requested to open directly in "edit" mode, we are done.
    453    if (!showEditUI) {
    454      StarUI.showConfirmation();
    455      return;
    456    }
    457 
    458    let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(info);
    459 
    460    await StarUI.showEditBookmarkPopup(node, isNewBookmark, url);
    461  },
    462 
    463  /**
    464   * Adds a bookmark to the page targeted by a link.
    465   *
    466   * @param url (string)
    467   *        the address of the link target
    468   * @param title
    469   *        The link text
    470   */
    471  async bookmarkLink(url, title) {
    472    let bm = await PlacesUtils.bookmarks.fetch({ url });
    473    if (bm) {
    474      let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
    475      await PlacesUIUtils.showBookmarkDialog(
    476        { action: "edit", node },
    477        window.top
    478      );
    479      return;
    480    }
    481 
    482    let parentGuid = await PlacesUIUtils.defaultParentGuid;
    483    let defaultInsertionPoint = new PlacesInsertionPoint({
    484      parentGuid,
    485    });
    486    await PlacesUIUtils.showBookmarkDialog(
    487      {
    488        action: "add",
    489        type: "bookmark",
    490        uri: Services.io.newURI(url),
    491        title,
    492        defaultInsertionPoint,
    493        hiddenRows: ["location", "keyword"],
    494      },
    495      window.top
    496    );
    497  },
    498 
    499  /**
    500   * Bookmarks the given tabs loaded in the current browser.
    501   *
    502   * @param {Array} tabs
    503   *        If no given tabs, bookmark all current tabs.
    504   */
    505  async bookmarkTabs(tabs) {
    506    tabs = tabs ?? gBrowser.visibleTabs.filter(tab => !tab.pinned);
    507    let pages = PlacesCommandHook.getUniquePages(tabs).map(
    508      // Bookmark exposable url.
    509      page =>
    510        Object.assign(page, { uri: Services.io.createExposableURI(page.uri) })
    511    );
    512    await PlacesUIUtils.showBookmarkPagesDialog(pages);
    513  },
    514 
    515  /**
    516   * List of nsIURI objects characterizing tabs given in param.
    517   * Duplicates are discarded.
    518   */
    519  getUniquePages(tabs) {
    520    let uniquePages = {};
    521    let URIs = [];
    522 
    523    tabs.forEach(tab => {
    524      let browser = tab.linkedBrowser;
    525      let uri = browser.currentURI;
    526      let title = browser.contentTitle || tab.label;
    527      let spec = uri.spec;
    528      if (!(spec in uniquePages)) {
    529        uniquePages[spec] = null;
    530        URIs.push({ uri, title });
    531      }
    532    });
    533    return URIs;
    534  },
    535 
    536  /**
    537   * Opens the Places Organizer.
    538   *
    539   * @param {string} item The item to select in the organizer window,
    540   *                      options are (case sensitive):
    541   *                      BookmarksMenu, BookmarksToolbar, UnfiledBookmarks,
    542   *                      AllBookmarks, History, Downloads.
    543   */
    544  showPlacesOrganizer(item) {
    545    var organizer = Services.wm.getMostRecentWindow("Places:Organizer");
    546    // Due to bug 528706, getMostRecentWindow can return closed windows.
    547    if (!organizer || organizer.closed) {
    548      // No currently open places window, so open one with the specified mode.
    549      openDialog(
    550        "chrome://browser/content/places/places.xhtml",
    551        "",
    552        "chrome,toolbar=yes,dialog=no,resizable",
    553        item
    554      );
    555    } else {
    556      organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(item);
    557      organizer.focus();
    558    }
    559  },
    560 
    561  async searchBookmarks() {
    562    let win =
    563      BrowserWindowTracker.getTopWindow() ??
    564      (await BrowserWindowTracker.promiseOpenWindow());
    565    win.gURLBar.search(UrlbarTokenizer.RESTRICT.BOOKMARK, {
    566      searchModeEntry: "bookmarkmenu",
    567    });
    568  },
    569 
    570  async searchHistory() {
    571    let win =
    572      BrowserWindowTracker.getTopWindow() ??
    573      (await BrowserWindowTracker.promiseOpenWindow());
    574    win.gURLBar.search(UrlbarTokenizer.RESTRICT.HISTORY, {
    575      searchModeEntry: "historymenu",
    576    });
    577  },
    578 };
    579 
    580 // View for the history menu.
    581 class HistoryMenu extends PlacesMenu {
    582  constructor(aPopupShowingEvent) {
    583    super(aPopupShowingEvent, "place:sort=4&maxResults=15");
    584  }
    585 
    586  // Called by the base class (PlacesViewBase) so we can initialize some
    587  // element references before the several superclass constructors call our
    588  // methods which depend on these.
    589  _init() {
    590    super._init();
    591    let elements = {
    592      undoTabMenu: "historyUndoMenu",
    593      hiddenTabsMenu: "hiddenTabsMenu",
    594      undoWindowMenu: "historyUndoWindowMenu",
    595      syncTabsMenuitem: "sync-tabs-menuitem",
    596    };
    597    for (let [key, elemId] of Object.entries(elements)) {
    598      this[key] = document.getElementById(elemId);
    599    }
    600  }
    601 
    602  toggleHiddenTabs() {
    603    const isShown =
    604      window.gBrowser && gBrowser.visibleTabs.length < gBrowser.tabs.length;
    605    this.hiddenTabsMenu.hidden = !isShown;
    606  }
    607 
    608  toggleRecentlyClosedTabs() {
    609    // enable/disable the Recently Closed Tabs sub menu
    610    // no restorable tabs, so disable menu
    611    if (SessionStore.getClosedTabCount() == 0) {
    612      this.undoTabMenu.setAttribute("disabled", true);
    613    } else {
    614      this.undoTabMenu.removeAttribute("disabled");
    615    }
    616  }
    617 
    618  /**
    619   * Populate when the history menu is opened
    620   */
    621  populateUndoSubmenu() {
    622    var undoPopup = this.undoTabMenu.menupopup;
    623 
    624    // remove existing menu items
    625    while (undoPopup.hasChildNodes()) {
    626      undoPopup.firstChild.remove();
    627    }
    628 
    629    // no restorable tabs, so make sure menu is disabled, and return
    630    if (SessionStore.getClosedTabCount() == 0) {
    631      this.undoTabMenu.setAttribute("disabled", true);
    632      return;
    633    }
    634 
    635    // enable menu
    636    this.undoTabMenu.removeAttribute("disabled");
    637 
    638    // populate menu
    639    let tabsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getTabsFragment(
    640      window,
    641      "menuitem"
    642    );
    643    undoPopup.appendChild(tabsFragment);
    644  }
    645 
    646  toggleRecentlyClosedWindows() {
    647    // enable/disable the Recently Closed Windows sub menu
    648    // no restorable windows, so disable menu
    649    if (SessionStore.getClosedWindowCount() == 0) {
    650      this.undoWindowMenu.setAttribute("disabled", true);
    651    } else {
    652      this.undoWindowMenu.removeAttribute("disabled");
    653    }
    654  }
    655 
    656  /**
    657   * Populate when the history menu is opened
    658   */
    659  populateUndoWindowSubmenu() {
    660    let undoPopup = this.undoWindowMenu.menupopup;
    661 
    662    // remove existing menu items
    663    while (undoPopup.hasChildNodes()) {
    664      undoPopup.firstChild.remove();
    665    }
    666 
    667    // no restorable windows, so make sure menu is disabled, and return
    668    if (SessionStore.getClosedWindowCount() == 0) {
    669      this.undoWindowMenu.setAttribute("disabled", true);
    670      return;
    671    }
    672 
    673    // enable menu
    674    this.undoWindowMenu.removeAttribute("disabled");
    675 
    676    // populate menu
    677    let windowsFragment =
    678      RecentlyClosedTabsAndWindowsMenuUtils.getWindowsFragment(
    679        window,
    680        "menuitem",
    681        /* aPrefixRestoreAll = */ false
    682      );
    683    undoPopup.appendChild(windowsFragment);
    684  }
    685 
    686  toggleTabsFromOtherComputers() {
    687    // Enable/disable the Tabs From Other Computers menu. Some of the menus handled
    688    // by HistoryMenu do not have this menuitem.
    689    if (!this.syncTabsMenuitem) {
    690      return;
    691    }
    692 
    693    if (!PlacesUIUtils.shouldShowTabsFromOtherComputersMenuitem()) {
    694      this.syncTabsMenuitem.hidden = true;
    695      return;
    696    }
    697 
    698    this.syncTabsMenuitem.hidden = false;
    699  }
    700 
    701  _onPopupShowing(aEvent) {
    702    super._onPopupShowing(aEvent);
    703 
    704    // Don't handle events for submenus.
    705    if (aEvent.target != this.rootElement) {
    706      return;
    707    }
    708 
    709    this.toggleHiddenTabs();
    710    this.toggleRecentlyClosedTabs();
    711    this.toggleRecentlyClosedWindows();
    712    this.toggleTabsFromOtherComputers();
    713  }
    714 
    715  _onCommand(aEvent) {
    716    aEvent = BrowserUtils.getRootEvent(aEvent);
    717    let placesNode = aEvent.target._placesNode;
    718    if (placesNode) {
    719      if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
    720        PlacesUIUtils.markPageAsTyped(placesNode.uri);
    721      }
    722      openUILink(placesNode.uri, aEvent, {
    723        ignoreAlt: true,
    724        triggeringPrincipal:
    725          Services.scriptSecurityManager.getSystemPrincipal(),
    726      });
    727    }
    728  }
    729 }
    730 
    731 /**
    732 * Functions for handling events in the Bookmarks Toolbar and menu.
    733 */
    734 var BookmarksEventHandler = {
    735  /**
    736   * Handler for click event for an item in the bookmarks toolbar or menu.
    737   * Menus and submenus from the folder buttons bubble up to this handler.
    738   * Left-click is handled in the onCommand function.
    739   * When items are middle-clicked (or clicked with modifier), open in tabs.
    740   * If the click came through a menu, close the menu.
    741   *
    742   * @param aEvent
    743   *        DOMEvent for the click
    744   * @param aView
    745   *        The places view which aEvent should be associated with.
    746   */
    747 
    748  onMouseUp(aEvent) {
    749    // Handles middle-click or left-click with modifier if not browser.bookmarks.openInTabClosesMenu.
    750    if (aEvent.button == 2 || PlacesUIUtils.openInTabClosesMenu) {
    751      return;
    752    }
    753    let target = aEvent.originalTarget;
    754    if (target.tagName != "menuitem") {
    755      return;
    756    }
    757    let modifKey =
    758      AppConstants.platform === "macosx" ? aEvent.metaKey : aEvent.ctrlKey;
    759    if (modifKey || aEvent.button == 1) {
    760      target.setAttribute("closemenu", "none");
    761      var menupopup = target.parentNode;
    762      menupopup.addEventListener(
    763        "popuphidden",
    764        () => {
    765          target.removeAttribute("closemenu");
    766        },
    767        { once: true }
    768      );
    769    } else {
    770      // Handles edge case where same menuitem was opened previously
    771      // while menu was kept open, but now menu should close.
    772      target.removeAttribute("closemenu");
    773    }
    774  },
    775 
    776  onClick: function BEH_onClick(aEvent, aView) {
    777    // Only handle middle-click or left-click with modifiers.
    778    let modifKey;
    779    if (AppConstants.platform == "macosx") {
    780      modifKey = aEvent.metaKey || aEvent.shiftKey;
    781    } else {
    782      modifKey = aEvent.ctrlKey || aEvent.shiftKey;
    783    }
    784 
    785    if (aEvent.button == 2 || (aEvent.button == 0 && !modifKey)) {
    786      return;
    787    }
    788 
    789    var target = aEvent.originalTarget;
    790    // If this event bubbled up from a menu or menuitem,
    791    // close the menus if browser.bookmarks.openInTabClosesMenu.
    792    var tag = target.tagName;
    793    if (
    794      PlacesUIUtils.openInTabClosesMenu &&
    795      (tag == "menuitem" || tag == "menu")
    796    ) {
    797      closeMenus(aEvent.target);
    798    }
    799 
    800    if (target._placesNode && PlacesUtils.nodeIsContainer(target._placesNode)) {
    801      // Don't open the root folder in tabs when the empty area on the toolbar
    802      // is middle-clicked or when a non-bookmark item (except for Open in Tabs)
    803      // in a bookmarks menupopup is middle-clicked.
    804      if (target.localName == "menu" || target.localName == "toolbarbutton") {
    805        PlacesUIUtils.openMultipleLinksInTabs(
    806          target._placesNode,
    807          aEvent,
    808          aView
    809        );
    810      }
    811    } else if (aEvent.button == 1 && !(tag == "menuitem" || tag == "menu")) {
    812      // Call onCommand in the cases where it's not called automatically:
    813      // Middle-clicks outside of menus.
    814      this.onCommand(aEvent);
    815      aEvent.preventDefault();
    816      aEvent.stopPropagation();
    817    }
    818  },
    819 
    820  /**
    821   * Handler for command event for an item in the bookmarks toolbar.
    822   * Menus and submenus from the folder buttons bubble up to this handler.
    823   * Opens the item.
    824   *
    825   * @param aEvent
    826   *        DOMEvent for the command
    827   */
    828  onCommand: function BEH_onCommand(aEvent) {
    829    var target = aEvent.originalTarget;
    830    if (target._placesNode) {
    831      PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent);
    832      // Only record interactions through the Bookmarks Toolbar
    833      if (target.closest("#PersonalToolbar")) {
    834        Glean.browserEngagement.bookmarksToolbarBookmarkOpened.add(1);
    835      }
    836    }
    837  },
    838 
    839  fillInBHTooltip: function BEH_fillInBHTooltip(aTooltip, aEvent) {
    840    var node;
    841    var cropped = false;
    842    var targetURI;
    843 
    844    if (aTooltip.triggerNode.localName == "treechildren") {
    845      var tree = aTooltip.triggerNode.parentNode;
    846      var cell = tree.getCellAt(aEvent.clientX, aEvent.clientY);
    847      if (cell.row == -1) {
    848        aEvent.preventDefault();
    849        return;
    850      }
    851      node = tree.view.nodeForTreeIndex(cell.row);
    852      cropped = tree.isCellCropped(cell.row, cell.col);
    853    } else {
    854      // Check whether the tooltipNode is a Places node.
    855      // In such a case use it, otherwise check for targetURI attribute.
    856      var tooltipNode = aTooltip.triggerNode;
    857      if (tooltipNode._placesNode) {
    858        node = tooltipNode._placesNode;
    859      } else {
    860        // This is a static non-Places node.
    861        targetURI = tooltipNode.getAttribute("targetURI");
    862      }
    863    }
    864 
    865    if (!node && !targetURI) {
    866      aEvent.preventDefault();
    867      return;
    868    }
    869 
    870    // Show node.label as tooltip's title for non-Places nodes.
    871    var title = node ? node.title : tooltipNode.label;
    872 
    873    // Show URL only for Places URI-nodes or nodes with a targetURI attribute.
    874    var url;
    875    if (targetURI || PlacesUtils.nodeIsURI(node)) {
    876      url = targetURI || node.uri;
    877    }
    878 
    879    // Show tooltip for containers only if their title is cropped.
    880    if (!cropped && !url) {
    881      aEvent.preventDefault();
    882      return;
    883    }
    884 
    885    let tooltipTitle = aEvent.target.querySelector(".places-tooltip-title");
    886    tooltipTitle.hidden = !title || title == url;
    887    if (!tooltipTitle.hidden) {
    888      tooltipTitle.textContent = title;
    889    }
    890 
    891    let tooltipUrl = aEvent.target.querySelector(".places-tooltip-uri");
    892    tooltipUrl.hidden = !url;
    893    if (!tooltipUrl.hidden) {
    894      // Use `value` instead of `textContent` so cropping will apply
    895      tooltipUrl.value = url;
    896    }
    897 
    898    // Show tooltip.
    899  },
    900 };
    901 
    902 // Handles special drag and drop functionality for Places menus that are not
    903 // part of a Places view (e.g. the bookmarks menu in the menubar).
    904 var PlacesMenuDNDHandler = {
    905  _springLoadDelayMs: 350,
    906  _closeDelayMs: 500,
    907  _loadTimer: null,
    908  _closeTimer: null,
    909  _closingTimerNode: null,
    910 
    911  /**
    912   * Called when the user enters the <menu> element during a drag.
    913   *
    914   * @param   event
    915   *          The DragEnter event that spawned the opening.
    916   */
    917  onDragEnter: function PMDH_onDragEnter(event) {
    918    // Opening menus in a Places popup is handled by the view itself.
    919    if (!this._isStaticContainer(event.target)) {
    920      return;
    921    }
    922 
    923    // If we re-enter the same menu or anchor before the close timer runs out,
    924    // we should ensure that we do not close:
    925    if (this._closeTimer && this._closingTimerNode === event.currentTarget) {
    926      this._closeTimer.cancel();
    927      this._closingTimerNode = null;
    928      this._closeTimer = null;
    929    }
    930 
    931    PlacesControllerDragHelper.currentDropTarget = event.target;
    932    let popup = event.target.menupopup;
    933    if (
    934      this._loadTimer ||
    935      popup.state === "showing" ||
    936      popup.state === "open"
    937    ) {
    938      return;
    939    }
    940 
    941    this._loadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    942    this._loadTimer.initWithCallback(
    943      () => {
    944        this._loadTimer = null;
    945        popup.setAttribute("autoopened", "true");
    946        popup.openPopup();
    947      },
    948      this._springLoadDelayMs,
    949      Ci.nsITimer.TYPE_ONE_SHOT
    950    );
    951    event.preventDefault();
    952    event.stopPropagation();
    953  },
    954 
    955  /**
    956   * Handles dragleave on the <menu> element.
    957   */
    958  onDragLeave: function PMDH_onDragLeave(event) {
    959    // Handle menu-button separate targets.
    960    if (
    961      event.relatedTarget === event.currentTarget ||
    962      (event.relatedTarget &&
    963        event.relatedTarget.parentNode === event.currentTarget)
    964    ) {
    965      return;
    966    }
    967 
    968    // Closing menus in a Places popup is handled by the view itself.
    969    if (!this._isStaticContainer(event.target)) {
    970      return;
    971    }
    972 
    973    PlacesControllerDragHelper.currentDropTarget = null;
    974    let popup = event.target.menupopup;
    975 
    976    if (this._loadTimer) {
    977      this._loadTimer.cancel();
    978      this._loadTimer = null;
    979    }
    980    this._closeTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    981    this._closingTimerNode = event.currentTarget;
    982    this._closeTimer.initWithCallback(
    983      function () {
    984        this._closeTimer = null;
    985        this._closingTimerNode = null;
    986        let node = PlacesControllerDragHelper.currentDropTarget;
    987        let inHierarchy = false;
    988        while (node && !inHierarchy) {
    989          inHierarchy = node == event.target;
    990          node = node.parentNode;
    991        }
    992        if (!inHierarchy && popup && popup.hasAttribute("autoopened")) {
    993          popup.removeAttribute("autoopened");
    994          popup.hidePopup();
    995        }
    996      },
    997      this._closeDelayMs,
    998      Ci.nsITimer.TYPE_ONE_SHOT
    999    );
   1000  },
   1001 
   1002  /**
   1003   * Determines if a XUL element represents a static container.
   1004   *
   1005   * @returns true if the element is a container element (menu or
   1006   *`         menu-toolbarbutton), false otherwise.
   1007   */
   1008  _isStaticContainer: function PMDH__isContainer(node) {
   1009    let isMenu =
   1010      node.localName == "menu" ||
   1011      (node.localName == "toolbarbutton" &&
   1012        node.getAttribute("type") == "menu");
   1013    let isStatic =
   1014      !("_placesNode" in node) &&
   1015      node.menupopup &&
   1016      node.menupopup.hasAttribute("placespopup") &&
   1017      !node.parentNode.hasAttribute("placespopup");
   1018    return isMenu && isStatic;
   1019  },
   1020 
   1021  /**
   1022   * Called when the user drags over the <menu> element.
   1023   *
   1024   * @param   event
   1025   *          The DragOver event.
   1026   */
   1027  onDragOver: function PMDH_onDragOver(event) {
   1028    PlacesControllerDragHelper.currentDropTarget = event.target;
   1029    let ip = new PlacesInsertionPoint({
   1030      parentGuid: PlacesUtils.bookmarks.menuGuid,
   1031    });
   1032    if (ip && PlacesControllerDragHelper.canDrop(ip, event.dataTransfer)) {
   1033      event.preventDefault();
   1034    }
   1035 
   1036    event.stopPropagation();
   1037  },
   1038 
   1039  /**
   1040   * Called when the user drops on the <menu> element.
   1041   *
   1042   * @param   event
   1043   *          The Drop event.
   1044   */
   1045  onDrop: function PMDH_onDrop(event) {
   1046    // Put the item at the end of bookmark menu.
   1047    let ip = new PlacesInsertionPoint({
   1048      parentGuid: PlacesUtils.bookmarks.menuGuid,
   1049    });
   1050    PlacesControllerDragHelper.onDrop(ip, event.dataTransfer);
   1051    PlacesControllerDragHelper.currentDropTarget = null;
   1052    event.stopPropagation();
   1053  },
   1054 };
   1055 
   1056 /**
   1057 * This object handles the initialization and uninitialization of the bookmarks
   1058 * toolbar. It also has helper functions for the managed bookmarks button.
   1059 */
   1060 var PlacesToolbarHelper = {
   1061  get _viewElt() {
   1062    return document.getElementById("PlacesToolbar");
   1063  },
   1064 
   1065  /**
   1066   * Initialize. This will check whether we've finished startup and can
   1067   * show toolbars.
   1068   */
   1069  async init() {
   1070    await PlacesUIUtils.canLoadToolbarContentPromise;
   1071    this._realInit();
   1072  },
   1073 
   1074  /**
   1075   * Actually initialize the places view (if needed; we might still no-op).
   1076   */
   1077  _realInit() {
   1078    let viewElt = this._viewElt;
   1079    if (!viewElt || viewElt._placesView || window.closed) {
   1080      return;
   1081    }
   1082 
   1083    // CustomizableUI.addListener is idempotent, so we can safely
   1084    // call this multiple times.
   1085    CustomizableUI.addListener(this);
   1086 
   1087    if (!this._isObservingToolbars) {
   1088      this._isObservingToolbars = true;
   1089      window.addEventListener("toolbarvisibilitychange", this);
   1090    }
   1091 
   1092    // If the bookmarks toolbar item is:
   1093    // - not in a toolbar, or;
   1094    // - the toolbar is collapsed, or;
   1095    // - the toolbar is hidden some other way:
   1096    // don't initialize.  Also, there is no need to initialize the toolbar if
   1097    // customizing, because that will happen when the customization is done.
   1098    let toolbar = this._getParentToolbar(viewElt);
   1099    if (
   1100      !toolbar ||
   1101      toolbar.collapsed ||
   1102      this._isCustomizing ||
   1103      getComputedStyle(toolbar, "").display == "none"
   1104    ) {
   1105      return;
   1106    }
   1107 
   1108    new PlacesToolbar(
   1109      `place:parent=${PlacesUtils.bookmarks.toolbarGuid}`,
   1110      document.getElementById("PlacesToolbarItems"),
   1111      viewElt
   1112    );
   1113 
   1114    if (toolbar.id == "PersonalToolbar") {
   1115      // We just created a new view, thus we must check again the empty toolbar
   1116      // message, regardless of "initialized".
   1117      BookmarkingUI.updateEmptyToolbarMessage()
   1118        .finally(() => {
   1119          toolbar.toggleAttribute("initialized", true);
   1120        })
   1121        .catch(console.error);
   1122    }
   1123  },
   1124 
   1125  async getIsEmpty() {
   1126    if (!this._viewElt._placesView) {
   1127      return true;
   1128    }
   1129    await this._viewElt._placesView.promiseRebuilt();
   1130    return !document.getElementById("PlacesToolbarItems").hasChildNodes();
   1131  },
   1132 
   1133  handleEvent(event) {
   1134    switch (event.type) {
   1135      case "toolbarvisibilitychange":
   1136        if (event.target == this._getParentToolbar(this._viewElt)) {
   1137          this._resetView();
   1138        }
   1139        break;
   1140    }
   1141  },
   1142 
   1143  /**
   1144   * This is a no-op if we haven't been initialized.
   1145   */
   1146  uninit: function PTH_uninit() {
   1147    if (this._isObservingToolbars) {
   1148      delete this._isObservingToolbars;
   1149      window.removeEventListener("toolbarvisibilitychange", this);
   1150    }
   1151    CustomizableUI.removeListener(this);
   1152  },
   1153 
   1154  customizeStart: function PTH_customizeStart() {
   1155    try {
   1156      let viewElt = this._viewElt;
   1157      if (viewElt && viewElt._placesView) {
   1158        viewElt._placesView.uninit();
   1159      }
   1160    } finally {
   1161      this._isCustomizing = true;
   1162    }
   1163  },
   1164 
   1165  customizeDone: function PTH_customizeDone() {
   1166    this._isCustomizing = false;
   1167    this.init();
   1168  },
   1169 
   1170  onPlaceholderCommand() {
   1171    let widgetGroup = CustomizableUI.getWidget("personal-bookmarks");
   1172    let widget = widgetGroup.forWindow(window);
   1173    if (
   1174      widget.overflowed ||
   1175      widgetGroup.areaType == CustomizableUI.TYPE_PANEL
   1176    ) {
   1177      PlacesCommandHook.showPlacesOrganizer("BookmarksToolbar");
   1178    }
   1179  },
   1180 
   1181  _getParentToolbar(element) {
   1182    while (element) {
   1183      if (element.localName == "toolbar") {
   1184        return element;
   1185      }
   1186      element = element.parentNode;
   1187    }
   1188    return null;
   1189  },
   1190 
   1191  onWidgetUnderflow(aNode) {
   1192    // The view gets broken by being removed and reinserted by the overflowable
   1193    // toolbar, so we have to force an uninit and reinit.
   1194    let win = aNode.ownerGlobal;
   1195    if (aNode.id == "personal-bookmarks" && win == window) {
   1196      this._resetView();
   1197    }
   1198  },
   1199 
   1200  onWidgetAdded(aWidgetId) {
   1201    if (aWidgetId == "personal-bookmarks" && !this._isCustomizing) {
   1202      // It's possible (with the "Add to Menu", "Add to Toolbar" context
   1203      // options) that the Places Toolbar Items have been moved without
   1204      // letting us prepare and handle it with with customizeStart and
   1205      // customizeDone. If that's the case, we need to reset the views
   1206      // since they're probably broken from the DOM reparenting.
   1207      this._resetView();
   1208    }
   1209  },
   1210 
   1211  _resetView() {
   1212    if (this._viewElt) {
   1213      // It's possible that the placesView might not exist, and we need to
   1214      // do a full init. This could happen if the Bookmarks Toolbar Items are
   1215      // moved to the Menu Panel, and then to the toolbar with the "Add to Toolbar"
   1216      // context menu option, outside of customize mode.
   1217      if (this._viewElt._placesView) {
   1218        this._viewElt._placesView.uninit();
   1219      }
   1220      this.init();
   1221    }
   1222  },
   1223 
   1224  async populateManagedBookmarks(popup) {
   1225    if (popup.hasChildNodes()) {
   1226      return;
   1227    }
   1228    // Show item's uri in the status bar when hovering, and clear on exit
   1229    popup.addEventListener("DOMMenuItemActive", function (event) {
   1230      XULBrowserWindow.setOverLink(event.target.link);
   1231    });
   1232    popup.addEventListener("DOMMenuItemInactive", function () {
   1233      XULBrowserWindow.setOverLink("");
   1234    });
   1235    let fragment = document.createDocumentFragment();
   1236    await this.addManagedBookmarks(
   1237      fragment,
   1238      Services.policies.getActivePolicies().ManagedBookmarks
   1239    );
   1240    popup.appendChild(fragment);
   1241  },
   1242 
   1243  async addManagedBookmarks(menu, children) {
   1244    for (let i = 0; i < children.length; i++) {
   1245      let entry = children[i];
   1246      if (entry.children) {
   1247        // It's a folder.
   1248        let submenu = document.createXULElement("menu");
   1249        if (entry.name) {
   1250          submenu.setAttribute("label", entry.name);
   1251        } else {
   1252          document.l10n.setAttributes(submenu, "managed-bookmarks-subfolder");
   1253        }
   1254        submenu.setAttribute("container", "true");
   1255        submenu.classList.add("menu-iconic", "bookmark-item");
   1256        let submenupopup = document.createXULElement("menupopup");
   1257        submenu.appendChild(submenupopup);
   1258        menu.appendChild(submenu);
   1259        this.addManagedBookmarks(submenupopup, entry.children);
   1260      } else if (entry.name && entry.url) {
   1261        // It's bookmark.
   1262        let { preferredURI } = Services.uriFixup.getFixupURIInfo(entry.url);
   1263        let menuitem = document.createXULElement("menuitem");
   1264        menuitem.setAttribute("label", entry.name);
   1265        menuitem.setAttribute(
   1266          "image",
   1267          "page-icon:" + ChromeUtils.encodeURIForSrcset(preferredURI.spec)
   1268        );
   1269        menuitem.classList.add(
   1270          "menuitem-iconic",
   1271          "menuitem-with-favicon",
   1272          "bookmark-item"
   1273        );
   1274        menuitem.link = preferredURI.spec;
   1275        menu.appendChild(menuitem);
   1276      }
   1277    }
   1278  },
   1279 
   1280  openManagedBookmark(event) {
   1281    openUILink(event.target.link, event, {
   1282      triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
   1283    });
   1284  },
   1285 
   1286  onDragStartManaged(event) {
   1287    if (!event.target.link) {
   1288      return;
   1289    }
   1290 
   1291    let dt = event.dataTransfer;
   1292 
   1293    let node = {};
   1294    node.type = 0;
   1295    node.title = event.target.label;
   1296    node.uri = event.target.link;
   1297 
   1298    function addData(type, index) {
   1299      let wrapNode = PlacesUtils.wrapNode(node, type);
   1300      dt.mozSetDataAt(type, wrapNode, index);
   1301    }
   1302 
   1303    addData(PlacesUtils.TYPE_X_MOZ_URL, 0);
   1304    addData(PlacesUtils.TYPE_PLAINTEXT, 0);
   1305    addData(PlacesUtils.TYPE_HTML, 0);
   1306  },
   1307 };
   1308 
   1309 /**
   1310 * Handles the bookmarks menu-button in the toolbar.
   1311 */
   1312 
   1313 var BookmarkingUI = {
   1314  STAR_ID: "star-button",
   1315  STAR_BOX_ID: "star-button-box",
   1316  BOOKMARK_BUTTON_ID: "bookmarks-menu-button",
   1317  BOOKMARK_BUTTON_SHORTCUT: "addBookmarkAsKb",
   1318  get button() {
   1319    delete this.button;
   1320    let widgetGroup = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID);
   1321    return (this.button = widgetGroup.forWindow(window).node);
   1322  },
   1323 
   1324  get star() {
   1325    delete this.star;
   1326    return (this.star = document.getElementById(this.STAR_ID));
   1327  },
   1328 
   1329  get starBox() {
   1330    delete this.starBox;
   1331    return (this.starBox = document.getElementById(this.STAR_BOX_ID));
   1332  },
   1333 
   1334  get anchor() {
   1335    let action = PageActions.actionForID(PageActions.ACTION_ID_BOOKMARK);
   1336    return BrowserPageActions.panelAnchorNodeForAction(action);
   1337  },
   1338 
   1339  get stringbundleset() {
   1340    delete this.stringbundleset;
   1341    return (this.stringbundleset = document.getElementById("stringbundleset"));
   1342  },
   1343 
   1344  get toolbar() {
   1345    delete this.toolbar;
   1346    return (this.toolbar = document.getElementById("PersonalToolbar"));
   1347  },
   1348 
   1349  STATUS_UPDATING: -1,
   1350  STATUS_UNSTARRED: 0,
   1351  STATUS_STARRED: 1,
   1352  get status() {
   1353    if (this._pendingUpdate) {
   1354      return this.STATUS_UPDATING;
   1355    }
   1356    return this.star.hasAttribute("starred")
   1357      ? this.STATUS_STARRED
   1358      : this.STATUS_UNSTARRED;
   1359  },
   1360 
   1361  onPopupShowing: function BUI_onPopupShowing(event) {
   1362    // Don't handle events for submenus.
   1363    if (event.target.id != "BMB_bookmarksPopup") {
   1364      return;
   1365    }
   1366 
   1367    // On non-photon, this code should never be reached. However, if you click
   1368    // the outer button's border, some cpp code for the menu button's XBL
   1369    // binding decides to open the popup even though the dropmarker is invisible.
   1370    //
   1371    // Separately, in Photon, if the button is in the dynamic portion of the
   1372    // overflow panel, we want to show a subview instead.
   1373    if (
   1374      this.button.getAttribute("cui-areatype") == CustomizableUI.TYPE_PANEL ||
   1375      this.button.hasAttribute("overflowedItem")
   1376    ) {
   1377      this._showSubView();
   1378      event.preventDefault();
   1379      event.stopPropagation();
   1380      return;
   1381    }
   1382 
   1383    let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID).forWindow(
   1384      window
   1385    );
   1386    if (widget.overflowed) {
   1387      // Don't open a popup in the overflow popup, rather just open the Library.
   1388      event.preventDefault();
   1389      widget.node.removeAttribute("closemenu");
   1390      PlacesCommandHook.showPlacesOrganizer("BookmarksMenu");
   1391      return;
   1392    }
   1393 
   1394    document.getElementById("BMB_mobileBookmarks").hidden =
   1395      !SHOW_MOBILE_BOOKMARKS;
   1396 
   1397    this.updateLabel(
   1398      "BMB_viewBookmarksSidebar",
   1399      SidebarController.currentID == "viewBookmarksSidebar"
   1400    );
   1401    this.updateLabel("BMB_viewBookmarksToolbar", !this.toolbar.collapsed);
   1402  },
   1403 
   1404  updateLabel(elementId, visible) {
   1405    let element = PanelMultiView.getViewNode(document, elementId);
   1406    let l10nID = element.getAttribute("data-l10n-id");
   1407    document.l10n.setAttributes(element, l10nID, { isVisible: !!visible });
   1408  },
   1409 
   1410  toggleBookmarksToolbar(reason) {
   1411    let newState = this.toolbar.collapsed ? "always" : "never";
   1412    Services.prefs.setCharPref(
   1413      "browser.toolbars.bookmarks.visibility",
   1414      // See firefox.js for possible values
   1415      newState
   1416    );
   1417 
   1418    CustomizableUI.setToolbarVisibility(this.toolbar.id, newState, false);
   1419    BrowserUsageTelemetry.recordToolbarVisibility(
   1420      this.toolbar.id,
   1421      newState,
   1422      reason
   1423    );
   1424  },
   1425 
   1426  isOnNewTabPage(uri) {
   1427    if (!uri) {
   1428      return false;
   1429    }
   1430    // Prevent loading AboutNewTab.sys.mjs during startup path if it
   1431    // is only the newTabURL getter we are interested in.
   1432    let newTabURL = Cu.isESModuleLoaded(
   1433      "resource:///modules/AboutNewTab.sys.mjs"
   1434    )
   1435      ? AboutNewTab.newTabURL
   1436      : "about:newtab";
   1437    // Don't treat a custom "about:blank" new tab URL as the "New Tab Page"
   1438    // due to about:blank being used in different contexts and the
   1439    // difficulty in determining if the eventual page load is
   1440    // about:blank or if the about:blank load is just temporary.
   1441    if (newTabURL == "about:blank") {
   1442      newTabURL = "about:newtab";
   1443    }
   1444    let newTabURLs = [
   1445      newTabURL,
   1446      "about:home",
   1447      "chrome://browser/content/blanktab.html",
   1448      // Add the "about:tor" uri. See tor-browser#41717.
   1449      // NOTE: "about:newtab", "about:welcome", "about:home" and
   1450      // "about:privatebrowsing" can also redirect to "about:tor".
   1451      "about:tor",
   1452    ];
   1453    if (PrivateBrowsingUtils.isWindowPrivate(window)) {
   1454      newTabURLs.push("about:privatebrowsing");
   1455    }
   1456    return newTabURLs.some(newTabUriString =>
   1457      this._newTabURI(newTabUriString)?.equalsExceptRef(uri)
   1458    );
   1459  },
   1460 
   1461  _newTabURI(uriString) {
   1462    let uri = this._newTabURICache.get(uriString);
   1463    if (uri === undefined) {
   1464      uri = Services.io.newURI(uriString);
   1465      this._newTabURICache.set(uriString, uri);
   1466    }
   1467    return uri;
   1468  },
   1469  _newTabURICache: new Map(),
   1470 
   1471  buildBookmarksToolbarSubmenu(toolbar) {
   1472    let alwaysShowMenuItem = document.createXULElement("menuitem");
   1473    let alwaysHideMenuItem = document.createXULElement("menuitem");
   1474    let showOnNewTabMenuItem = document.createXULElement("menuitem");
   1475    let menuPopup = document.createXULElement("menupopup");
   1476    menuPopup.append(
   1477      alwaysShowMenuItem,
   1478      showOnNewTabMenuItem,
   1479      alwaysHideMenuItem
   1480    );
   1481    let menu = document.createXULElement("menu");
   1482    menu.appendChild(menuPopup);
   1483 
   1484    menu.setAttribute("label", toolbar.getAttribute("toolbarname"));
   1485    menu.setAttribute("id", "toggle_" + toolbar.id);
   1486    menu.setAttribute("accesskey", toolbar.getAttribute("accesskey"));
   1487    menu.setAttribute("toolbarId", toolbar.id);
   1488 
   1489    // Used by the Places context menu in the Bookmarks Toolbar
   1490    // when nothing is selected
   1491    menu.setAttribute("selection-type", "none|single");
   1492 
   1493    MozXULElement.insertFTLIfNeeded("browser/toolbarContextMenu.ftl");
   1494    let menuItems = [
   1495      [
   1496        showOnNewTabMenuItem,
   1497        "toolbar-context-menu-bookmarks-toolbar-on-new-tab-2",
   1498        "newtab",
   1499      ],
   1500      [
   1501        alwaysShowMenuItem,
   1502        "toolbar-context-menu-bookmarks-toolbar-always-show-2",
   1503        "always",
   1504      ],
   1505      [
   1506        alwaysHideMenuItem,
   1507        "toolbar-context-menu-bookmarks-toolbar-never-show-2",
   1508        "never",
   1509      ],
   1510    ];
   1511    menuItems.map(([menuItem, l10nId, visibilityEnum]) => {
   1512      document.l10n.setAttributes(menuItem, l10nId);
   1513      menuItem.setAttribute("type", "radio");
   1514      // The persisted state of the PersonalToolbar is stored in
   1515      // "browser.toolbars.bookmarks.visibility".
   1516      menuItem.toggleAttribute(
   1517        "checked",
   1518        gBookmarksToolbarVisibility == visibilityEnum
   1519      );
   1520      // Identify these items for "onViewToolbarCommand" so
   1521      // we know to check the visibilityEnum value.
   1522      menuItem.dataset.bookmarksToolbarVisibility = true;
   1523      menuItem.dataset.visibilityEnum = visibilityEnum;
   1524      menuItem.addEventListener("command", onViewToolbarCommand);
   1525    });
   1526    let menuItemForNextStateFromKbShortcut =
   1527      gBookmarksToolbarVisibility == "never"
   1528        ? alwaysShowMenuItem
   1529        : alwaysHideMenuItem;
   1530    menuItemForNextStateFromKbShortcut.setAttribute(
   1531      "key",
   1532      "viewBookmarksToolbarKb"
   1533    );
   1534 
   1535    return menu;
   1536  },
   1537 
   1538  /**
   1539   * Check if we need to make the empty toolbar message `hidden`.
   1540   * We'll have it unhidden during startup, to make sure the toolbar
   1541   * has height, and we'll unhide it if there is nothing else on the toolbar.
   1542   * We hide it in customize mode, unless there's nothing on the toolbar.
   1543   */
   1544  async updateEmptyToolbarMessage() {
   1545    let { initialHiddenState, checkHasBookmarks } = (() => {
   1546      // Do we have visible kids?
   1547      if (
   1548        this.toolbar.querySelector(
   1549          `:scope > toolbarpaletteitem > toolbarbutton:not([hidden]),
   1550           :scope > toolbarpaletteitem > toolbaritem:not([hidden], #personal-bookmarks),
   1551           :scope > toolbarbutton:not([hidden]),
   1552           :scope > toolbaritem:not([hidden], #personal-bookmarks)`
   1553        )
   1554      ) {
   1555        return { initialHiddenState: true, checkHasBookmarks: false };
   1556      }
   1557 
   1558      if (this._isCustomizing) {
   1559        return { initialHiddenState: true, checkHasBookmarks: false };
   1560      }
   1561 
   1562      // If bookmarks have been moved out of the toolbar, we show the message.
   1563      let bookmarksToolbarItemsPlacement =
   1564        CustomizableUI.getPlacementOfWidget("personal-bookmarks");
   1565      let bookmarksItemInToolbar =
   1566        bookmarksToolbarItemsPlacement?.area == CustomizableUI.AREA_BOOKMARKS;
   1567      if (!bookmarksItemInToolbar) {
   1568        return { initialHiddenState: false, checkHasBookmarks: false };
   1569      }
   1570 
   1571      if (!this.toolbar.hasAttribute("initialized")) {
   1572        // If the toolbar has not been initialized yet, unhide the message, it
   1573        // will be made 0-width and visibility: hidden anyway, to keep the
   1574        // toolbar height stable.
   1575        return { initialHiddenState: false, checkHasBookmarks: true };
   1576      }
   1577 
   1578      // Check visible bookmark nodes.
   1579      if (
   1580        this.toolbar.querySelector(
   1581          `#PlacesToolbarItems > toolbarseparator,
   1582           #PlacesToolbarItems > toolbarbutton`
   1583        )
   1584      ) {
   1585        return { initialHiddenState: true, checkHasBookmarks: false };
   1586      }
   1587      return { initialHiddenState: true, checkHasBookmarks: true };
   1588    })();
   1589 
   1590    let emptyMsg = document.getElementById("personal-toolbar-empty");
   1591    emptyMsg.hidden = initialHiddenState;
   1592    if (checkHasBookmarks) {
   1593      emptyMsg.hidden = !(await PlacesToolbarHelper.getIsEmpty());
   1594    }
   1595  },
   1596 
   1597  _uninitView: function BUI__uninitView() {
   1598    // When an element with a placesView attached is removed and re-inserted,
   1599    // XBL reapplies the binding causing any kind of issues and possible leaks,
   1600    // so kill current view and let popupshowing generate a new one.
   1601    if (this.button._placesView) {
   1602      this.button._placesView.uninit();
   1603    }
   1604    // Also uninit the main menubar placesView, since it would have the same
   1605    // issues.
   1606    let menubar = document.getElementById("bookmarksMenu");
   1607    if (menubar && menubar._placesView) {
   1608      menubar._placesView.uninit();
   1609    }
   1610 
   1611    // We have to do the same thing for the "special" views underneath the
   1612    // the bookmarks menu.
   1613    const kSpecialViewNodeIDs = [
   1614      "BMB_bookmarksToolbar",
   1615      "BMB_unsortedBookmarks",
   1616    ];
   1617    for (let viewNodeID of kSpecialViewNodeIDs) {
   1618      let elem = document.getElementById(viewNodeID);
   1619      if (elem && elem._placesView) {
   1620        elem._placesView.uninit();
   1621      }
   1622    }
   1623  },
   1624 
   1625  onCustomizeStart: function BUI_customizeStart(aWindow) {
   1626    if (aWindow == window) {
   1627      this._uninitView();
   1628      this._isCustomizing = true;
   1629 
   1630      this.updateEmptyToolbarMessage().catch(console.error);
   1631 
   1632      let isVisible =
   1633        Services.prefs.getCharPref(
   1634          "browser.toolbars.bookmarks.visibility",
   1635          "newtab"
   1636        ) != "never";
   1637      // Temporarily show the bookmarks toolbar in Customize mode if
   1638      // the toolbar isn't set to Never. We don't have to worry about
   1639      // hiding when leaving customize mode since the toolbar will
   1640      // hide itself on location change.
   1641      setToolbarVisibility(this.toolbar, isVisible, false);
   1642    }
   1643  },
   1644 
   1645  onWidgetAdded: function BUI_widgetAdded(aWidgetId, aArea) {
   1646    if (aWidgetId == this.BOOKMARK_BUTTON_ID) {
   1647      this._onWidgetWasMoved();
   1648    }
   1649    if (aArea == CustomizableUI.AREA_BOOKMARKS) {
   1650      this.updateEmptyToolbarMessage().catch(console.error);
   1651    }
   1652  },
   1653 
   1654  onWidgetRemoved: function BUI_widgetRemoved(aWidgetId, aOldArea) {
   1655    if (aWidgetId == this.BOOKMARK_BUTTON_ID) {
   1656      this._onWidgetWasMoved();
   1657    }
   1658    if (aOldArea == CustomizableUI.AREA_BOOKMARKS) {
   1659      this.updateEmptyToolbarMessage().catch(console.error);
   1660    }
   1661  },
   1662 
   1663  onWidgetReset: function BUI_widgetReset(aNode) {
   1664    if (aNode == this.button) {
   1665      this._onWidgetWasMoved();
   1666    }
   1667  },
   1668 
   1669  onWidgetUndoMove: function BUI_undoWidgetUndoMove(aNode) {
   1670    if (aNode == this.button) {
   1671      this._onWidgetWasMoved();
   1672    }
   1673  },
   1674 
   1675  onWidgetBeforeDOMChange: function BUI_onWidgetBeforeDOMChange(
   1676    aNode,
   1677    aNextNode,
   1678    aContainer,
   1679    aIsRemoval
   1680  ) {
   1681    if (aNode.id == "import-button") {
   1682      this._updateImportButton(aNode, aIsRemoval ? null : aContainer);
   1683    }
   1684  },
   1685 
   1686  _updateImportButton: function BUI_updateImportButton(aNode, aContainer) {
   1687    // The import button behaves like a bookmark item when in the bookmarks
   1688    // toolbar, otherwise like a regular toolbar button.
   1689    let isBookmarkItem = aContainer == this.toolbar;
   1690    aNode.classList.toggle("toolbarbutton-1", !isBookmarkItem);
   1691    aNode.classList.toggle("bookmark-item", isBookmarkItem);
   1692  },
   1693 
   1694  _onWidgetWasMoved: function BUI_widgetWasMoved() {
   1695    // If we're moved outside of customize mode, we need to uninit
   1696    // our view so it gets reconstructed.
   1697    if (!this._isCustomizing) {
   1698      this._uninitView();
   1699    }
   1700  },
   1701 
   1702  onCustomizeEnd: function BUI_customizeEnd(aWindow) {
   1703    if (aWindow == window) {
   1704      this._isCustomizing = false;
   1705      this.updateEmptyToolbarMessage().catch(console.error);
   1706    }
   1707  },
   1708 
   1709  init() {
   1710    CustomizableUI.addListener(this);
   1711    let importButton = document.getElementById("import-button");
   1712    if (importButton) {
   1713      this._updateImportButton(importButton, importButton.parentNode);
   1714    }
   1715    this.updateEmptyToolbarMessage().catch(console.error);
   1716  },
   1717 
   1718  _hasBookmarksObserver: false,
   1719  _itemGuids: new Set(),
   1720  uninit: function BUI_uninit() {
   1721    this.updateBookmarkPageMenuItem(true);
   1722    CustomizableUI.removeListener(this);
   1723 
   1724    this._uninitView();
   1725 
   1726    if (this._hasBookmarksObserver) {
   1727      PlacesUtils.observers.removeListener(
   1728        [
   1729          "bookmark-added",
   1730          "bookmark-removed",
   1731          "bookmark-moved",
   1732          "bookmark-url-changed",
   1733        ],
   1734        this.handlePlacesEvents
   1735      );
   1736    }
   1737 
   1738    if (this._pendingUpdate) {
   1739      delete this._pendingUpdate;
   1740    }
   1741  },
   1742 
   1743  onLocationChange: function BUI_onLocationChange() {
   1744    if (this._uri && gBrowser.currentURI.equals(this._uri)) {
   1745      return;
   1746    }
   1747    this.updateStarState();
   1748  },
   1749 
   1750  updateStarState: function BUI_updateStarState() {
   1751    this._uri = gBrowser.currentURI;
   1752    this._itemGuids.clear();
   1753    let guids = new Set();
   1754 
   1755    // those objects are use to check if we are in the current iteration before
   1756    // returning any result.
   1757    let pendingUpdate = (this._pendingUpdate = {});
   1758 
   1759    PlacesUtils.bookmarks
   1760      .fetch({ url: this._uri }, b => guids.add(b.guid), { concurrent: true })
   1761      .catch(console.error)
   1762      .then(() => {
   1763        if (pendingUpdate != this._pendingUpdate) {
   1764          return;
   1765        }
   1766 
   1767        // It's possible that "bookmark-added" gets called before the async statement
   1768        // calls back.  For such an edge case, retain all unique entries from the
   1769        // array.
   1770        if (this._itemGuids.size > 0) {
   1771          this._itemGuids = new Set(...this._itemGuids, ...guids);
   1772        } else {
   1773          this._itemGuids = guids;
   1774        }
   1775 
   1776        this._updateStar();
   1777 
   1778        // Start observing bookmarks if needed.
   1779        if (!this._hasBookmarksObserver) {
   1780          try {
   1781            this.handlePlacesEvents = this.handlePlacesEvents.bind(this);
   1782            PlacesUtils.observers.addListener(
   1783              [
   1784                "bookmark-added",
   1785                "bookmark-removed",
   1786                "bookmark-moved",
   1787                "bookmark-url-changed",
   1788              ],
   1789              this.handlePlacesEvents
   1790            );
   1791            this._hasBookmarksObserver = true;
   1792          } catch (ex) {
   1793            console.error(
   1794              "BookmarkingUI failed adding a bookmarks observer: ",
   1795              ex
   1796            );
   1797          }
   1798        }
   1799 
   1800        delete this._pendingUpdate;
   1801      });
   1802  },
   1803 
   1804  _updateStar: function BUI__updateStar() {
   1805    let starred = this._itemGuids.size > 0;
   1806 
   1807    // Update the image for all elements.
   1808    for (let element of [
   1809      this.star,
   1810      document.getElementById("context-bookmarkpage"),
   1811      PanelMultiView.getViewNode(document, "panelMenuBookmarkThisPage"),
   1812      document.getElementById("pageAction-panel-bookmark"),
   1813    ]) {
   1814      if (!element) {
   1815        // The page action panel element may not have been created yet.
   1816        continue;
   1817      }
   1818      element.toggleAttribute("starred", starred);
   1819    }
   1820 
   1821    if (!this.starBox) {
   1822      // The BOOKMARK_BUTTON_SHORTCUT exists only in browser.xhtml.
   1823      // Return early if we're not in this context, but still reset the
   1824      // Bookmark Page items.
   1825      this.updateBookmarkPageMenuItem(true);
   1826      return;
   1827    }
   1828 
   1829    // Update the tooltip for elements that require it.
   1830    let shortcut = document.getElementById(this.BOOKMARK_BUTTON_SHORTCUT);
   1831    let l10nArgs = {
   1832      shortcut: ShortcutUtils.prettifyShortcut(shortcut),
   1833    };
   1834    document.l10n.setAttributes(
   1835      this.starBox,
   1836      starred ? "urlbar-star-edit-bookmark" : "urlbar-star-add-bookmark",
   1837      l10nArgs
   1838    );
   1839 
   1840    // Update the Bookmark Page menuitem when bookmarked state changes.
   1841    this.updateBookmarkPageMenuItem();
   1842 
   1843    Services.obs.notifyObservers(
   1844      null,
   1845      "bookmark-icon-updated",
   1846      starred ? "starred" : "unstarred"
   1847    );
   1848  },
   1849 
   1850  /**
   1851   * Update the "Bookmark Page…" menuitems on the menubar, panels, context
   1852   * menu and page actions.
   1853   *
   1854   * @param {boolean} [forceReset] passed when we're destroyed and the label
   1855   * should go back to the default (Bookmark Page), for MacOS.
   1856   */
   1857  updateBookmarkPageMenuItem(forceReset = false) {
   1858    let isStarred = !forceReset && this._itemGuids.size > 0;
   1859    // Define the l10n id which will be used to localize elements
   1860    // that only require a label using the menubar.ftl messages.
   1861    let menuItemL10nId = isStarred ? "menu-edit-bookmark" : "menu-bookmark-tab";
   1862    let menuItem = document.getElementById("menu_bookmarkThisPage");
   1863    if (menuItem) {
   1864      // Localize the menubar item.
   1865      document.l10n.setAttributes(menuItem, menuItemL10nId);
   1866    }
   1867 
   1868    let panelMenuItemL10nId = isStarred
   1869      ? "bookmarks-subview-edit-bookmark"
   1870      : "bookmarks-subview-bookmark-tab";
   1871    let panelMenuToolbarButton = PanelMultiView.getViewNode(
   1872      document,
   1873      "panelMenuBookmarkThisPage"
   1874    );
   1875    if (panelMenuToolbarButton) {
   1876      document.l10n.setAttributes(panelMenuToolbarButton, panelMenuItemL10nId);
   1877    }
   1878 
   1879    // Localize the context menu item element.
   1880    let contextItem = document.getElementById("context-bookmarkpage");
   1881    // On macOS regular menuitems are used and the shortcut isn't added
   1882    if (contextItem) {
   1883      if (AppConstants.platform == "macosx") {
   1884        let contextItemL10nId = isStarred
   1885          ? "main-context-menu-edit-bookmark-mac"
   1886          : "main-context-menu-bookmark-page-mac";
   1887        document.l10n.setAttributes(contextItem, contextItemL10nId);
   1888      } else {
   1889        let shortcutElem = document.getElementById(
   1890          this.BOOKMARK_BUTTON_SHORTCUT
   1891        );
   1892        if (shortcutElem) {
   1893          let shortcut = ShortcutUtils.prettifyShortcut(shortcutElem);
   1894          let contextItemL10nId = isStarred
   1895            ? "main-context-menu-edit-bookmark-with-shortcut"
   1896            : "main-context-menu-bookmark-page-with-shortcut";
   1897          let l10nArgs = { shortcut };
   1898          document.l10n.setAttributes(contextItem, contextItemL10nId, l10nArgs);
   1899        } else {
   1900          let contextItemL10nId = isStarred
   1901            ? "main-context-menu-edit-bookmark"
   1902            : "main-context-menu-bookmark-page";
   1903          document.l10n.setAttributes(contextItem, contextItemL10nId);
   1904        }
   1905      }
   1906    }
   1907 
   1908    // Update Page Actions.
   1909    if (document.getElementById("page-action-buttons")) {
   1910      // Fetch the label attribute value of the message and
   1911      // apply it on the star title.
   1912      //
   1913      // Note: This should be updated once bug 1608198 is fixed.
   1914      this._latestMenuItemL10nId = menuItemL10nId;
   1915      document.l10n.formatMessages([{ id: menuItemL10nId }]).then(l10n => {
   1916        // It's possible for this promise to be scheduled multiple times.
   1917        // In such a case, we'd like to avoid setting the title if there's
   1918        // a newer l10n id pending to be set.
   1919        if (this._latestMenuItemL10nId != menuItemL10nId) {
   1920          return;
   1921        }
   1922 
   1923        // We assume that menuItemL10nId has a single attribute.
   1924        let label = l10n[0].attributes[0].value;
   1925 
   1926        // Update the label for the page action panel.
   1927        let panelButton = BrowserPageActions.panelButtonNodeForActionID(
   1928          PageActions.ACTION_ID_BOOKMARK
   1929        );
   1930        if (panelButton) {
   1931          panelButton.setAttribute("label", label);
   1932        }
   1933      });
   1934    }
   1935  },
   1936 
   1937  onMainMenuPopupShowing: function BUI_onMainMenuPopupShowing(event) {
   1938    // Don't handle events for submenus.
   1939    if (event.target.id != "bookmarksMenuPopup") {
   1940      return;
   1941    }
   1942 
   1943    document.getElementById("menu_mobileBookmarks").hidden =
   1944      !SHOW_MOBILE_BOOKMARKS;
   1945  },
   1946 
   1947  showSubView(anchor) {
   1948    this._showSubView(null, anchor);
   1949  },
   1950 
   1951  _showSubView(
   1952    event,
   1953    anchor = document.getElementById(this.BOOKMARK_BUTTON_ID)
   1954  ) {
   1955    let view = PanelMultiView.getViewNode(document, "PanelUI-bookmarks");
   1956    view.addEventListener("ViewShowing", this);
   1957    view.addEventListener("ViewHiding", this);
   1958    anchor.setAttribute("closemenu", "none");
   1959    this.updateLabel("panelMenu_viewBookmarksToolbar", !this.toolbar.collapsed);
   1960    PanelUI.showSubView("PanelUI-bookmarks", anchor, event);
   1961  },
   1962 
   1963  onCommand: function BUI_onCommand(aEvent) {
   1964    if (aEvent.target != aEvent.currentTarget) {
   1965      return;
   1966    }
   1967 
   1968    // Handle special case when the button is in the panel.
   1969    if (this.button.getAttribute("cui-areatype") == CustomizableUI.TYPE_PANEL) {
   1970      this._showSubView(aEvent);
   1971      return;
   1972    }
   1973    let widget = CustomizableUI.getWidget(this.BOOKMARK_BUTTON_ID).forWindow(
   1974      window
   1975    );
   1976    if (widget.overflowed) {
   1977      // Close the overflow panel because the Edit Bookmark panel will appear.
   1978      widget.node.removeAttribute("closemenu");
   1979    }
   1980    this.onStarCommand(aEvent);
   1981  },
   1982 
   1983  onStarCommand(aEvent) {
   1984    // Ignore non-left clicks on the star, or if we are updating its state.
   1985    if (
   1986      !this._pendingUpdate &&
   1987      (aEvent.type != "click" || aEvent.button == 0)
   1988    ) {
   1989      PlacesCommandHook.bookmarkPage();
   1990    }
   1991  },
   1992 
   1993  handleEvent: function BUI_handleEvent(aEvent) {
   1994    switch (aEvent.type) {
   1995      case "ViewShowing":
   1996        this.onPanelMenuViewShowing(aEvent);
   1997        break;
   1998      case "ViewHiding":
   1999        this.onPanelMenuViewHiding(aEvent);
   2000        break;
   2001      case "command":
   2002        if (aEvent.target.id == "panelMenu_searchBookmarks") {
   2003          PlacesCommandHook.searchBookmarks();
   2004        } else if (aEvent.target.id == "panelMenu_viewBookmarksToolbar") {
   2005          this.toggleBookmarksToolbar("bookmark-tools");
   2006        }
   2007        break;
   2008    }
   2009  },
   2010 
   2011  onPanelMenuViewShowing: function BUI_onViewShowing(aEvent) {
   2012    let panelview = aEvent.target;
   2013 
   2014    // Get all statically placed buttons to supply them with keyboard shortcuts.
   2015    let staticButtons = panelview.getElementsByTagName("toolbarbutton");
   2016    for (let i = 0, l = staticButtons.length; i < l; ++i) {
   2017      CustomizableUI.addShortcut(staticButtons[i]);
   2018    }
   2019 
   2020    // Setup the Places view.
   2021    // We restrict the amount of results to 42. Not 50, but 42. Why? Because 42.
   2022    let query =
   2023      "place:queryType=" +
   2024      Ci.nsINavHistoryQueryOptions.QUERY_TYPE_BOOKMARKS +
   2025      "&sort=" +
   2026      Ci.nsINavHistoryQueryOptions.SORT_BY_DATEADDED_DESCENDING +
   2027      "&maxResults=42&excludeQueries=1";
   2028 
   2029    this._panelMenuView = new PlacesPanelview(
   2030      query,
   2031      document.getElementById("panelMenu_bookmarksMenu"),
   2032      panelview
   2033    );
   2034    panelview.removeEventListener("ViewShowing", this);
   2035    panelview.addEventListener("command", this);
   2036  },
   2037 
   2038  onPanelMenuViewHiding: function BUI_onViewHiding(aEvent) {
   2039    this._panelMenuView.uninit();
   2040    delete this._panelMenuView;
   2041    let panelview = aEvent.target;
   2042    panelview.removeEventListener("ViewHiding", this);
   2043    panelview.removeEventListener("command", this);
   2044  },
   2045 
   2046  handlePlacesEvents(aEvents) {
   2047    let isStarUpdateNeeded = false;
   2048    let affectsOtherBookmarksFolder = false;
   2049    let affectsBookmarksToolbarFolder = false;
   2050 
   2051    for (let ev of aEvents) {
   2052      switch (ev.type) {
   2053        case "bookmark-added":
   2054          // Only need to update the UI if it wasn't marked as starred before:
   2055          if (this._itemGuids.size == 0) {
   2056            if (ev.url && ev.url == this._uri.spec) {
   2057              // If a new bookmark has been added to the tracked uri, register it.
   2058              if (!this._itemGuids.has(ev.guid)) {
   2059                this._itemGuids.add(ev.guid);
   2060                isStarUpdateNeeded = true;
   2061              }
   2062            }
   2063          }
   2064 
   2065          if (ev.parentGuid === PlacesUtils.bookmarks.toolbarGuid) {
   2066            Glean.browserEngagement.bookmarksToolbarBookmarkAdded.add(1);
   2067          }
   2068          if (ev.parentGuid == PlacesUtils.bookmarks.tagsGuid) {
   2069            StarUI.userHasTags = true;
   2070          }
   2071          break;
   2072        case "bookmark-removed":
   2073          // If one of the tracked bookmarks has been removed, unregister it.
   2074          if (this._itemGuids.has(ev.guid)) {
   2075            this._itemGuids.delete(ev.guid);
   2076            // Only need to update the UI if the page is no longer starred
   2077            if (this._itemGuids.size == 0) {
   2078              isStarUpdateNeeded = true;
   2079            }
   2080          }
   2081 
   2082          // Reset the default location if it is equal to the folder
   2083          // being deleted. Just check the preference directly since we
   2084          // do not want to do a asynchronous db lookup.
   2085          PlacesUIUtils.defaultParentGuid.then(parentGuid => {
   2086            if (
   2087              ev.itemType == PlacesUtils.bookmarks.TYPE_FOLDER &&
   2088              ev.guid == parentGuid
   2089            ) {
   2090              Services.prefs.setCharPref(
   2091                "browser.bookmarks.defaultLocation",
   2092                PlacesUtils.bookmarks.toolbarGuid
   2093              );
   2094            }
   2095          });
   2096          break;
   2097        case "bookmark-moved":
   2098          if (
   2099            ev.parentGuid === PlacesUtils.bookmarks.unfiledGuid ||
   2100            ev.oldParentGuid === PlacesUtils.bookmarks.unfiledGuid
   2101          ) {
   2102            affectsOtherBookmarksFolder = true;
   2103          }
   2104 
   2105          if (
   2106            ev.parentGuid == PlacesUtils.bookmarks.toolbarGuid ||
   2107            ev.oldParentGuid == PlacesUtils.bookmarks.toolbarGuid
   2108          ) {
   2109            affectsBookmarksToolbarFolder = true;
   2110            if (ev.oldParentGuid != PlacesUtils.bookmarks.toolbarGuid) {
   2111              Glean.browserEngagement.bookmarksToolbarBookmarkAdded.add(1);
   2112            }
   2113          }
   2114          break;
   2115        case "bookmark-url-changed":
   2116          // If the changed bookmark was tracked, check if it is now pointing to
   2117          // a different uri and unregister it.
   2118          if (this._itemGuids.has(ev.guid) && ev.url != this._uri.spec) {
   2119            this._itemGuids.delete(ev.guid);
   2120            // Only need to update the UI if the page is no longer starred
   2121            if (this._itemGuids.size == 0) {
   2122              this._updateStar();
   2123            }
   2124          } else if (
   2125            !this._itemGuids.has(ev.guid) &&
   2126            ev.url == this._uri.spec
   2127          ) {
   2128            // If another bookmark is now pointing to the tracked uri, register it.
   2129            this._itemGuids.add(ev.guid);
   2130            // Only need to update the UI if it wasn't marked as starred before:
   2131            if (this._itemGuids.size == 1) {
   2132              this._updateStar();
   2133            }
   2134          }
   2135 
   2136          break;
   2137      }
   2138 
   2139      if (ev.parentGuid == PlacesUtils.bookmarks.unfiledGuid) {
   2140        affectsOtherBookmarksFolder = true;
   2141      } else if (ev.parentGuid == PlacesUtils.bookmarks.toolbarGuid) {
   2142        affectsBookmarksToolbarFolder = true;
   2143      }
   2144    }
   2145 
   2146    if (isStarUpdateNeeded) {
   2147      this._updateStar();
   2148    }
   2149 
   2150    // Run after the notification has been handled by the views.
   2151    Services.tm.dispatchToMainThread(() => {
   2152      if (affectsOtherBookmarksFolder) {
   2153        this.maybeShowOtherBookmarksFolder().catch(console.error);
   2154      }
   2155      if (affectsBookmarksToolbarFolder) {
   2156        this.updateEmptyToolbarMessage().catch(console.error);
   2157      }
   2158    });
   2159  },
   2160 
   2161  onWidgetUnderflow(aNode) {
   2162    let win = aNode.ownerGlobal;
   2163    if (aNode.id != this.BOOKMARK_BUTTON_ID || win != window) {
   2164      return;
   2165    }
   2166 
   2167    // The view gets broken by being removed and reinserted. Uninit
   2168    // here so popupshowing will generate a new one:
   2169    this._uninitView();
   2170  },
   2171 
   2172  async maybeShowOtherBookmarksFolder() {
   2173    // PlacesToolbar._placesView can be undefined if the toolbar isn't initialized,
   2174    // collapsed, or hidden in some other way.
   2175    let toolbar = document.getElementById("PlacesToolbar");
   2176    if (!toolbar?._placesView) {
   2177      return;
   2178    }
   2179 
   2180    let placement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
   2181    let otherBookmarks = document.getElementById("OtherBookmarks");
   2182    if (
   2183      !SHOW_OTHER_BOOKMARKS ||
   2184      placement?.area != CustomizableUI.AREA_BOOKMARKS
   2185    ) {
   2186      if (otherBookmarks) {
   2187        otherBookmarks.hidden = true;
   2188      }
   2189      return;
   2190    }
   2191 
   2192    let instance = (this._showOtherBookmarksInstance = {});
   2193    let unfiledGuid = PlacesUtils.bookmarks.unfiledGuid;
   2194    let numberOfBookmarks = (await PlacesUtils.bookmarks.fetch(unfiledGuid))
   2195      .childCount;
   2196    if (instance != this._showOtherBookmarksInstance) {
   2197      return;
   2198    }
   2199 
   2200    if (numberOfBookmarks > 0) {
   2201      // Build the "Other Bookmarks" button if it doesn't exist.
   2202      if (!otherBookmarks) {
   2203        const node = PlacesUtils.getFolderContents(unfiledGuid).root;
   2204        otherBookmarks = this.buildOtherBookmarksFolder(node);
   2205      }
   2206      otherBookmarks.hidden = false;
   2207    } else if (otherBookmarks) {
   2208      otherBookmarks.hidden = true;
   2209    }
   2210  },
   2211 
   2212  buildShowOtherBookmarksMenuItem() {
   2213    // Building this only if there's bookmarks in unfiled would cause
   2214    // synchronous IO, thus we just add it as disabled and enable it once the
   2215    // information is available.
   2216    let menuItem = document.createXULElement("menuitem");
   2217 
   2218    menuItem.setAttribute("id", "show-other-bookmarks_PersonalToolbar");
   2219    menuItem.setAttribute("toolbarId", "PersonalToolbar");
   2220    menuItem.setAttribute("type", "checkbox");
   2221    menuItem.setAttribute("selection-type", "none|single");
   2222    menuItem.setAttribute("start-disabled", "true");
   2223    menuItem.toggleAttribute("checked", SHOW_OTHER_BOOKMARKS);
   2224 
   2225    MozXULElement.insertFTLIfNeeded("browser/toolbarContextMenu.ftl");
   2226    document.l10n.setAttributes(
   2227      menuItem,
   2228      "toolbar-context-menu-bookmarks-show-other-bookmarks"
   2229    );
   2230    menuItem.addEventListener("command", () => {
   2231      Services.prefs.setBoolPref(
   2232        "browser.toolbars.bookmarks.showOtherBookmarks",
   2233        !SHOW_OTHER_BOOKMARKS
   2234      );
   2235    });
   2236    // Enable the menuItem if there's unfiled bookmarks
   2237    PlacesUtils.bookmarks.fetch(PlacesUtils.bookmarks.unfiledGuid).then(bm => {
   2238      if (bm.childCount) {
   2239        menuItem.disabled = false;
   2240      }
   2241    });
   2242 
   2243    return menuItem;
   2244  },
   2245 
   2246  buildOtherBookmarksFolder(node) {
   2247    let otherBookmarksButton = document.createXULElement("toolbarbutton");
   2248    otherBookmarksButton.setAttribute("type", "menu");
   2249    otherBookmarksButton.setAttribute("container", "true");
   2250    otherBookmarksButton.id = "OtherBookmarks";
   2251    otherBookmarksButton.className = "bookmark-item";
   2252    otherBookmarksButton.hidden = "true";
   2253    otherBookmarksButton.addEventListener("popupshowing", event =>
   2254      document
   2255        .getElementById("PlacesToolbar")
   2256        ._placesView._onOtherBookmarksPopupShowing(event)
   2257    );
   2258 
   2259    MozXULElement.insertFTLIfNeeded("browser/places.ftl");
   2260    document.l10n.setAttributes(otherBookmarksButton, "other-bookmarks-folder");
   2261 
   2262    let otherBookmarksPopup = document.createXULElement("menupopup", {
   2263      is: "places-popup",
   2264    });
   2265    otherBookmarksPopup.setAttribute("placespopup", "true");
   2266    otherBookmarksPopup.setAttribute("context", "placesContext");
   2267    otherBookmarksPopup.classList.add("toolbar-menupopup");
   2268    otherBookmarksPopup.id = "OtherBookmarksPopup";
   2269 
   2270    otherBookmarksPopup._placesNode = PlacesUtils.asContainer(node);
   2271    otherBookmarksButton._placesNode = PlacesUtils.asContainer(node);
   2272 
   2273    otherBookmarksButton.appendChild(otherBookmarksPopup);
   2274 
   2275    let chevronButton = document.getElementById("PlacesChevron");
   2276    chevronButton.parentNode.append(otherBookmarksButton);
   2277 
   2278    let placesToolbar = document.getElementById("PlacesToolbar");
   2279    placesToolbar._placesView._otherBookmarks = otherBookmarksButton;
   2280    placesToolbar._placesView._otherBookmarksPopup = otherBookmarksPopup;
   2281    return otherBookmarksButton;
   2282  },
   2283 };