/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
// This file is loaded into the browser window scope.
/* eslint-env mozilla/browser-window */
XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "NEWTAB_ENABLED",
  "browser.newtabpage.enabled",
  false
);
XPCOMUtils.defineLazyPreferenceGetter(
  this,
  "SHOW_OTHER_BOOKMARKS",
  "browser.toolbars.bookmarks.showOtherBookmarks",
  true,
  (aPref, aPrevVal, aNewVal) => {
    BookmarkingUI.maybeShowOtherBookmarksFolder();
    document
      .getElementById("PlacesToolbar")
      ?._placesView?.updateNodesVisibility();
  }
);
ChromeUtils.defineModuleGetter(
  this,
  "PanelMultiView",
  "resource:///modules/PanelMultiView.jsm"
);
var StarUI = {
  _itemGuids: null,
  _batching: false,
  _isNewBookmark: false,
  _isComposing: false,
  _autoCloseTimer: 0,
  // The autoclose timer is diasbled if the user interacts with the
  // popup, such as making a change through typing or clicking on
  // the popup.
  _autoCloseTimerEnabled: true,
  // The autoclose timeout length. 3500ms matches the timeout that Pocket uses
  // in browser/components/pocket/content/panels/js/saved.js.
  _autoCloseTimeout: 3500,
  _removeBookmarksOnPopupHidden: false,
  _element(aID) {
    return document.getElementById(aID);
  },
  // Edit-bookmark panel
  get panel() {
    delete this.panel;
    this._createPanelIfNeeded();
    var element = this._element("editBookmarkPanel");
    // initially the panel is hidden
    // to avoid impacting startup / new window performance
    element.hidden = false;
    element.addEventListener("keypress", this, { mozSystemGroup: true });
    element.addEventListener("mousedown", this);
    element.addEventListener("mouseout", this);
    element.addEventListener("mousemove", this);
    element.addEventListener("compositionstart", this);
    element.addEventListener("compositionend", this);
    element.addEventListener("input", this);
    element.addEventListener("popuphidden", this);
    element.addEventListener("popupshown", this);
    return (this.panel = element);
  },
  // nsIDOMEventListener
  handleEvent(aEvent) {
    switch (aEvent.type) {
      case "mousemove":
        clearTimeout(this._autoCloseTimer);
        // The autoclose timer is not disabled on generic mouseout
        // because the user may not have actually interacted with the popup.
        break;
      case "popuphidden": {
        clearTimeout(this._autoCloseTimer);
        if (aEvent.originalTarget == this.panel) {
          if (gEditItemOverlay.delayedApplyEnabled) {
            this._handlePopupHiddenEvent().catch(console.error);
          } else {
            this._handlePopupHiddenEventInstantApply().catch(console.error);
          }
        }
        break;
      }
      case "keypress":
        clearTimeout(this._autoCloseTimer);
        this._autoCloseTimerEnabled = false;
        if (aEvent.defaultPrevented) {
          // The event has already been consumed inside of the panel.
          break;
        }
        switch (aEvent.keyCode) {
          case KeyEvent.DOM_VK_ESCAPE:
            if (this._isNewBookmark) {
              this._removeBookmarksOnPopupHidden = true;
            }
            this.panel.hidePopup();
            break;
          case KeyEvent.DOM_VK_RETURN:
            if (
              aEvent.target.classList.contains("expander-up") ||
              aEvent.target.classList.contains("expander-down") ||
              aEvent.target.id == "editBMPanel_newFolderButton" ||
              aEvent.target.id == "editBookmarkPanelRemoveButton"
            ) {
              // XXX Why is this necessary? The defaultPrevented check should
              //    be enough.
              break;
            }
            this.panel.hidePopup();
            break;
          // This case is for catching character-generating keypresses
          case 0:
            let accessKey = document.getElementById("key_close");
            if (eventMatchesKey(aEvent, accessKey)) {
              this.panel.hidePopup();
            }
            break;
        }
        break;
      case "compositionend":
        // After composition is committed, "mouseout" or something can set
        // auto close timer.
        this._isComposing = false;
        break;
      case "compositionstart":
        if (aEvent.defaultPrevented) {
          // If the composition was canceled, nothing to do here.
          break;
        }
        this._isComposing = true;
      // Explicit fall-through, during composition, panel shouldn't be hidden automatically.
      case "input":
      // Might have edited some text without keyboard events nor composition
      // events. Fall-through to cancel auto close in such case.
      case "mousedown":
        clearTimeout(this._autoCloseTimer);
        this._autoCloseTimerEnabled = false;
        break;
      case "mouseout":
        if (!this._autoCloseTimerEnabled) {
          // Don't autoclose the popup if the user has made a selection
          // or keypress and then subsequently mouseout.
          break;
        }
      // Explicit fall-through
      case "popupshown":
        // Don't handle events for descendent elements.
        if (aEvent.target != aEvent.currentTarget) {
          break;
        }
        // auto-close if new and not interacted with
        if (this._isNewBookmark && !this._isComposing) {
          let delay = this._autoCloseTimeout;
          if (this._closePanelQuickForTesting) {
            delay /= 10;
          }
          clearTimeout(this._autoCloseTimer);
          this._autoCloseTimer = setTimeout(() => {
            if (!this.panel.matches(":hover")) {
              this.panel.hidePopup(true);
            }
          }, delay);
          this._autoCloseTimerEnabled = true;
        }
        break;
    }
  },
  /**
   * Handle popup hidden event in delayed apply mode.
   */
  async _handlePopupHiddenEvent() {
    const {
      bookmarkState,
      didChangeFolder,
      selectedFolderGuid,
    } = gEditItemOverlay;
    gEditItemOverlay.uninitPanel(true);
    // Capture _removeBookmarksOnPopupHidden and _itemGuids values. Reset them
    // before we handle the next popup.
    const removeBookmarksOnPopupHidden = this._removeBookmarksOnPopupHidden;
    this._removeBookmarksOnPopupHidden = false;
    const guidsForRemoval = this._itemGuids;
    this._itemGuids = null;
    if (removeBookmarksOnPopupHidden && guidsForRemoval) {
      if (!this._isNewBookmark) {
        // Remove all bookmarks for the bookmark's url, this also removes
        // the tags for the url.
        await PlacesTransactions.Remove(guidsForRemoval).transact();
      } else {
        BookmarkingUI.star.removeAttribute("starred");
      }
      return;
    }
    await this._storeRecentlyUsedFolder(selectedFolderGuid, didChangeFolder);
    await bookmarkState.save();
    if (this._isNewBookmark) {
      this.showConfirmation();
    }
  },
  /**
   * Handle popup hidden event in instant apply mode.
   */
  async _handlePopupHiddenEventInstantApply() {
    const { selectedFolderGuid, didChangeFolder } = gEditItemOverlay;
    gEditItemOverlay.uninitPanel(true);
    // Capture _removeBookmarksOnPopupHidden and _itemGuids values. Reset them
    // before we handle the next popup.
    const removeBookmarksOnPopupHidden = this._removeBookmarksOnPopupHidden;
    this._removeBookmarksOnPopupHidden = false;
    const guidsForRemoval = this._itemGuids;
    this._itemGuids = null;
    if (this._batching) {
      this.endBatch();
    }
    if (removeBookmarksOnPopupHidden && guidsForRemoval) {
      if (this._isNewBookmark) {
        await PlacesTransactions.undo();
        return;
      }
      // Remove all bookmarks for the bookmark's url, this also removes
      // the tags for the url.
      await PlacesTransactions.Remove(guidsForRemoval).transact();
    } else if (this._isNewBookmark) {
      this.showConfirmation();
    }
    if (!removeBookmarksOnPopupHidden) {
      await this._storeRecentlyUsedFolder(selectedFolderGuid, didChangeFolder);
    }
  },
  async showEditBookmarkPopup(aNode, aIsNewBookmark, aUrl) {
    // Slow double-clicks (not true double-clicks) shouldn't
    // cause the panel to flicker.
    if (this.panel.state != "closed") {
      return;
    }
    this._isNewBookmark = aIsNewBookmark;
    this._itemGuids = null;
    let titleL10nID = this._isNewBookmark
      ? "bookmarks-add-bookmark"
      : "bookmarks-edit-bookmark";
    document.l10n.setAttributes(
      this._element("editBookmarkPanelTitle"),
      titleL10nID
    );
    this._element(
      "editBookmarkPanel_showForNewBookmarks"
    ).checked = this.showForNewBookmarks;
    this._itemGuids = [];
    await PlacesUtils.bookmarks.fetch({ url: aUrl }, bookmark =>
      this._itemGuids.push(bookmark.guid)
    );
    let removeButton = this._element("editBookmarkPanelRemoveButton");
    if (this._isNewBookmark) {
      document.l10n.setAttributes(removeButton, "bookmark-panel-cancel");
    } else {
      // The label of the remove button differs if the URI is bookmarked
      // multiple times.
      document.l10n.setAttributes(removeButton, "bookmark-panel-remove", {
        count: this._itemGuids.length,
      });
    }
    this._setIconAndPreviewImage();
    if (!gEditItemOverlay.delayedApplyEnabled) {
      this.beginBatch();
    }
    let onPanelReady = fn => {
      let target = this.panel;
      if (target.parentNode) {
        // By targeting the panel's parent and using a capturing listener, we
        // can have our listener called before others waiting for the panel to
        // be shown (which probably expect the panel to be fully initialized)
        target = target.parentNode;
      }
      target.addEventListener(
        "popupshown",
        function(event) {
          fn();
        },
        { capture: true, once: true }
      );
    };
    await gEditItemOverlay.initPanel({
      node: aNode,
      onPanelReady,
      hiddenRows: ["location", "keyword"],
      focusedElement: "preferred",
      isNewBookmark: this._isNewBookmark,
    });
    this.panel.openPopup(BookmarkingUI.anchor, "bottomright topright");
  },
  _createPanelIfNeeded() {
    // Lazy load the editBookmarkPanel the first time we need to display it.
    if (!this._element("editBookmarkPanel")) {
      MozXULElement.insertFTLIfNeeded("browser/editBookmarkOverlay.ftl");
      let template = this._element("editBookmarkPanelTemplate");
      let clone = template.content.cloneNode(true);
      template.replaceWith(clone);
    }
  },
  _setIconAndPreviewImage() {
    let faviconImage = this._element("editBookmarkPanelFavicon");
    faviconImage.removeAttribute("iconloadingprincipal");
    faviconImage.removeAttribute("src");
    let tab = gBrowser.selectedTab;
    if (tab.hasAttribute("image") && !tab.hasAttribute("busy")) {
      faviconImage.setAttribute(
        "iconloadingprincipal",
        tab.getAttribute("iconloadingprincipal")
      );
      faviconImage.setAttribute("src", tab.getAttribute("image"));
    }
    let canvas = PageThumbs.createCanvas(window);
    PageThumbs.captureToCanvas(gBrowser.selectedBrowser, canvas).catch(e =>
      console.error(e)
    );
    document.mozSetImageElement("editBookmarkPanelImageCanvas", canvas);
  },
  removeBookmarkButtonCommand: function SU_removeBookmarkButtonCommand() {
    this._removeBookmarksOnPopupHidden = true;
    this.panel.hidePopup();
  },
  // Matching the way it is used in the Library, editBookmarkOverlay implements
  // an instant-apply UI, having no batched-Undo/Redo support.
  // However, in this context (the Star UI) we have a Cancel button whose
  // expected behavior is to undo all the operations done in the panel.
  // Sometime in the future this needs to be reimplemented using a
  // non-instant apply code path, but for the time being, we patch-around
  // editBookmarkOverlay so that all of the actions done in the panel
  // are treated by PlacesTransactions as a single batch.  To do so,
  // we start a PlacesTransactions batch when the star UI panel is shown, and
  // we keep the batch ongoing until the panel is hidden.
  _batchBlockingDeferred: null,
  beginBatch() {
    if (this._batching) {
      return;
    }
    this._batchBlockingDeferred = PromiseUtils.defer();
    PlacesTransactions.batch(async () => {
      // First await for the batch to be concluded.
      await this._batchBlockingDeferred.promise;
      // And then for any pending promises added in the meanwhile.
      await Promise.all(gEditItemOverlay.transactionPromises);
    });
    this._batching = true;
  },
  endBatch() {
    if (!this._batching) {
      return;
    }
    this._batchBlockingDeferred.resolve();
    this._batching = false;
  },
  async _storeRecentlyUsedFolder(selectedFolderGuid, didChangeFolder) {
    if (!selectedFolderGuid) {
      return;
    }
    // If we're changing where a bookmark gets saved, persist that location.
    if (didChangeFolder) {
      Services.prefs.setCharPref(
        "browser.bookmarks.defaultLocation",
        selectedFolderGuid
      );
    }
    // Don't store folders that are always displayed in "Recent Folders".
    if (PlacesUtils.bookmarks.userContentRoots.includes(selectedFolderGuid)) {
      return;
    }
    // List of recently used folders:
    let lastUsedFolderGuids = await PlacesUtils.metadata.get(
      PlacesUIUtils.LAST_USED_FOLDERS_META_KEY,
      []
    );
    let index = lastUsedFolderGuids.indexOf(selectedFolderGuid);
    if (index > 1) {
      // The guid is in the array but not the most recent.
      lastUsedFolderGuids.splice(index, 1);
      lastUsedFolderGuids.unshift(selectedFolderGuid);
    } else if (index == -1) {
      lastUsedFolderGuids.unshift(selectedFolderGuid);
    }
    while (lastUsedFolderGuids.length > PlacesUIUtils.maxRecentFolders) {
      lastUsedFolderGuids.pop();
    }
    await PlacesUtils.metadata.set(
      PlacesUIUtils.LAST_USED_FOLDERS_META_KEY,
      lastUsedFolderGuids
    );
  },
  onShowForNewBookmarksCheckboxCommand() {
    Services.prefs.setBoolPref(
      "browser.bookmarks.editDialog.showForNewBookmarks",
      this._element("editBookmarkPanel_showForNewBookmarks").checked
    );
  },
  showConfirmation() {
    // Show the "Saved to bookmarks" hint for the first three times
    const HINT_COUNT_PREF =
      "browser.bookmarks.editDialog.confirmationHintShowCount";
    const HINT_COUNT = Services.prefs.getIntPref(HINT_COUNT_PREF, 0);
    if (HINT_COUNT >= 3) {
      return;
    }
    Services.prefs.setIntPref(HINT_COUNT_PREF, HINT_COUNT + 1);
    let anchor;
    if (window.toolbar.visible) {
      for (let id of ["library-button", "bookmarks-menu-button"]) {
        let element = document.getElementById(id);
        if (
          element &&
          element.getAttribute("cui-areatype") != "panel" &&
          element.getAttribute("overflowedItem") != "true"
        ) {
          anchor = element;
          break;
        }
      }
    }
    if (!anchor) {
      anchor = document.getElementById("PanelUI-menu-button");
    }
    ConfirmationHint.show(anchor, "confirmation-hint-page-bookmarked");
  },
};
XPCOMUtils.defineLazyPreferenceGetter(
  StarUI,
  "showForNewBookmarks",
  "browser.bookmarks.editDialog.showForNewBookmarks"
);
var PlacesCommandHook = {
  /**
   * Adds a bookmark to the page loaded in the current browser.
   */
  async bookmarkPage() {
    let browser = gBrowser.selectedBrowser;
    let url = URL.fromURI(browser.currentURI);
    let info = await PlacesUtils.bookmarks.fetch({ url });
    let isNewBookmark = !info;
    let showEditUI = !isNewBookmark || StarUI.showForNewBookmarks;
    if (isNewBookmark) {
      // This is async because we have to validate the guid
      // coming from prefs.
      let parentGuid = await PlacesUIUtils.defaultParentGuid;
      info = { url, parentGuid };
      // Bug 1148838 - Make this code work for full page plugins.
      let charset = null;
      let isErrorPage = false;
      if (browser.documentURI) {
        isErrorPage = /^about:(neterror|certerror|blocked)/.test(
          browser.documentURI.spec
        );
      }
      try {
        if (isErrorPage) {
          let entry = await PlacesUtils.history.fetch(browser.currentURI);
          if (entry) {
            info.title = entry.title;
          }
        } else {
          info.title = browser.contentTitle;
        }
        info.title = info.title || url.href;
        charset = browser.characterSet;
      } catch (e) {
        console.error(e);
      }
      if (showEditUI && !gEditItemOverlay.delayedApplyEnabled) {
        // If we bookmark the page here but open right into a cancelable
        // state (i.e. new bookmark in Library), start batching here so
        // all of the actions can be undone in a single undo step.
        StarUI.beginBatch();
      }
      if (
        !gEditItemOverlay.delayedApplyEnabled ||
        !StarUI.showForNewBookmarks
      ) {
        info.guid = await PlacesTransactions.NewBookmark(info).transact();
      } else {
        info.guid = PlacesUtils.bookmarks.unsavedGuid;
        BookmarkingUI.star.setAttribute("starred", "true");
      }
      if (charset) {
        PlacesUIUtils.setCharsetForPage(url, charset, window).catch(
          console.error
        );
      }
    }
    // Revert the contents of the location bar
    gURLBar.handleRevert();
    // If it was not requested to open directly in "edit" mode, we are done.
    if (!showEditUI) {
      StarUI.showConfirmation();
      return;
    }
    let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(info);
    await StarUI.showEditBookmarkPopup(node, isNewBookmark, url);
  },
  /**
   * Adds a bookmark to the page targeted by a link.
   * @param url (string)
   *        the address of the link target
   * @param title
   *        The link text
   */
  async bookmarkLink(url, title) {
    let bm = await PlacesUtils.bookmarks.fetch({ url });
    if (bm) {
      let node = await PlacesUIUtils.promiseNodeLikeFromFetchInfo(bm);
      await PlacesUIUtils.showBookmarkDialog(
        { action: "edit", node },
        window.top
      );
      return;
    }
    let parentGuid = await PlacesUIUtils.defaultParentGuid;
    let defaultInsertionPoint = new PlacesInsertionPoint({
      parentGuid,
    });
    await PlacesUIUtils.showBookmarkDialog(
      {
        action: "add",
        type: "bookmark",
        uri: Services.io.newURI(url),
        title,
        defaultInsertionPoint,
        hiddenRows: ["location", "keyword"],
      },
      window.top
    );
  },
  /**
   * List of nsIURI objects characterizing tabs given in param.
   * Duplicates are discarded.
   */
  getUniquePages(tabs) {
    let uniquePages = {};
    let URIs = [];
    tabs.forEach(tab => {
      let browser = tab.linkedBrowser;
      let uri = browser.currentURI;
      let title = browser.contentTitle || tab.label;
      let spec = uri.spec;
      if (!(spec in uniquePages)) {
        uniquePages[spec] = null;
        URIs.push({ uri, title });
      }
    });
    return URIs;
  },
  /**
   * List of nsIURI objects characterizing the tabs currently open in the
   * browser, modulo pinned tabs. The URIs will be in the order in which their
   * corresponding tabs appeared and duplicates are discarded.
   */
  get uniqueCurrentPages() {
    let visibleUnpinnedTabs = gBrowser.visibleTabs.filter(tab => !tab.pinned);
    return this.getUniquePages(visibleUnpinnedTabs);
  },
  /**
   * List of nsIURI objects characterizing the tabs currently
   * selected in the window. Duplicates are discarded.
   */
  get uniqueSelectedPages() {
    return this.getUniquePages(gBrowser.selectedTabs);
  },
  /**
   * Opens the Places Organizer.
   * @param {String} item The item to select in the organizer window,
   *                      options are (case sensitive):
   *                      BookmarksMenu, BookmarksToolbar, UnfiledBookmarks,
   *                      AllBookmarks, History, Downloads.
   */
  showPlacesOrganizer(item) {
    var organizer = Services.wm.getMostRecentWindow("Places:Organizer");
    // Due to bug 528706, getMostRecentWindow can return closed windows.
    if (!organizer || organizer.closed) {
      // No currently open places window, so open one with the specified mode.
      openDialog(
        "chrome://browser/content/places/places.xhtml",
        "",
        "chrome,toolbar=yes,dialog=no,resizable",
        item
      );
    } else {
      organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(item);
      organizer.focus();
    }
  },
  searchBookmarks() {
    gURLBar.search(UrlbarTokenizer.RESTRICT.BOOKMARK, {
      searchModeEntry: "bookmarkmenu",
    });
  },
};
ChromeUtils.defineESModuleGetters(this, {
  RecentlyClosedTabsAndWindowsMenuUtils:
    "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.sys.mjs",
});
// View for the history menu.
class HistoryMenu extends PlacesMenu {
  constructor(aPopupShowingEvent) {
    super(aPopupShowingEvent, "place:sort=4&maxResults=15");
  }
  // Called by the base class (PlacesViewBase) so we can initialize some
  // element references before the several superclass constructors call our
  // methods which depend on these.
  _init() {
    super._init();
    let elements = {
      undoTabMenu: "historyUndoMenu",
      hiddenTabsMenu: "hiddenTabsMenu",
      undoWindowMenu: "historyUndoWindowMenu",
      syncTabsMenuitem: "sync-tabs-menuitem",
    };
    for (let [key, elemId] of Object.entries(elements)) {
      this[key] = document.getElementById(elemId);
    }
  }
  _getClosedTabCount() {
    try {
      return SessionStore.getClosedTabCount(window);
    } catch (ex) {
      // SessionStore doesn't track the hidden window, so just return zero then.
      return 0;
    }
  }
  toggleHiddenTabs() {
    const isShown =
      window.gBrowser && gBrowser.visibleTabs.length < gBrowser.tabs.length;
    this.hiddenTabsMenu.hidden = !isShown;
  }
  toggleRecentlyClosedTabs() {
    // enable/disable the Recently Closed Tabs sub menu
    // no restorable tabs, so disable menu
    if (this._getClosedTabCount() == 0) {
      this.undoTabMenu.setAttribute("disabled", true);
    } else {
      this.undoTabMenu.removeAttribute("disabled");
    }
  }
  /**
   * Populate when the history menu is opened
   */
  populateUndoSubmenu() {
    var undoPopup = this.undoTabMenu.menupopup;
    // remove existing menu items
    while (undoPopup.hasChildNodes()) {
      undoPopup.firstChild.remove();
    }
    // no restorable tabs, so make sure menu is disabled, and return
    if (this._getClosedTabCount() == 0) {
      this.undoTabMenu.setAttribute("disabled", true);
      return;
    }
    // enable menu
    this.undoTabMenu.removeAttribute("disabled");
    // populate menu
    let tabsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getTabsFragment(
      window,
      "menuitem",
      /* aPrefixRestoreAll = */ false,
      "menu-history-reopen-all-tabs"
    );
    undoPopup.appendChild(tabsFragment);
  }
  toggleRecentlyClosedWindows() {
    // enable/disable the Recently Closed Windows sub menu
    // no restorable windows, so disable menu
    if (SessionStore.getClosedWindowCount() == 0) {
      this.undoWindowMenu.setAttribute("disabled", true);
    } else {
      this.undoWindowMenu.removeAttribute("disabled");
    }
  }
  /**
   * Populate when the history menu is opened
   */
  populateUndoWindowSubmenu() {
    let undoPopup = this.undoWindowMenu.menupopup;
    // remove existing menu items
    while (undoPopup.hasChildNodes()) {
      undoPopup.firstChild.remove();
    }
    // no restorable windows, so make sure menu is disabled, and return
    if (SessionStore.getClosedWindowCount() == 0) {
      this.undoWindowMenu.setAttribute("disabled", true);
      return;
    }
    // enable menu
    this.undoWindowMenu.removeAttribute("disabled");
    // populate menu
    let windowsFragment = RecentlyClosedTabsAndWindowsMenuUtils.getWindowsFragment(
      window,
      "menuitem",
      /* aPrefixRestoreAll = */ false,
      "menu-history-reopen-all-windows"
    );
    undoPopup.appendChild(windowsFragment);
  }
  toggleTabsFromOtherComputers() {
    // Enable/disable the Tabs From Other Computers menu. Some of the menus handled
    // by HistoryMenu do not have this menuitem.
    if (!this.syncTabsMenuitem) {
      return;
    }
    if (!PlacesUIUtils.shouldShowTabsFromOtherComputersMenuitem()) {
      this.syncTabsMenuitem.hidden = true;
      return;
    }
    this.syncTabsMenuitem.hidden = false;
  }
  _onPopupShowing(aEvent) {
    super._onPopupShowing(aEvent);
    // Don't handle events for submenus.
    if (aEvent.target != aEvent.currentTarget) {
      return;
    }
    this.toggleHiddenTabs();
    this.toggleRecentlyClosedTabs();
    this.toggleRecentlyClosedWindows();
    this.toggleTabsFromOtherComputers();
  }
  _onCommand(aEvent) {
    aEvent = getRootEvent(aEvent);
    let placesNode = aEvent.target._placesNode;
    if (placesNode) {
      if (!PrivateBrowsingUtils.isWindowPrivate(window)) {
        PlacesUIUtils.markPageAsTyped(placesNode.uri);
      }
      openUILink(placesNode.uri, aEvent, {
        ignoreAlt: true,
        triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
      });
    }
  }
}
/**
 * Functions for handling events in the Bookmarks Toolbar and menu.
 */
