/* 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.defineLazyScriptGetter(this, ["PlacesToolbar", "PlacesMenu",
                                         "PlacesPanelview", "PlacesPanelMenuView"],
                                  "chrome://browser/content/places/browserPlacesViews.js");
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/extensions/pocket/content/panels/js/saved.js.
  _autoCloseTimeout: 3500,
  _removeBookmarksOnPopupHidden: false,
  _element(aID) {
    return document.getElementById(aID);
  },
  get showForNewBookmarks() {
    return Services.prefs.getBoolPref("browser.bookmarks.editDialog.showForNewBookmarks");
  },
  // Edit-bookmark panel
  get panel() {
    delete this.panel;
    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) {
          let selectedFolderGuid = gEditItemOverlay.selectedFolderGuid;
          gEditItemOverlay.uninitPanel(true);
          this._anchorElement.removeAttribute("open");
          this._anchorElement = null;
          let removeBookmarksOnPopupHidden = this._removeBookmarksOnPopupHidden;
          this._removeBookmarksOnPopupHidden = false;
          let guidsForRemoval = this._itemGuids;
          this._itemGuids = null;
          if (this._batching) {
            this.endBatch();
          }
          if (removeBookmarksOnPopupHidden && guidsForRemoval) {
            if (this._isNewBookmark) {
              PlacesTransactions.undo().catch(Cu.reportError);
              break;
            }
            // Remove all bookmarks for the bookmark's url, this also removes
            // the tags for the url.
            PlacesTransactions.Remove(guidsForRemoval)
                              .transact().catch(Cu.reportError);
          } else if (this._isNewBookmark) {
            this.showConfirmation();
          }
          if (!removeBookmarksOnPopupHidden) {
            this._storeRecentlyUsedFolder(selectedFolderGuid).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;
    }
  },
  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;
    this._element("editBookmarkPanelTitle").value =
      this._isNewBookmark ?
        gNavigatorBundle.getString("editBookmarkPanel.newBookmarkTitle") :
        gNavigatorBundle.getString("editBookmarkPanel.editBookmarkTitle");
    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) {
      removeButton.label = gNavigatorBundle.getString("editBookmarkPanel.cancel.label");
      removeButton.setAttribute("accesskey",
        gNavigatorBundle.getString("editBookmarkPanel.cancel.accesskey"));
    } else {
      // The label of the remove button differs if the URI is bookmarked
      // multiple times.
      let bookmarksCount = this._itemGuids.length;
      let forms = gNavigatorBundle.getString("editBookmark.removeBookmarks.label");
      let label = PluralForm.get(bookmarksCount, forms)
                            .replace("#1", bookmarksCount);
      removeButton.label = label;
      removeButton.setAttribute("accesskey",
        gNavigatorBundle.getString("editBookmark.removeBookmarks.accesskey"));
    }
    this._setIconAndPreviewImage();
    this.beginBatch();
    this._anchorElement = BookmarkingUI.anchor;
    this._anchorElement.setAttribute("open", "true");
    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});
    };
    gEditItemOverlay.initPanel({ node: aNode,
                                 onPanelReady,
                                 hiddenRows: ["location", "keyword"],
                                 focusedElement: "preferred"});
    this.panel.openPopup(this._anchorElement, "bottomcenter topright");
  },
  _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);
    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 () => {
      await this._batchBlockingDeferred.promise;
    });
    this._batching = true;
  },
  endBatch() {
    if (!this._batching)
      return;
    this._batchBlockingDeferred.resolve();
    this._batchBlockingDeferred = null;
    this._batching = false;
  },
  async _storeRecentlyUsedFolder(selectedFolderGuid) {
    // These are displayed by default, so don't save the folder for them.
    if (!selectedFolderGuid ||
        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);
    }
    if (lastUsedFolderGuids.length > 5) {
      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() {
    LibraryUI.triggerLibraryAnimation("bookmark");
    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") != "menu-panel" &&
            element.getAttribute("overflowedItem") != "true") {
          anchor = element;
          break;
        }
      }
    }
    if (!anchor) {
      anchor = document.getElementById("PanelUI-menu-button");
    }
    ConfirmationHint.show(anchor, "pageBookmarked");
  }
};
var PlacesCommandHook = {
  /**
   * Adds a bookmark to the page loaded in the current browser.
   */
  async bookmarkPage() {
    let browser = gBrowser.selectedBrowser;
    let url = new URL(browser.currentURI.spec);
    let info = await PlacesUtils.bookmarks.fetch({ url });
    let isNewBookmark = !info;
    let showEditUI = !isNewBookmark || StarUI.showForNewBookmarks;
    if (isNewBookmark) {
      let parentGuid = PlacesUtils.bookmarks.unfiledGuid;
      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) {
        Cu.reportError(e);
      }
      if (showEditUI) {
        // 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();
      }
      info.guid = await PlacesTransactions.NewBookmark(info).transact();
      if (charset) {
        PlacesUIUtils.setCharsetForPage(url, charset, window).catch(Cu.reportError);
      }
    }
    // 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);
      PlacesUIUtils.showBookmarkDialog({ action: "edit", node }, window.top);
      return;
    }
    let defaultInsertionPoint = new PlacesInsertionPoint({
      parentId: PlacesUtils.bookmarksMenuFolderId,
      parentGuid: PlacesUtils.bookmarks.menuGuid
    });
    PlacesUIUtils.showBookmarkDialog({ action: "add",
                                       type: "bookmark",
                                       uri: makeURI(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);
  },
  /**
   * Adds a folder with bookmarks to URIList given in param.
   */
  bookmarkPages(URIList) {
    if (URIList.length > 1) {
      PlacesUIUtils.showBookmarkDialog({ action: "add",
                                         type: "folder",
                                         URIList,
                                       }, window);
    }
  },
  /**
   * Updates disabled state for the "Bookmark All Tabs" command.
   */
  updateBookmarkAllTabsCommand:
  function PCH_updateBookmarkAllTabsCommand() {
    // There's nothing to do in non-browser windows.
    if (window.location.href != AppConstants.BROWSER_CHROME_URL)
      return;
    // Disable "Bookmark All Tabs" if there are less than two
    // "unique current pages".
    goSetCommandEnabled("Browser:BookmarkAllTabs",
                        this.uniqueCurrentPages.length >= 2);
  },
  /**
   * Adds a Live Bookmark to a feed associated with the current page.
   * @param     url
   *            The nsIURI of the page the feed was attached to
   * @title     title
   *            The title of the feed. Optional.
   */
  async addLiveBookmark(url, feedTitle) {
    let toolbarIP = new PlacesInsertionPoint({
      parentId: PlacesUtils.toolbarFolderId,
      parentGuid: PlacesUtils.bookmarks.toolbarGuid
    });
    let feedURI = makeURI(url);
    let title = feedTitle || gBrowser.contentTitle;
    PlacesUIUtils.showBookmarkDialog({ action: "add",
                                       type: "livemark",
                                       feedURI,
                                       siteURI: gBrowser.currentURI,
                                       title,
                                       defaultInsertionPoint: toolbarIP,
                                       hiddenRows: [ "feedLocation",
                                                     "siteLocation" ]
                                     }, window);
  },
  /**
   * 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.xul",
                 "", "chrome,toolbar=yes,dialog=no,resizable", item);
    } else {
      organizer.PlacesOrganizer.selectLeftPaneContainerByHierarchy(item);
      organizer.focus();
    }
  },
  searchBookmarks() {
    focusAndSelectUrlBar();
    for (let char of ["*", " "]) {
      let code = char.charCodeAt(0);
      gURLBar.inputField.dispatchEvent(new KeyboardEvent("keypress", {
        keyCode: code,
        charCode: code,
        bubbles: true
      }));
    }
  }
};
ChromeUtils.defineModuleGetter(this, "RecentlyClosedTabsAndWindowsMenuUtils",
  "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm");
// View for the history menu.
function HistoryMenu(aPopupShowingEvent) {
  // Workaround for Bug 610187.  The sidebar does not include all the Places
  // views definitions, and we don't need them there.
  // Defining the prototype inheritance in the prototype itself would cause
  // browser.js to halt on "PlacesMenu is not defined" error.
  this.__proto__.__proto__ = PlacesMenu.prototype;
  Object.keys(this._elements).forEach(name => {
    this[name] = document.getElementById(this._elements[name]);
  });
  PlacesMenu.call(this, aPopupShowingEvent,
                  "place:sort=4&maxResults=15");
}
HistoryMenu.prototype = {
  _elements: {
    undoTabMenu: "historyUndoMenu",
    hiddenTabsMenu: "hiddenTabsMenu",
    undoWindowMenu: "historyUndoWindowMenu",
    syncTabsMenuitem: "sync-tabs-menuitem",
  },
  _getClosedTabCount() {
    // SessionStore doesn't track the hidden window, so just return zero then.
    if (window == Services.appShell.hiddenDOMWindow) {
      return 0;
    }
    return SessionStore.getClosedTabCount(window);
  },
  toggleHiddenTabs() {
    if (window.gBrowser &&
        gBrowser.visibleTabs.length < gBrowser.tabs.length) {
      this.hiddenTabsMenu.removeAttribute("hidden");
    } else {
      this.hiddenTabsMenu.setAttribute("hidden", "true");
    }
  },
  toggleRecentlyClosedTabs: function HM_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: function PHM_populateUndoSubmenu() {
    var undoPopup = this.undoTabMenu.firstChild;
    // 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");
    undoPopup.appendChild(tabsFragment);
  },
  toggleRecentlyClosedWindows: function PHM_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: function PHM_populateUndoWindowSubmenu() {
    let undoPopup = this.undoWindowMenu.firstChild;
    // 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");
    undoPopup.appendChild(windowsFragment);
  },
  toggleTabsFromOtherComputers: function PHM_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.setAttribute("hidden", true);
      return;
    }
    this.syncTabsMenuitem.setAttribute("hidden", false);
  },
  _onPopupShowing: function HM__onPopupShowing(aEvent) {
    PlacesMenu.prototype._onPopupShowing.apply(this, arguments);
    // Don't handle events for submenus.
    if (aEvent.target != aEvent.currentTarget)
      return;
    this.toggleHiddenTabs();
    this.toggleRecentlyClosedTabs();
    this.toggleRecentlyClosedWindows();
    this.toggleTabsFromOtherComputers();
  },
  _onCommand: function HM__onCommand(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 left-click with modifier if not browser.bookmarks.openInTabClosesMenu.
    if (aEvent.button != 0 || PlacesUIUtils.openInTabClosesMenu)
      return;
    let target = aEvent.originalTarget;
    if (target.tagName != "menuitem")
      return;
    let modifKey = AppConstants.platform === "macosx" ? aEvent.metaKey
                                                      : aEvent.ctrlKey;
    if (modifKey) {
      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.openContainerNodeInTabs(target._placesNode, aEvent, aView);
    } else if (aEvent.button == 1) {
      // left-clicks with modifier are already served by onCommand
      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);
  },
  fillInBHTooltip: function BEH_fillInBHTooltip(aDocument, aEvent) {
    var node;
    var cropped = false;
    var targetURI;
    if (aDocument.tooltipNode.localName == "treechildren") {
      var tree = aDocument.tooltipNode.parentNode;
      var tbo = tree.treeBoxObject;
      var cell = tbo.getCellAt(aEvent.clientX, aEvent.clientY);
      if (cell.row == -1)
        return false;
      node = tree.view.nodeForTreeIndex(cell.row);
      cropped = tbo.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 = aDocument.tooltipNode;
      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;
    var tooltipTitle = aDocument.getElementById("bhtTitleText");
    tooltipTitle.hidden = (!title || (title == url));
    if (!tooltipTitle.hidden)
      tooltipTitle.textContent = title;
    var tooltipUrl = aDocument.getElementById("bhtUrlText");
    tooltipUrl.hidden = !url;
    if (!tooltipUrl.hidden)
      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