var BookmarksEventHandler = {
  /**
   * Handler for click event for an item in the bookmarks toolbar or menu.
   * Menus and submenus from the folder buttons bubble up to this handler.
   * Left-click is handled in the onCommand function.
   * When items are middle-clicked (or clicked with modifier), open in tabs.
   * If the click came through a menu, close the menu.
   * @param aEvent
   *        DOMEvent for the click
   * @param aView
   *        The places view which aEvent should be associated with.
   */
  onMouseUp(aEvent) {
    // Handles middle-click or left-click with modifier if not browser.bookmarks.openInTabClosesMenu.
    if (aEvent.button == 2 || PlacesUIUtils.openInTabClosesMenu) {
      return;
    }
    let target = aEvent.originalTarget;
    if (target.tagName != "menuitem") {
      return;
    }
    let modifKey =
      AppConstants.platform === "macosx" ? aEvent.metaKey : aEvent.ctrlKey;
    if (modifKey || aEvent.button == 1) {
      target.setAttribute("closemenu", "none");
      var menupopup = target.parentNode;
      menupopup.addEventListener(
        "popuphidden",
        () => {
          target.removeAttribute("closemenu");
        },
        { once: true }
      );
    } else {
      // Handles edge case where same menuitem was opened previously
      // while menu was kept open, but now menu should close.
      target.removeAttribute("closemenu");
    }
  },
  onClick: function BEH_onClick(aEvent, aView) {
    // Only handle middle-click or left-click with modifiers.
    let modifKey;
    if (AppConstants.platform == "macosx") {
      modifKey = aEvent.metaKey || aEvent.shiftKey;
    } else {
      modifKey = aEvent.ctrlKey || aEvent.shiftKey;
    }
    if (aEvent.button == 2 || (aEvent.button == 0 && !modifKey)) {
      return;
    }
    var target = aEvent.originalTarget;
    // If this event bubbled up from a menu or menuitem,
    // close the menus if browser.bookmarks.openInTabClosesMenu.
    var tag = target.tagName;
    if (
      PlacesUIUtils.openInTabClosesMenu &&
      (tag == "menuitem" || tag == "menu")
    ) {
      closeMenus(aEvent.target);
    }
    if (target._placesNode && PlacesUtils.nodeIsContainer(target._placesNode)) {
      // Don't open the root folder in tabs when the empty area on the toolbar
      // is middle-clicked or when a non-bookmark item (except for Open in Tabs)
      // in a bookmarks menupopup is middle-clicked.
      if (target.localName == "menu" || target.localName == "toolbarbutton") {
        PlacesUIUtils.openMultipleLinksInTabs(
          target._placesNode,
          aEvent,
          aView
        );
      }
    } else if (aEvent.button == 1 && !(tag == "menuitem" || tag == "menu")) {
      // Call onCommand in the cases where it's not called automatically:
      // Middle-clicks outside of menus.
      this.onCommand(aEvent);
    }
  },
  /**
   * Handler for command event for an item in the bookmarks toolbar.
   * Menus and submenus from the folder buttons bubble up to this handler.
   * Opens the item.
   * @param aEvent
   *        DOMEvent for the command
   */
  onCommand: function BEH_onCommand(aEvent) {
    var target = aEvent.originalTarget;
    if (target._placesNode) {
      PlacesUIUtils.openNodeWithEvent(target._placesNode, aEvent);
      // Only record interactions through the Bookmarks Toolbar
      if (target.closest("#PersonalToolbar")) {
        Services.telemetry.scalarAdd(
          "browser.engagement.bookmarks_toolbar_bookmark_opened",
          1
        );
      }
    }
  },
  fillInBHTooltip: function BEH_fillInBHTooltip(aTooltip, aEvent) {
    var node;
    var cropped = false;
    var targetURI;
    if (aTooltip.triggerNode.localName == "treechildren") {
      var tree = aTooltip.triggerNode.parentNode;
      var cell = tree.getCellAt(aEvent.clientX, aEvent.clientY);
      if (cell.row == -1) {
        return false;
      }
      node = tree.view.nodeForTreeIndex(cell.row);
      cropped = tree.isCellCropped(cell.row, cell.col);
    } else {
      // Check whether the tooltipNode is a Places node.
      // In such a case use it, otherwise check for targetURI attribute.
      var tooltipNode = aTooltip.triggerNode;
      if (tooltipNode._placesNode) {
        node = tooltipNode._placesNode;
      } else {
        // This is a static non-Places node.
        targetURI = tooltipNode.getAttribute("targetURI");
      }
    }
    if (!node && !targetURI) {
      return false;
    }
    // Show node.label as tooltip's title for non-Places nodes.
    var title = node ? node.title : tooltipNode.label;
    // Show URL only for Places URI-nodes or nodes with a targetURI attribute.
    var url;
    if (targetURI || PlacesUtils.nodeIsURI(node)) {
      url = targetURI || node.uri;
    }
    // Show tooltip for containers only if their title is cropped.
    if (!cropped && !url) {
      return false;
    }
    let tooltipTitle = aEvent.target.querySelector(".places-tooltip-title");
    tooltipTitle.hidden = !title || title == url;
    if (!tooltipTitle.hidden) {
      tooltipTitle.textContent = title;
    }
    let tooltipUrl = aEvent.target.querySelector(".places-tooltip-uri");
    tooltipUrl.hidden = !url;
    if (!tooltipUrl.hidden) {
      // Use `value` instead of `textContent` so cropping will apply
      tooltipUrl.value = url;
    }
    // Show tooltip.
    return true;
  },
};
// Handles special drag and drop functionality for Places menus that are not
// part of a Places view (e.g. the bookmarks menu in the menubar).
var PlacesMenuDNDHandler = {
  _springLoadDelayMs: 350,
  _closeDelayMs: 500,
  _loadTimer: null,
  _closeTimer: null,
  _closingTimerNode: null,
  /**
   * Called when the user enters the