summaryrefslogtreecommitdiffstats
path: root/browser/components/places/content/browserPlacesViews.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/places/content/browserPlacesViews.js')
-rw-r--r--browser/components/places/content/browserPlacesViews.js2443
1 files changed, 2443 insertions, 0 deletions
diff --git a/browser/components/places/content/browserPlacesViews.js b/browser/components/places/content/browserPlacesViews.js
new file mode 100644
index 0000000000..17c9347e39
--- /dev/null
+++ b/browser/components/places/content/browserPlacesViews.js
@@ -0,0 +1,2443 @@
+/* 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/. */
+
+/* eslint-env mozilla/browser-window */
+
+/**
+ * The base view implements everything that's common to the toolbar and
+ * menu views.
+ */
+function PlacesViewBase(aPlace, aOptions = {}) {
+ if ("rootElt" in aOptions) {
+ this._rootElt = aOptions.rootElt;
+ }
+ if ("viewElt" in aOptions) {
+ this._viewElt = aOptions.viewElt;
+ }
+ this.options = aOptions;
+ this._controller = new PlacesController(this);
+ this.place = aPlace;
+ this._viewElt.controllers.appendController(this._controller);
+}
+
+PlacesViewBase.interfaces = [
+ Ci.nsINavHistoryResultObserver,
+ Ci.nsISupportsWeakReference,
+];
+
+PlacesViewBase.prototype = {
+ // The xul element that holds the entire view.
+ _viewElt: null,
+ get viewElt() {
+ return this._viewElt;
+ },
+
+ get associatedElement() {
+ return this._viewElt;
+ },
+
+ get controllers() {
+ return this._viewElt.controllers;
+ },
+
+ // The xul element that represents the root container.
+ _rootElt: null,
+
+ // Set to true for views that are represented by native widgets (i.e.
+ // the native mac menu).
+ _nativeView: false,
+
+ QueryInterface: ChromeUtils.generateQI(PlacesViewBase.interfaces),
+
+ _place: "",
+ get place() {
+ return this._place;
+ },
+ set place(val) {
+ this._place = val;
+
+ let history = PlacesUtils.history;
+ let query = {},
+ options = {};
+ history.queryStringToQuery(val, query, options);
+ let result = history.executeQuery(query.value, options.value);
+ result.addObserver(this);
+ },
+
+ _result: null,
+ get result() {
+ return this._result;
+ },
+ set result(val) {
+ if (this._result == val) {
+ return;
+ }
+
+ if (this._result) {
+ this._result.removeObserver(this);
+ this._resultNode.containerOpen = false;
+ }
+
+ if (this._rootElt.localName == "menupopup") {
+ this._rootElt._built = false;
+ }
+
+ this._result = val;
+ if (val) {
+ this._resultNode = val.root;
+ this._rootElt._placesNode = this._resultNode;
+ this._domNodes = new Map();
+ this._domNodes.set(this._resultNode, this._rootElt);
+
+ // This calls _rebuild through invalidateContainer.
+ this._resultNode.containerOpen = true;
+ } else {
+ this._resultNode = null;
+ delete this._domNodes;
+ }
+ },
+
+ _options: null,
+ get options() {
+ return this._options;
+ },
+ set options(val) {
+ if (!val) {
+ val = {};
+ }
+
+ if (!("extraClasses" in val)) {
+ val.extraClasses = {};
+ }
+ this._options = val;
+ },
+
+ /**
+ * Gets the DOM node used for the given places node.
+ *
+ * @param aPlacesNode
+ * a places result node.
+ * @param aAllowMissing
+ * whether the node may be missing
+ * @throws if there is no DOM node set for aPlacesNode.
+ */
+ _getDOMNodeForPlacesNode: function PVB__getDOMNodeForPlacesNode(
+ aPlacesNode,
+ aAllowMissing = false
+ ) {
+ let node = this._domNodes.get(aPlacesNode, null);
+ if (!node && !aAllowMissing) {
+ throw new Error(
+ "No DOM node set for aPlacesNode.\nnode.type: " +
+ aPlacesNode.type +
+ ". node.parent: " +
+ aPlacesNode
+ );
+ }
+ return node;
+ },
+
+ get controller() {
+ return this._controller;
+ },
+
+ get selType() {
+ return "single";
+ },
+ selectItems() {},
+ selectAll() {},
+
+ get selectedNode() {
+ if (this._contextMenuShown) {
+ let anchor = this._contextMenuShown.triggerNode;
+ if (!anchor) {
+ return null;
+ }
+
+ if (anchor._placesNode) {
+ return this._rootElt == anchor ? null : anchor._placesNode;
+ }
+
+ anchor = anchor.parentNode;
+ return this._rootElt == anchor ? null : anchor._placesNode || null;
+ }
+ return null;
+ },
+
+ get hasSelection() {
+ return this.selectedNode != null;
+ },
+
+ get selectedNodes() {
+ let selectedNode = this.selectedNode;
+ return selectedNode ? [selectedNode] : [];
+ },
+
+ get removableSelectionRanges() {
+ // On static content the current selectedNode would be the selection's
+ // parent node. We don't want to allow removing a node when the
+ // selection is not explicit.
+ if (
+ document.popupNode &&
+ (document.popupNode == "menupopup" || !document.popupNode._placesNode)
+ ) {
+ return [];
+ }
+
+ return [this.selectedNodes];
+ },
+
+ get draggableSelection() {
+ return [this._draggedElt];
+ },
+
+ get insertionPoint() {
+ // There is no insertion point for history queries, so bail out now and
+ // save a lot of work when updating commands.
+ let resultNode = this._resultNode;
+ if (
+ PlacesUtils.nodeIsQuery(resultNode) &&
+ PlacesUtils.asQuery(resultNode).queryOptions.queryType ==
+ Ci.nsINavHistoryQueryOptions.QUERY_TYPE_HISTORY
+ ) {
+ return null;
+ }
+
+ // By default, the insertion point is at the top level, at the end.
+ let index = PlacesUtils.bookmarks.DEFAULT_INDEX;
+ let container = this._resultNode;
+ let orientation = Ci.nsITreeView.DROP_BEFORE;
+ let tagName = null;
+
+ let selectedNode = this.selectedNode;
+ if (selectedNode) {
+ let popup = document.popupNode;
+ if (
+ !popup._placesNode ||
+ popup._placesNode == this._resultNode ||
+ popup._placesNode.itemId == -1 ||
+ !selectedNode.parent
+ ) {
+ // If a static menuitem is selected, or if the root node is selected,
+ // the insertion point is inside the folder, at the end.
+ container = selectedNode;
+ orientation = Ci.nsITreeView.DROP_ON;
+ } else {
+ // In all other cases the insertion point is before that node.
+ container = selectedNode.parent;
+ index = container.getChildIndex(selectedNode);
+ if (PlacesUtils.nodeIsTagQuery(container)) {
+ tagName = PlacesUtils.asQuery(container).query.tags[0];
+ }
+ }
+ }
+
+ if (this.controller.disallowInsertion(container)) {
+ return null;
+ }
+
+ return new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(container),
+ parentGuid: PlacesUtils.getConcreteItemGuid(container),
+ index,
+ orientation,
+ tagName,
+ });
+ },
+
+ buildContextMenu: function PVB_buildContextMenu(aPopup) {
+ this._contextMenuShown = aPopup;
+ window.updateCommands("places");
+
+ // Ensure that an existing "Show Other Bookmarks" item is removed before adding it
+ // again. This item should only be added when gBookmarksToolbar2h2020 is true, but
+ // its possible the pref could be toggled off in the same window. This results in
+ // the "Show Other Bookmarks" menu item still being visible even when the pref is
+ // set to false.
+ let existingOtherBookmarksItem = aPopup.querySelector(
+ "#show-other-bookmarks_PersonalToolbar"
+ );
+ existingOtherBookmarksItem?.remove();
+
+ // Add the View menu for the Bookmarks Toolbar and "Show Other Bookmarks" menu item
+ // if the click originated from the Bookmarks Toolbar.
+ if (gBookmarksToolbar2h2020) {
+ let existingSubmenu = aPopup.querySelector("#toggle_PersonalToolbar");
+ existingSubmenu?.remove();
+ let bookmarksToolbar = document.getElementById("PersonalToolbar");
+ if (bookmarksToolbar?.contains(aPopup.triggerNode)) {
+ let menu = BookmarkingUI.buildBookmarksToolbarSubmenu(bookmarksToolbar);
+ aPopup.appendChild(menu);
+
+ if (
+ aPopup.triggerNode.id === "OtherBookmarks" ||
+ aPopup.triggerNode.id === "PlacesChevron" ||
+ aPopup.triggerNode.id === "PlacesToolbarItems"
+ ) {
+ let otherBookmarksMenuItem = BookmarkingUI.buildShowOtherBookmarksMenuItem();
+
+ if (otherBookmarksMenuItem) {
+ aPopup.insertBefore(
+ otherBookmarksMenuItem,
+ menu.nextElementSibling
+ );
+ }
+ }
+ }
+ }
+
+ return this.controller.buildContextMenu(aPopup);
+ },
+
+ destroyContextMenu: function PVB_destroyContextMenu(aPopup) {
+ this._contextMenuShown = null;
+ },
+
+ clearAllContents(aPopup) {
+ let kid = aPopup.firstElementChild;
+ while (kid) {
+ let next = kid.nextElementSibling;
+ if (!kid.classList.contains("panel-header")) {
+ kid.remove();
+ }
+ kid = next;
+ }
+ aPopup._emptyMenuitem = aPopup._startMarker = aPopup._endMarker = null;
+ },
+
+ _cleanPopup: function PVB_cleanPopup(aPopup, aDelay) {
+ // Ensure markers are here when `invalidateContainer` is called before the
+ // popup is shown, which may the case for panelviews, for example.
+ this._ensureMarkers(aPopup);
+ // Remove Places nodes from the popup.
+ let child = aPopup._startMarker;
+ while (child.nextElementSibling != aPopup._endMarker) {
+ let sibling = child.nextElementSibling;
+ if (sibling._placesNode && !aDelay) {
+ aPopup.removeChild(sibling);
+ } else if (sibling._placesNode && aDelay) {
+ // HACK (bug 733419): the popups originating from the OS X native
+ // menubar don't live-update while open, thus we don't clean it
+ // until the next popupshowing, to avoid zombie menuitems.
+ if (!aPopup._delayedRemovals) {
+ aPopup._delayedRemovals = [];
+ }
+ aPopup._delayedRemovals.push(sibling);
+ child = child.nextElementSibling;
+ } else {
+ child = child.nextElementSibling;
+ }
+ }
+ },
+
+ _rebuildPopup: function PVB__rebuildPopup(aPopup) {
+ let resultNode = aPopup._placesNode;
+ if (!resultNode.containerOpen) {
+ return;
+ }
+
+ this._cleanPopup(aPopup);
+
+ let cc = resultNode.childCount;
+ if (cc > 0) {
+ this._setEmptyPopupStatus(aPopup, false);
+ let fragment = document.createDocumentFragment();
+ for (let i = 0; i < cc; ++i) {
+ let child = resultNode.getChild(i);
+ this._insertNewItemToPopup(child, fragment);
+ }
+ aPopup.insertBefore(fragment, aPopup._endMarker);
+ } else {
+ this._setEmptyPopupStatus(aPopup, true);
+ }
+ aPopup._built = true;
+ },
+
+ _removeChild: function PVB__removeChild(aChild) {
+ // If document.popupNode pointed to this child, null it out,
+ // otherwise controller's command-updating may rely on the removed
+ // item still being "selected".
+ if (document.popupNode == aChild) {
+ document.popupNode = null;
+ }
+
+ aChild.remove();
+ },
+
+ _setEmptyPopupStatus: function PVB__setEmptyPopupStatus(aPopup, aEmpty) {
+ if (!aPopup._emptyMenuitem) {
+ let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
+ aPopup._emptyMenuitem = document.createXULElement("menuitem");
+ aPopup._emptyMenuitem.setAttribute("label", label);
+ aPopup._emptyMenuitem.setAttribute("disabled", true);
+ aPopup._emptyMenuitem.className = "bookmark-item";
+ if (typeof this.options.extraClasses.entry == "string") {
+ aPopup._emptyMenuitem.classList.add(this.options.extraClasses.entry);
+ }
+ }
+
+ if (aEmpty) {
+ aPopup.setAttribute("emptyplacesresult", "true");
+ // Don't add the menuitem if there is static content.
+ if (
+ !aPopup._startMarker.previousElementSibling &&
+ !aPopup._endMarker.nextElementSibling
+ ) {
+ aPopup.insertBefore(aPopup._emptyMenuitem, aPopup._endMarker);
+ }
+ } else {
+ aPopup.removeAttribute("emptyplacesresult");
+ try {
+ aPopup.removeChild(aPopup._emptyMenuitem);
+ } catch (ex) {}
+ }
+ },
+
+ _createDOMNodeForPlacesNode: function PVB__createDOMNodeForPlacesNode(
+ aPlacesNode
+ ) {
+ this._domNodes.delete(aPlacesNode);
+
+ let element;
+ let type = aPlacesNode.type;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ element = document.createXULElement("menuseparator");
+ element.setAttribute("class", "small-separator");
+ } else {
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
+ element = document.createXULElement("menuitem");
+ element.className =
+ "menuitem-iconic bookmark-item menuitem-with-favicon";
+ element.setAttribute(
+ "scheme",
+ PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri)
+ );
+ } else if (PlacesUtils.containerTypes.includes(type)) {
+ element = document.createXULElement("menu");
+ element.setAttribute("container", "true");
+
+ if (aPlacesNode.type == Ci.nsINavHistoryResultNode.RESULT_TYPE_QUERY) {
+ element.setAttribute("query", "true");
+ if (PlacesUtils.nodeIsTagQuery(aPlacesNode)) {
+ element.setAttribute("tagContainer", "true");
+ } else if (PlacesUtils.nodeIsDay(aPlacesNode)) {
+ element.setAttribute("dayContainer", "true");
+ } else if (PlacesUtils.nodeIsHost(aPlacesNode)) {
+ element.setAttribute("hostContainer", "true");
+ }
+ }
+
+ let popup = document.createXULElement("menupopup", {
+ is: "places-popup",
+ });
+ popup._placesNode = PlacesUtils.asContainer(aPlacesNode);
+
+ if (!this._nativeView) {
+ popup.setAttribute("placespopup", "true");
+ }
+
+ element.appendChild(popup);
+ element.className = "menu-iconic bookmark-item";
+ if (typeof this.options.extraClasses.entry == "string") {
+ element.classList.add(this.options.extraClasses.entry);
+ }
+
+ this._domNodes.set(aPlacesNode, popup);
+ } else {
+ throw new Error("Unexpected node");
+ }
+
+ element.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
+
+ let icon = aPlacesNode.icon;
+ if (icon) {
+ element.setAttribute("image", icon);
+ }
+ }
+
+ element._placesNode = aPlacesNode;
+ if (!this._domNodes.has(aPlacesNode)) {
+ this._domNodes.set(aPlacesNode, element);
+ }
+
+ return element;
+ },
+
+ _insertNewItemToPopup: function PVB__insertNewItemToPopup(
+ aNewChild,
+ aInsertionNode,
+ aBefore = null
+ ) {
+ let element = this._createDOMNodeForPlacesNode(aNewChild);
+
+ if (element.localName == "menuitem" || element.localName == "menu") {
+ if (typeof this.options.extraClasses.entry == "string") {
+ element.classList.add(this.options.extraClasses.entry);
+ }
+ }
+
+ aInsertionNode.insertBefore(element, aBefore);
+ return element;
+ },
+
+ toggleCutNode: function PVB_toggleCutNode(aPlacesNode, aValue) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // We may get the popup for menus, but we need the menu itself.
+ if (elt.localName == "menupopup") {
+ elt = elt.parentNode;
+ }
+ if (aValue) {
+ elt.setAttribute("cutting", "true");
+ } else {
+ elt.removeAttribute("cutting");
+ }
+ },
+
+ nodeURIChanged: function PVB_nodeURIChanged(aPlacesNode, aURIString) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup") {
+ elt = elt.parentNode;
+ }
+
+ elt.setAttribute(
+ "scheme",
+ PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri)
+ );
+ },
+
+ nodeIconChanged: function PVB_nodeIconChanged(aPlacesNode) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // There's no UI representation for the root node, thus there's nothing to
+ // be done when the icon changes.
+ if (elt == this._rootElt) {
+ return;
+ }
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup") {
+ elt = elt.parentNode;
+ }
+ // We must remove and reset the attribute to force an update.
+ elt.removeAttribute("image");
+ elt.setAttribute("image", aPlacesNode.icon);
+ },
+
+ nodeTitleChanged: function PVB_nodeTitleChanged(aPlacesNode, aNewTitle) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // There's no UI representation for the root node, thus there's
+ // nothing to be done when the title changes.
+ if (elt == this._rootElt) {
+ return;
+ }
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup") {
+ elt = elt.parentNode;
+ }
+
+ if (!aNewTitle && elt.localName != "toolbarbutton") {
+ // Many users consider toolbars as shortcuts containers, so explicitly
+ // allow empty labels on toolbarbuttons. For any other element try to be
+ // smarter, guessing a title from the uri.
+ elt.setAttribute("label", PlacesUIUtils.getBestTitle(aPlacesNode));
+ } else {
+ elt.setAttribute("label", aNewTitle);
+ }
+ },
+
+ nodeRemoved: function PVB_nodeRemoved(
+ aParentPlacesNode,
+ aPlacesNode,
+ aIndex
+ ) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup") {
+ elt = elt.parentNode;
+ }
+
+ if (parentElt._built) {
+ parentElt.removeChild(elt);
+
+ // Figure out if we need to show the "<Empty>" menu-item.
+ // TODO Bug 517701: This doesn't seem to handle the case of an empty
+ // root.
+ if (parentElt._startMarker.nextElementSibling == parentElt._endMarker) {
+ this._setEmptyPopupStatus(parentElt, true);
+ }
+ }
+ },
+
+ // Opt-out of history details updates, since all the views derived from this
+ // are not showing them.
+ observeHistoryDetails: false,
+ nodeHistoryDetailsChanged() {},
+ nodeTagsChanged() {},
+ nodeDateAddedChanged() {},
+ nodeLastModifiedChanged() {},
+ nodeKeywordChanged() {},
+ sortingChanged() {},
+ batching() {},
+
+ nodeInserted: function PVB_nodeInserted(
+ aParentPlacesNode,
+ aPlacesNode,
+ aIndex
+ ) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (!parentElt._built) {
+ return;
+ }
+
+ let index =
+ Array.prototype.indexOf.call(parentElt.children, parentElt._startMarker) +
+ aIndex +
+ 1;
+ this._insertNewItemToPopup(
+ aPlacesNode,
+ parentElt,
+ parentElt.children[index] || parentElt._endMarker
+ );
+ this._setEmptyPopupStatus(parentElt, false);
+ },
+
+ nodeMoved: function PBV_nodeMoved(
+ aPlacesNode,
+ aOldParentPlacesNode,
+ aOldIndex,
+ aNewParentPlacesNode,
+ aNewIndex
+ ) {
+ // Note: the current implementation of moveItem does not actually
+ // use this notification when the item in question is moved from one
+ // folder to another. Instead, it calls nodeRemoved and nodeInserted
+ // for the two folders. Thus, we can assume old-parent == new-parent.
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup") {
+ elt = elt.parentNode;
+ }
+
+ // If our root node is a folder, it might be moved. There's nothing
+ // we need to do in that case.
+ if (elt == this._rootElt) {
+ return;
+ }
+
+ let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
+ if (parentElt._built) {
+ // Move the node.
+ parentElt.removeChild(elt);
+ let index =
+ Array.prototype.indexOf.call(
+ parentElt.children,
+ parentElt._startMarker
+ ) +
+ aNewIndex +
+ 1;
+ parentElt.insertBefore(elt, parentElt.children[index]);
+ }
+ },
+
+ containerStateChanged: function PVB_containerStateChanged(
+ aPlacesNode,
+ aOldState,
+ aNewState
+ ) {
+ if (
+ aNewState == Ci.nsINavHistoryContainerResultNode.STATE_OPENED ||
+ aNewState == Ci.nsINavHistoryContainerResultNode.STATE_CLOSED
+ ) {
+ this.invalidateContainer(aPlacesNode);
+ }
+ },
+
+ /**
+ * Checks whether the popup associated with the provided element is open.
+ * This method may be overridden by classes that extend this base class.
+ *
+ * @param {Element} elt
+ * @return {Boolean}
+ */
+ _isPopupOpen(elt) {
+ return !!elt.parentNode.open;
+ },
+
+ invalidateContainer: function PVB_invalidateContainer(aPlacesNode) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ elt._built = false;
+
+ // If the menupopup is open we should live-update it.
+ if (this._isPopupOpen(elt)) {
+ this._rebuildPopup(elt);
+ }
+ },
+
+ uninit: function PVB_uninit() {
+ if (this._result) {
+ this._result.removeObserver(this);
+ this._resultNode.containerOpen = false;
+ this._resultNode = null;
+ this._result = null;
+ }
+
+ if (this._controller) {
+ this._controller.terminate();
+ // Removing the controller will fail if it is already no longer there.
+ // This can happen if the view element was removed/reinserted without
+ // our knowledge. There is no way to check for that having happened
+ // without the possibility of an exception. :-(
+ try {
+ this._viewElt.controllers.removeController(this._controller);
+ } catch (ex) {
+ } finally {
+ this._controller = null;
+ }
+ }
+
+ delete this._viewElt._placesView;
+ },
+
+ get isRTL() {
+ if ("_isRTL" in this) {
+ return this._isRTL;
+ }
+
+ return (this._isRTL =
+ document.defaultView.getComputedStyle(this.viewElt).direction == "rtl");
+ },
+
+ get ownerWindow() {
+ return window;
+ },
+
+ /**
+ * Adds an "Open All in Tabs" menuitem to the bottom of the popup.
+ * @param aPopup
+ * a Places popup.
+ */
+ _mayAddCommandsItems: function PVB__mayAddCommandsItems(aPopup) {
+ // The command items are never added to the root popup.
+ if (aPopup == this._rootElt) {
+ return;
+ }
+
+ let hasMultipleURIs = false;
+
+ // Check if the popup contains at least 2 menuitems with places nodes.
+ // We don't currently support opening multiple uri nodes when they are not
+ // populated by the result.
+ if (aPopup._placesNode.childCount > 0) {
+ let currentChild = aPopup.firstElementChild;
+ let numURINodes = 0;
+ while (currentChild) {
+ if (currentChild.localName == "menuitem" && currentChild._placesNode) {
+ if (++numURINodes == 2) {
+ break;
+ }
+ }
+ currentChild = currentChild.nextElementSibling;
+ }
+ hasMultipleURIs = numURINodes > 1;
+ }
+
+ if (!hasMultipleURIs) {
+ aPopup.setAttribute("nofooterpopup", "true");
+ } else {
+ aPopup.removeAttribute("nofooterpopup");
+ }
+
+ if (!hasMultipleURIs) {
+ // We don't have to show any option.
+ if (aPopup._endOptOpenAllInTabs) {
+ aPopup.removeChild(aPopup._endOptOpenAllInTabs);
+ aPopup._endOptOpenAllInTabs = null;
+
+ aPopup.removeChild(aPopup._endOptSeparator);
+ aPopup._endOptSeparator = null;
+ }
+ } else if (!aPopup._endOptOpenAllInTabs) {
+ // Create a separator before options.
+ aPopup._endOptSeparator = document.createXULElement("menuseparator");
+ aPopup._endOptSeparator.className = "bookmarks-actions-menuseparator";
+ aPopup.appendChild(aPopup._endOptSeparator);
+
+ // Add the "Open All in Tabs" menuitem.
+ aPopup._endOptOpenAllInTabs = document.createXULElement("menuitem");
+ aPopup._endOptOpenAllInTabs.className = "openintabs-menuitem";
+
+ if (typeof this.options.extraClasses.entry == "string") {
+ aPopup._endOptOpenAllInTabs.classList.add(
+ this.options.extraClasses.entry
+ );
+ }
+ if (typeof this.options.extraClasses.footer == "string") {
+ aPopup._endOptOpenAllInTabs.classList.add(
+ this.options.extraClasses.footer
+ );
+ }
+
+ aPopup._endOptOpenAllInTabs.setAttribute(
+ "oncommand",
+ "PlacesUIUtils.openMultipleLinksInTabs(this.parentNode._placesNode, event, " +
+ "PlacesUIUtils.getViewForNode(this));"
+ );
+ aPopup._endOptOpenAllInTabs.setAttribute(
+ "onclick",
+ "checkForMiddleClick(this, event); event.stopPropagation();"
+ );
+ aPopup._endOptOpenAllInTabs.setAttribute(
+ "label",
+ gNavigatorBundle.getString("menuOpenAllInTabs.label")
+ );
+ aPopup.appendChild(aPopup._endOptOpenAllInTabs);
+ }
+ },
+
+ _ensureMarkers: function PVB__ensureMarkers(aPopup) {
+ if (aPopup._startMarker) {
+ return;
+ }
+
+ // _startMarker is an hidden menuseparator that lives before places nodes.
+ aPopup._startMarker = document.createXULElement("menuseparator");
+ aPopup._startMarker.hidden = true;
+ aPopup.insertBefore(aPopup._startMarker, aPopup.firstElementChild);
+
+ // _endMarker is a DOM node that lives after places nodes, specified with
+ // the 'insertionPoint' option or will be a hidden menuseparator.
+ let node = this.options.insertionPoint
+ ? aPopup.querySelector(this.options.insertionPoint)
+ : null;
+ if (node) {
+ aPopup._endMarker = node;
+ } else {
+ aPopup._endMarker = document.createXULElement("menuseparator");
+ aPopup._endMarker.hidden = true;
+ }
+ aPopup.appendChild(aPopup._endMarker);
+
+ // Move the markers to the right position.
+ let firstNonStaticNodeFound = false;
+ for (let i = 0; i < aPopup.children.length; i++) {
+ let child = aPopup.children[i];
+ // Menus that have static content at the end, but are initially empty,
+ // use a special "builder" attribute to figure out where to start
+ // inserting places nodes.
+ if (child.getAttribute("builder") == "end") {
+ aPopup.insertBefore(aPopup._endMarker, child);
+ break;
+ }
+
+ if (child._placesNode && !firstNonStaticNodeFound) {
+ firstNonStaticNodeFound = true;
+ aPopup.insertBefore(aPopup._startMarker, child);
+ }
+ }
+ if (!firstNonStaticNodeFound) {
+ aPopup.insertBefore(aPopup._startMarker, aPopup._endMarker);
+ }
+ },
+
+ _onPopupShowing: function PVB__onPopupShowing(aEvent) {
+ // Avoid handling popupshowing of inner views.
+ let popup = aEvent.originalTarget;
+
+ this._ensureMarkers(popup);
+
+ // Remove any delayed element, see _cleanPopup for details.
+ if ("_delayedRemovals" in popup) {
+ while (popup._delayedRemovals.length) {
+ popup.removeChild(popup._delayedRemovals.shift());
+ }
+ }
+
+ if (popup._placesNode && PlacesUIUtils.getViewForNode(popup) == this) {
+ if (!popup._placesNode.containerOpen) {
+ popup._placesNode.containerOpen = true;
+ }
+ if (!popup._built) {
+ this._rebuildPopup(popup);
+ }
+
+ this._mayAddCommandsItems(popup);
+ }
+ },
+
+ _addEventListeners: function PVB__addEventListeners(
+ aObject,
+ aEventNames,
+ aCapturing = false
+ ) {
+ for (let i = 0; i < aEventNames.length; i++) {
+ aObject.addEventListener(aEventNames[i], this, aCapturing);
+ }
+ },
+
+ _removeEventListeners: function PVB__removeEventListeners(
+ aObject,
+ aEventNames,
+ aCapturing = false
+ ) {
+ for (let i = 0; i < aEventNames.length; i++) {
+ aObject.removeEventListener(aEventNames[i], this, aCapturing);
+ }
+ },
+};
+
+function PlacesToolbar(aPlace) {
+ let startTime = Date.now();
+ // Add some smart getters for our elements.
+ let thisView = this;
+ [
+ ["_viewElt", "PlacesToolbar"],
+ ["_rootElt", "PlacesToolbarItems"],
+ ["_dropIndicator", "PlacesToolbarDropIndicator"],
+ ["_chevron", "PlacesChevron"],
+ ["_chevronPopup", "PlacesChevronPopup"],
+ ].forEach(function(elementGlobal) {
+ let [name, id] = elementGlobal;
+ thisView.__defineGetter__(name, function() {
+ let element = document.getElementById(id);
+ if (!element) {
+ return null;
+ }
+
+ delete thisView[name];
+ return (thisView[name] = element);
+ });
+ });
+
+ this._viewElt._placesView = this;
+
+ this._dragRoot = BookmarkingUI.toolbar.contains(this._viewElt)
+ ? BookmarkingUI.toolbar
+ : this._viewElt;
+ this._addEventListeners(this._dragRoot, this._cbEvents, false);
+ this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
+ this._addEventListeners(this._rootElt, ["overflow", "underflow"], true);
+ this._addEventListeners(window, ["resize", "unload"], false);
+
+ // If personal-bookmarks has been dragged to the tabs toolbar,
+ // we have to track addition and removals of tabs, to properly
+ // recalculate the available space for bookmarks.
+ // TODO (bug 734730): Use a performant mutation listener when available.
+ if (
+ this._viewElt.parentNode.parentNode ==
+ document.getElementById("TabsToolbar")
+ ) {
+ this._addEventListeners(
+ gBrowser.tabContainer,
+ ["TabOpen", "TabClose"],
+ false
+ );
+ }
+
+ this._updatingNodesVisibility = false;
+
+ PlacesViewBase.call(this, aPlace);
+
+ Services.telemetry
+ .getHistogramById("FX_BOOKMARKS_TOOLBAR_INIT_MS")
+ .add(Date.now() - startTime);
+}
+
+PlacesToolbar.prototype = {
+ __proto__: PlacesViewBase.prototype,
+
+ _cbEvents: [
+ "dragstart",
+ "dragover",
+ "dragexit",
+ "dragend",
+ "drop",
+ "mousemove",
+ "mouseover",
+ "mouseout",
+ "mousedown",
+ ],
+
+ QueryInterface: ChromeUtils.generateQI([
+ "nsITimerCallback",
+ ...PlacesViewBase.interfaces,
+ ]),
+
+ uninit: function PT_uninit() {
+ if (this._dragRoot) {
+ this._removeEventListeners(this._dragRoot, this._cbEvents, false);
+ }
+ this._removeEventListeners(
+ this._rootElt,
+ ["popupshowing", "popuphidden"],
+ true
+ );
+ this._removeEventListeners(this._rootElt, ["overflow", "underflow"], true);
+ this._removeEventListeners(window, ["resize", "unload"], false);
+ this._removeEventListeners(
+ gBrowser.tabContainer,
+ ["TabOpen", "TabClose"],
+ false
+ );
+
+ if (this._chevron._placesView) {
+ this._chevron._placesView.uninit();
+ }
+
+ if (this._otherBookmarks?._placesView) {
+ this._otherBookmarks._placesView.uninit();
+ }
+
+ PlacesViewBase.prototype.uninit.apply(this, arguments);
+ },
+
+ _openedMenuButton: null,
+ _allowPopupShowing: true,
+
+ get _isAlive() {
+ return this._resultNode && this._rootElt;
+ },
+
+ _runBeforeFrameRender(callback) {
+ return new Promise((resolve, reject) => {
+ window.requestAnimationFrame(() => {
+ try {
+ resolve(callback());
+ } catch (err) {
+ reject(err);
+ }
+ });
+ });
+ },
+
+ async _rebuild() {
+ // Clear out references to existing nodes, since they will be removed
+ // and re-added.
+ if (this._overFolder.elt) {
+ this._clearOverFolder();
+ }
+
+ this._openedMenuButton = null;
+ while (this._rootElt.hasChildNodes()) {
+ this._rootElt.firstChild.remove();
+ }
+
+ let cc = this._resultNode.childCount;
+ if (cc > 0) {
+ // There could be a lot of nodes, but we only want to build the ones that
+ // are more likely to be shown, not all of them.
+ // We also don't want to wait for reflows at every node insertion, to
+ // calculate a precise number of visible items, thus we guess a size from
+ // the first non-separator node (because separators have flexible size).
+ let startIndex = 0;
+ let limit = await this._runBeforeFrameRender(() => {
+ if (!this._isAlive) {
+ return cc;
+ }
+
+ // Look for the first non-separator node.
+ let elt;
+ while (startIndex < cc) {
+ elt = this._insertNewItem(
+ this._resultNode.getChild(startIndex),
+ this._rootElt
+ );
+ ++startIndex;
+ if (elt.localName != "toolbarseparator") {
+ break;
+ }
+ }
+ if (!elt) {
+ return cc;
+ }
+
+ return window.promiseDocumentFlushed(() => {
+ // We assume a button with just the icon will be more or less a square,
+ // then compensate the measurement error by considering a larger screen
+ // width. Moreover the window could be bigger than the screen.
+ let size = elt.clientHeight || 1; // Sanity fallback.
+ return Math.min(cc, parseInt((window.screen.width * 1.5) / size));
+ });
+ });
+
+ if (!this._isAlive) {
+ return;
+ }
+
+ let fragment = document.createDocumentFragment();
+ for (let i = startIndex; i < limit; ++i) {
+ this._insertNewItem(this._resultNode.getChild(i), fragment);
+ }
+ window.requestAnimationFrame(() => {
+ if (this._isAlive) {
+ this._rootElt.appendChild(fragment);
+ this.updateNodesVisibility();
+ }
+ });
+ }
+
+ if (this._chevronPopup.hasAttribute("type")) {
+ // Chevron has already been initialized, but since we are forcing
+ // a rebuild of the toolbar, it has to be rebuilt.
+ // Otherwise, it will be initialized when the toolbar overflows.
+ this._chevronPopup.place = this.place;
+ }
+
+ // Rebuild the "Other Bookmarks" folder if it already exists.
+ let otherBookmarks = document.getElementById("OtherBookmarks");
+ otherBookmarks?.remove();
+
+ BookmarkingUI.maybeShowOtherBookmarksFolder();
+ },
+
+ _insertNewItem: function PT__insertNewItem(
+ aChild,
+ aInsertionNode,
+ aBefore = null
+ ) {
+ this._domNodes.delete(aChild);
+
+ let type = aChild.type;
+ let button;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ button = document.createXULElement("toolbarseparator");
+ } else {
+ button = document.createXULElement("toolbarbutton");
+ button.className = "bookmark-item";
+ button.setAttribute("label", aChild.title || "");
+
+ if (PlacesUtils.containerTypes.includes(type)) {
+ button.setAttribute("type", "menu");
+ button.setAttribute("container", "true");
+
+ if (PlacesUtils.nodeIsQuery(aChild)) {
+ button.setAttribute("query", "true");
+ if (PlacesUtils.nodeIsTagQuery(aChild)) {
+ button.setAttribute("tagContainer", "true");
+ }
+ }
+
+ let popup = document.createXULElement("menupopup", {
+ is: "places-popup",
+ });
+ popup.setAttribute("placespopup", "true");
+ button.appendChild(popup);
+ popup._placesNode = PlacesUtils.asContainer(aChild);
+ popup.setAttribute("context", "placesContext");
+
+ this._domNodes.set(aChild, popup);
+ } else if (PlacesUtils.nodeIsURI(aChild)) {
+ button.setAttribute(
+ "scheme",
+ PlacesUIUtils.guessUrlSchemeForUI(aChild.uri)
+ );
+ }
+ }
+
+ button._placesNode = aChild;
+ let { icon } = button._placesNode;
+ if (icon) {
+ button.setAttribute("image", icon);
+ }
+ if (!this._domNodes.has(aChild)) {
+ this._domNodes.set(aChild, button);
+ }
+
+ if (aBefore) {
+ aInsertionNode.insertBefore(button, aBefore);
+ } else {
+ aInsertionNode.appendChild(button);
+ }
+ return button;
+ },
+
+ _updateChevronPopupNodesVisibility: function PT__updateChevronPopupNodesVisibility() {
+ // Note the toolbar by default builds less nodes than the chevron popup.
+ for (
+ let toolbarNode = this._rootElt.firstElementChild,
+ node = this._chevronPopup._startMarker.nextElementSibling;
+ toolbarNode && node;
+ toolbarNode = toolbarNode.nextElementSibling,
+ node = node.nextElementSibling
+ ) {
+ node.hidden = toolbarNode.style.visibility != "hidden";
+ }
+ },
+
+ _onChevronPopupShowing: function PT__onChevronPopupShowing(aEvent) {
+ // Handle popupshowing only for the chevron popup, not for nested ones.
+ if (aEvent.target != this._chevronPopup) {
+ return;
+ }
+
+ if (!this._chevron._placesView) {
+ this._chevron._placesView = new PlacesMenu(aEvent, this.place);
+ }
+
+ this._updateChevronPopupNodesVisibility();
+ },
+
+ _onOtherBookmarksPopupShowing: function PT__onOtherBookmarksPopupShowing(
+ aEvent
+ ) {
+ if (aEvent.target != this._otherBookmarksPopup) {
+ return;
+ }
+
+ if (!this._otherBookmarks._placesView) {
+ this._otherBookmarks._placesView = new PlacesMenu(
+ aEvent,
+ "place:parent=" + PlacesUtils.bookmarks.unfiledGuid
+ );
+ }
+ },
+
+ handleEvent: function PT_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "unload":
+ this.uninit();
+ break;
+ case "resize":
+ // This handler updates nodes visibility in both the toolbar
+ // and the chevron popup when a window resize does not change
+ // the overflow status of the toolbar.
+ if (aEvent.target == aEvent.currentTarget) {
+ this.updateNodesVisibility();
+ }
+ break;
+ case "overflow":
+ if (!this._isOverflowStateEventRelevant(aEvent)) {
+ return;
+ }
+ // Avoid triggering overflow in containers if possible
+ aEvent.stopPropagation();
+ this._onOverflow();
+ break;
+ case "underflow":
+ if (!this._isOverflowStateEventRelevant(aEvent)) {
+ return;
+ }
+ // Avoid triggering underflow in containers if possible
+ aEvent.stopPropagation();
+ this._onUnderflow();
+ break;
+ case "TabOpen":
+ case "TabClose":
+ this.updateNodesVisibility();
+ break;
+ case "dragstart":
+ this._onDragStart(aEvent);
+ break;
+ case "dragover":
+ this._onDragOver(aEvent);
+ break;
+ case "dragexit":
+ this._onDragExit(aEvent);
+ break;
+ case "dragend":
+ this._onDragEnd(aEvent);
+ break;
+ case "drop":
+ this._onDrop(aEvent);
+ break;
+ case "mouseover":
+ this._onMouseOver(aEvent);
+ break;
+ case "mousemove":
+ this._onMouseMove(aEvent);
+ break;
+ case "mouseout":
+ this._onMouseOut(aEvent);
+ break;
+ case "mousedown":
+ this._onMouseDown(aEvent);
+ break;
+ case "popupshowing":
+ this._onPopupShowing(aEvent);
+ break;
+ case "popuphidden":
+ this._onPopupHidden(aEvent);
+ break;
+ default:
+ throw new Error("Trying to handle unexpected event.");
+ }
+ },
+
+ _isOverflowStateEventRelevant: function PT_isOverflowStateEventRelevant(
+ aEvent
+ ) {
+ // Ignore events not aimed at ourselves, as well as purely vertical ones:
+ return aEvent.target == aEvent.currentTarget && aEvent.detail > 0;
+ },
+
+ _onOverflow: function PT_onOverflow() {
+ // Attach the popup binding to the chevron popup if it has not yet
+ // been initialized.
+ if (!this._chevronPopup.hasAttribute("type")) {
+ this._chevronPopup.setAttribute("place", this.place);
+ this._chevronPopup.setAttribute("type", "places");
+ }
+ this._chevron.collapsed = false;
+ this.updateNodesVisibility();
+ },
+
+ _onUnderflow: function PT_onUnderflow() {
+ this.updateNodesVisibility();
+ this._chevron.collapsed = true;
+ },
+
+ updateNodesVisibility: function PT_updateNodesVisibility() {
+ // Update the chevron on a timer. This will avoid repeated work when
+ // lot of changes happen in a small timeframe.
+ if (this._updateNodesVisibilityTimer) {
+ this._updateNodesVisibilityTimer.cancel();
+ }
+
+ this._updateNodesVisibilityTimer = this._setTimer(100);
+ },
+
+ async _updateNodesVisibilityTimerCallback() {
+ if (this._updatingNodesVisibility || window.closed) {
+ return;
+ }
+ this._updatingNodesVisibility = true;
+
+ let dwu = window.windowUtils;
+
+ let scrollRect = await window.promiseDocumentFlushed(() =>
+ dwu.getBoundsWithoutFlushing(this._rootElt)
+ );
+
+ let childOverflowed = false;
+
+ // We're about to potentially update a bunch of nodes, so we do it
+ // in a requestAnimationFrame so that other JS that's might execute
+ // in the same tick can avoid flushing styles and layout for these
+ // changes.
+ window.requestAnimationFrame(() => {
+ for (let child of this._rootElt.children) {
+ // Once a child overflows, all the next ones will.
+ if (!childOverflowed) {
+ let childRect = dwu.getBoundsWithoutFlushing(child);
+ childOverflowed = this.isRTL
+ ? childRect.left < scrollRect.left
+ : childRect.right > scrollRect.right;
+ }
+
+ if (childOverflowed) {
+ child.removeAttribute("image");
+ child.style.visibility = "hidden";
+ } else {
+ let icon = child._placesNode.icon;
+ if (icon) {
+ child.setAttribute("image", icon);
+ }
+ child.style.visibility = "visible";
+ }
+ }
+
+ // We rebuild the chevron on popupShowing, so if it is open
+ // we must update it.
+ if (!this._chevron.collapsed && this._chevron.open) {
+ this._updateChevronPopupNodesVisibility();
+ }
+
+ let event = new CustomEvent("BookmarksToolbarVisibilityUpdated", {
+ bubbles: true,
+ });
+ this._viewElt.dispatchEvent(event);
+ this._updatingNodesVisibility = false;
+ });
+ },
+
+ nodeInserted: function PT_nodeInserted(
+ aParentPlacesNode,
+ aPlacesNode,
+ aIndex
+ ) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (parentElt == this._rootElt) {
+ // Node is on the toolbar.
+ let children = this._rootElt.children;
+ // Nothing to do if it's a never-visible node, but note it's possible
+ // we are appending.
+ if (aIndex > children.length) {
+ return;
+ }
+
+ // Note that childCount is already accounting for the node being added,
+ // thus we must subtract one node from it.
+ if (this._resultNode.childCount - 1 > children.length) {
+ if (aIndex == children.length) {
+ // If we didn't build all the nodes and new node is being appended,
+ // we can skip it as well.
+ return;
+ }
+ // Keep the number of built nodes consistent.
+ this._rootElt.removeChild(this._rootElt.lastElementChild);
+ }
+
+ let button = this._insertNewItem(
+ aPlacesNode,
+ this._rootElt,
+ children[aIndex] || null
+ );
+ let prevSiblingOverflowed =
+ aIndex > 0 &&
+ aIndex <= children.length &&
+ children[aIndex - 1].style.visibility == "hidden";
+ if (prevSiblingOverflowed) {
+ button.style.visibility = "hidden";
+ } else {
+ let icon = aPlacesNode.icon;
+ if (icon) {
+ button.setAttribute("image", icon);
+ }
+ this.updateNodesVisibility();
+ }
+ return;
+ }
+
+ PlacesViewBase.prototype.nodeInserted.apply(this, arguments);
+ },
+
+ nodeRemoved: function PT_nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (parentElt == this._rootElt) {
+ // Node is on the toolbar.
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
+ // Nothing to do if it's a never-visible node.
+ if (!elt) {
+ return;
+ }
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup") {
+ elt = elt.parentNode;
+ }
+
+ let overflowed = elt.style.visibility == "hidden";
+ this._removeChild(elt);
+ if (this._resultNode.childCount > this._rootElt.children.length) {
+ // A new node should be built to keep a coherent number of children.
+ this._insertNewItem(
+ this._resultNode.getChild(this._rootElt.children.length),
+ this._rootElt
+ );
+ }
+ if (!overflowed) {
+ this.updateNodesVisibility();
+ }
+ return;
+ }
+
+ PlacesViewBase.prototype.nodeRemoved.apply(this, arguments);
+ },
+
+ nodeMoved: function PT_nodeMoved(
+ aPlacesNode,
+ aOldParentPlacesNode,
+ aOldIndex,
+ aNewParentPlacesNode,
+ aNewIndex
+ ) {
+ let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
+ if (parentElt == this._rootElt) {
+ // Node is on the toolbar.
+ // Do nothing if the node will never be visible.
+ let lastBuiltIndex = this._rootElt.children.length - 1;
+ if (aOldIndex > lastBuiltIndex && aNewIndex > lastBuiltIndex + 1) {
+ return;
+ }
+
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
+ if (elt) {
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup") {
+ elt = elt.parentNode;
+ }
+ this._removeChild(elt);
+ }
+
+ if (aNewIndex > lastBuiltIndex + 1) {
+ if (this._resultNode.childCount > this._rootElt.children.length) {
+ // If the element was built and becomes non built, another node should
+ // be built to keep a coherent number of children.
+ this._insertNewItem(
+ this._resultNode.getChild(this._rootElt.children.length),
+ this._rootElt
+ );
+ }
+ return;
+ }
+
+ if (!elt) {
+ // The node has not been inserted yet, so we must create it.
+ elt = this._insertNewItem(
+ aPlacesNode,
+ this._rootElt,
+ this._rootElt.children[aNewIndex]
+ );
+ let icon = aPlacesNode.icon;
+ if (icon) {
+ elt.setAttribute("image", icon);
+ }
+ } else {
+ this._rootElt.insertBefore(elt, this._rootElt.children[aNewIndex]);
+ }
+
+ // The chevron view may get nodeMoved after the toolbar. In such a case,
+ // we should ensure (by manually swapping menuitems) that the actual nodes
+ // are in the final position before updateNodesVisibility tries to update
+ // their visibility, or the chevron may go out of sync.
+ // Luckily updateNodesVisibility runs on a timer, so, by the time it updates
+ // nodes, the menu has already handled the notification.
+
+ this.updateNodesVisibility();
+ return;
+ }
+
+ PlacesViewBase.prototype.nodeMoved.apply(this, arguments);
+ },
+
+ nodeTitleChanged: function PT_nodeTitleChanged(aPlacesNode, aNewTitle) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
+
+ // Nothing to do if it's a never-visible node.
+ if (!elt || elt == this._rootElt) {
+ return;
+ }
+
+ PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
+
+ // Here we need the <menu>.
+ if (elt.localName == "menupopup") {
+ elt = elt.parentNode;
+ }
+
+ if (elt.parentNode == this._rootElt) {
+ // Node is on the toolbar.
+ if (elt.style.visibility != "hidden") {
+ this.updateNodesVisibility();
+ }
+ }
+ },
+
+ invalidateContainer: function PT_invalidateContainer(aPlacesNode) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true);
+ // Nothing to do if it's a never-visible node.
+ if (!elt) {
+ return;
+ }
+
+ if (elt == this._rootElt) {
+ // Container is the toolbar itself.
+ this._rebuild().catch(Cu.reportError);
+ return;
+ }
+
+ PlacesViewBase.prototype.invalidateContainer.apply(this, arguments);
+ },
+
+ _overFolder: { elt: null, openTimer: null, hoverTime: 350, closeTimer: null },
+
+ _clearOverFolder: function PT__clearOverFolder() {
+ // The mouse is no longer dragging over the stored menubutton.
+ // Close the menubutton, clear out drag styles, and clear all
+ // timers for opening/closing it.
+ if (this._overFolder.elt && this._overFolder.elt.menupopup) {
+ if (!this._overFolder.elt.menupopup.hasAttribute("dragover")) {
+ this._overFolder.elt.menupopup.hidePopup();
+ }
+ this._overFolder.elt.removeAttribute("dragover");
+ this._overFolder.elt = null;
+ }
+ if (this._overFolder.openTimer) {
+ this._overFolder.openTimer.cancel();
+ this._overFolder.openTimer = null;
+ }
+ if (this._overFolder.closeTimer) {
+ this._overFolder.closeTimer.cancel();
+ this._overFolder.closeTimer = null;
+ }
+ },
+
+ /**
+ * This function returns information about where to drop when dragging over
+ * the toolbar. The returned object has the following properties:
+ * - ip: the insertion point for the bookmarks service.
+ * - beforeIndex: child index to drop before, for the drop indicator.
+ * - folderElt: the folder to drop into, if applicable.
+ */
+ _getDropPoint: function PT__getDropPoint(aEvent) {
+ if (!PlacesUtils.nodeIsFolder(this._resultNode)) {
+ return null;
+ }
+
+ let dropPoint = { ip: null, beforeIndex: null, folderElt: null };
+ let elt = aEvent.target;
+ if (
+ elt._placesNode &&
+ elt != this._rootElt &&
+ elt.localName != "menupopup"
+ ) {
+ let eltRect = elt.getBoundingClientRect();
+ let eltIndex = Array.prototype.indexOf.call(this._rootElt.children, elt);
+ if (
+ PlacesUtils.nodeIsFolder(elt._placesNode) &&
+ !PlacesUIUtils.isFolderReadOnly(elt._placesNode)
+ ) {
+ // This is a folder.
+ // If we are in the middle of it, drop inside it.
+ // Otherwise, drop before it, with regards to RTL mode.
+ let threshold = eltRect.width * 0.25;
+ if (
+ this.isRTL
+ ? aEvent.clientX > eltRect.right - threshold
+ : aEvent.clientX < eltRect.left + threshold
+ ) {
+ // Drop before this folder.
+ dropPoint.ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(this._resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
+ index: eltIndex,
+ orientation: Ci.nsITreeView.DROP_BEFORE,
+ });
+ dropPoint.beforeIndex = eltIndex;
+ } else if (
+ this.isRTL
+ ? aEvent.clientX > eltRect.left + threshold
+ : aEvent.clientX < eltRect.right - threshold
+ ) {
+ // Drop inside this folder.
+ let tagName = PlacesUtils.nodeIsTagQuery(elt._placesNode)
+ ? elt._placesNode.title
+ : null;
+ dropPoint.ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(elt._placesNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(elt._placesNode),
+ tagName,
+ });
+ dropPoint.beforeIndex = eltIndex;
+ dropPoint.folderElt = elt;
+ } else {
+ // Drop after this folder.
+ let beforeIndex =
+ eltIndex == this._rootElt.children.length - 1 ? -1 : eltIndex + 1;
+
+ dropPoint.ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(this._resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
+ index: beforeIndex,
+ orientation: Ci.nsITreeView.DROP_BEFORE,
+ });
+ dropPoint.beforeIndex = beforeIndex;
+ }
+ } else {
+ // This is a non-folder node or a read-only folder.
+ // Drop before it with regards to RTL mode.
+ let threshold = eltRect.width * 0.5;
+ if (
+ this.isRTL
+ ? aEvent.clientX > eltRect.left + threshold
+ : aEvent.clientX < eltRect.left + threshold
+ ) {
+ // Drop before this bookmark.
+ dropPoint.ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(this._resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
+ index: eltIndex,
+ orientation: Ci.nsITreeView.DROP_BEFORE,
+ });
+ dropPoint.beforeIndex = eltIndex;
+ } else {
+ // Drop after this bookmark.
+ let beforeIndex =
+ eltIndex == this._rootElt.children.length - 1 ? -1 : eltIndex + 1;
+ dropPoint.ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(this._resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
+ index: beforeIndex,
+ orientation: Ci.nsITreeView.DROP_BEFORE,
+ });
+ dropPoint.beforeIndex = beforeIndex;
+ }
+ }
+ } else {
+ // We are most likely dragging on the empty area of the
+ // toolbar, we should drop after the last node.
+ dropPoint.ip = new PlacesInsertionPoint({
+ parentId: PlacesUtils.getConcreteItemId(this._resultNode),
+ parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode),
+ orientation: Ci.nsITreeView.DROP_BEFORE,
+ });
+ dropPoint.beforeIndex = -1;
+ }
+
+ return dropPoint;
+ },
+
+ _setTimer: function PT_setTimer(aTime) {
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT);
+ return timer;
+ },
+
+ notify: function PT_notify(aTimer) {
+ if (aTimer == this._updateNodesVisibilityTimer) {
+ this._updateNodesVisibilityTimer = null;
+ this._updateNodesVisibilityTimerCallback();
+ } else if (aTimer == this._ibTimer) {
+ // * Timer to turn off indicator bar.
+ this._dropIndicator.collapsed = true;
+ this._ibTimer = null;
+ } else if (aTimer == this._overFolder.openTimer) {
+ // * Timer to open a menubutton that's being dragged over.
+ // Set the autoopen attribute on the folder's menupopup so that
+ // the menu will automatically close when the mouse drags off of it.
+ this._overFolder.elt.menupopup.setAttribute("autoopened", "true");
+ this._overFolder.elt.open = true;
+ this._overFolder.openTimer = null;
+ } else if (aTimer == this._overFolder.closeTimer) {
+ // * Timer to close a menubutton that's been dragged off of.
+ // Close the menubutton if we are not dragging over it or one of
+ // its children. The autoopened attribute will let the menu know to
+ // close later if the menu is still being dragged over.
+ let currentPlacesNode = PlacesControllerDragHelper.currentDropTarget;
+ let inHierarchy = false;
+ while (currentPlacesNode) {
+ if (currentPlacesNode == this._rootElt) {
+ inHierarchy = true;
+ break;
+ }
+ currentPlacesNode = currentPlacesNode.parentNode;
+ }
+ // The _clearOverFolder() function will close the menu for
+ // _overFolder.elt. So null it out if we don't want to close it.
+ if (inHierarchy) {
+ this._overFolder.elt = null;
+ }
+
+ // Clear out the folder and all associated timers.
+ this._clearOverFolder();
+ }
+ },
+
+ _onMouseOver: function PT__onMouseOver(aEvent) {
+ let button = aEvent.target;
+ if (
+ button.parentNode == this._rootElt &&
+ button._placesNode &&
+ PlacesUtils.nodeIsURI(button._placesNode)
+ ) {
+ window.XULBrowserWindow.setOverLink(aEvent.target._placesNode.uri);
+ }
+ },
+
+ _onMouseOut: function PT__onMouseOut(aEvent) {
+ window.XULBrowserWindow.setOverLink("");
+ },
+
+ _onMouseDown: function PT__onMouseDown(aEvent) {
+ let target = aEvent.target;
+ if (
+ aEvent.button == 0 &&
+ target.localName == "toolbarbutton" &&
+ target.getAttribute("type") == "menu"
+ ) {
+ let modifKey = aEvent.shiftKey || aEvent.getModifierState("Accel");
+ if (modifKey) {
+ // Do not open the popup since BEH_onClick is about to
+ // open all child uri nodes in tabs.
+ this._allowPopupShowing = false;
+ }
+ }
+ },
+
+ _cleanupDragDetails: function PT__cleanupDragDetails() {
+ // Called on dragend and drop.
+ PlacesControllerDragHelper.currentDropTarget = null;
+ this._draggedElt = null;
+ if (this._ibTimer) {
+ this._ibTimer.cancel();
+ }
+
+ this._dropIndicator.collapsed = true;
+ },
+
+ _onDragStart: function PT__onDragStart(aEvent) {
+ // Sub menus have their own d&d handlers.
+ let draggedElt = aEvent.target;
+ if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode) {
+ return;
+ }
+
+ if (
+ draggedElt.localName == "toolbarbutton" &&
+ draggedElt.getAttribute("type") == "menu"
+ ) {
+ // If the drag gesture on a container is toward down we open instead
+ // of dragging.
+ let translateY = this._cachedMouseMoveEvent.clientY - aEvent.clientY;
+ let translateX = this._cachedMouseMoveEvent.clientX - aEvent.clientX;
+ if (translateY >= Math.abs(translateX / 2)) {
+ // Don't start the drag.
+ aEvent.preventDefault();
+ // Open the menu.
+ draggedElt.open = true;
+ return;
+ }
+
+ // If the menu is open, close it.
+ if (draggedElt.open) {
+ draggedElt.menupopup.hidePopup();
+ draggedElt.open = false;
+ }
+ }
+
+ // Activate the view and cache the dragged element.
+ this._draggedElt = draggedElt._placesNode;
+ this._rootElt.focus();
+
+ this._controller.setDataTransfer(aEvent);
+ aEvent.stopPropagation();
+ },
+
+ _onDragOver: function PT__onDragOver(aEvent) {
+ // Cache the dataTransfer
+ PlacesControllerDragHelper.currentDropTarget = aEvent.target;
+ let dt = aEvent.dataTransfer;
+
+ let dropPoint = this._getDropPoint(aEvent);
+ if (
+ !dropPoint ||
+ !dropPoint.ip ||
+ !PlacesControllerDragHelper.canDrop(dropPoint.ip, dt)
+ ) {
+ this._dropIndicator.collapsed = true;
+ aEvent.stopPropagation();
+ return;
+ }
+
+ if (this._ibTimer) {
+ this._ibTimer.cancel();
+ this._ibTimer = null;
+ }
+
+ if (dropPoint.folderElt || aEvent.originalTarget == this._chevron) {
+ // Dropping over a menubutton or chevron button.
+ // Set styles and timer to open relative menupopup.
+ let overElt = dropPoint.folderElt || this._chevron;
+ if (this._overFolder.elt != overElt) {
+ this._clearOverFolder();
+ this._overFolder.elt = overElt;
+ this._overFolder.openTimer = this._setTimer(this._overFolder.hoverTime);
+ }
+ if (!this._overFolder.elt.hasAttribute("dragover")) {
+ this._overFolder.elt.setAttribute("dragover", "true");
+ }
+
+ this._dropIndicator.collapsed = true;
+ } else {
+ // Dragging over a normal toolbarbutton,
+ // show indicator bar and move it to the appropriate drop point.
+ let ind = this._dropIndicator;
+ ind.parentNode.collapsed = false;
+ let halfInd = ind.clientWidth / 2;
+ let translateX;
+ if (this.isRTL) {
+ halfInd = Math.ceil(halfInd);
+ translateX = 0 - this._rootElt.getBoundingClientRect().right - halfInd;
+ if (this._rootElt.firstElementChild) {
+ if (dropPoint.beforeIndex == -1) {
+ translateX += this._rootElt.lastElementChild.getBoundingClientRect()
+ .left;
+ } else {
+ translateX += this._rootElt.children[
+ dropPoint.beforeIndex
+ ].getBoundingClientRect().right;
+ }
+ }
+ } else {
+ halfInd = Math.floor(halfInd);
+ translateX = 0 - this._rootElt.getBoundingClientRect().left + halfInd;
+ if (this._rootElt.firstElementChild) {
+ if (dropPoint.beforeIndex == -1) {
+ translateX += this._rootElt.lastElementChild.getBoundingClientRect()
+ .right;
+ } else {
+ translateX += this._rootElt.children[
+ dropPoint.beforeIndex
+ ].getBoundingClientRect().left;
+ }
+ }
+ }
+
+ ind.style.transform = "translate(" + Math.round(translateX) + "px)";
+ ind.style.marginInlineStart = -ind.clientWidth + "px";
+ ind.collapsed = false;
+
+ // Clear out old folder information.
+ this._clearOverFolder();
+ }
+
+ aEvent.preventDefault();
+ aEvent.stopPropagation();
+ },
+
+ _onDrop: function PT__onDrop(aEvent) {
+ PlacesControllerDragHelper.currentDropTarget = aEvent.target;
+
+ let dropPoint = this._getDropPoint(aEvent);
+ if (dropPoint && dropPoint.ip) {
+ PlacesControllerDragHelper.onDrop(
+ dropPoint.ip,
+ aEvent.dataTransfer
+ ).catch(Cu.reportError);
+ aEvent.preventDefault();
+ }
+
+ this._cleanupDragDetails();
+ aEvent.stopPropagation();
+ },
+
+ _onDragExit: function PT__onDragExit(aEvent) {
+ PlacesControllerDragHelper.currentDropTarget = null;
+
+ // Set timer to turn off indicator bar (if we turn it off
+ // here, dragenter might be called immediately after, creating
+ // flicker).
+ if (this._ibTimer) {
+ this._ibTimer.cancel();
+ }
+ this._ibTimer = this._setTimer(10);
+
+ // If we hovered over a folder, close it now.
+ if (this._overFolder.elt) {
+ this._overFolder.closeTimer = this._setTimer(this._overFolder.hoverTime);
+ }
+ },
+
+ _onDragEnd: function PT_onDragEnd(aEvent) {
+ this._cleanupDragDetails();
+ },
+
+ _onPopupShowing: function PT__onPopupShowing(aEvent) {
+ if (!this._allowPopupShowing) {
+ this._allowPopupShowing = true;
+ aEvent.preventDefault();
+ return;
+ }
+
+ let parent = aEvent.target.parentNode;
+ if (parent.localName == "toolbarbutton") {
+ this._openedMenuButton = parent;
+ }
+
+ PlacesViewBase.prototype._onPopupShowing.apply(this, arguments);
+ },
+
+ _onPopupHidden: function PT__onPopupHidden(aEvent) {
+ let popup = aEvent.target;
+ let placesNode = popup._placesNode;
+ // Avoid handling popuphidden of inner views
+ if (
+ placesNode &&
+ PlacesUIUtils.getViewForNode(popup) == this &&
+ // UI performance: folder queries are cheap, keep the resultnode open
+ // so we don't rebuild its contents whenever the popup is reopened.
+ !PlacesUtils.nodeIsFolder(placesNode)
+ ) {
+ placesNode.containerOpen = false;
+ }
+
+ let parent = popup.parentNode;
+ if (parent.localName == "toolbarbutton") {
+ this._openedMenuButton = null;
+ // Clear the dragover attribute if present, if we are dragging into a
+ // folder in the hierachy of current opened popup we don't clear
+ // this attribute on clearOverFolder. See Notify for closeTimer.
+ if (parent.hasAttribute("dragover")) {
+ parent.removeAttribute("dragover");
+ }
+ }
+ },
+
+ _onMouseMove: function PT__onMouseMove(aEvent) {
+ // Used in dragStart to prevent dragging folders when dragging down.
+ this._cachedMouseMoveEvent = aEvent;
+
+ if (
+ this._openedMenuButton == null ||
+ PlacesControllerDragHelper.getSession()
+ ) {
+ return;
+ }
+
+ let target = aEvent.originalTarget;
+ if (
+ this._openedMenuButton != target &&
+ target.localName == "toolbarbutton" &&
+ target.type == "menu"
+ ) {
+ this._openedMenuButton.open = false;
+ target.open = true;
+ }
+ },
+};
+
+/**
+ * View for Places menus. This object should be created during the first
+ * popupshowing that's dispatched on the menu.
+ */
+function PlacesMenu(aPopupShowingEvent, aPlace, aOptions) {
+ this._rootElt = aPopupShowingEvent.target; // <menupopup>
+ this._viewElt = this._rootElt.parentNode; // <menu>
+ this._viewElt._placesView = this;
+ this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true);
+ this._addEventListeners(window, ["unload"], false);
+
+ if (AppConstants.platform === "macosx") {
+ // Must walk up to support views in sub-menus, like Bookmarks Toolbar menu.
+ for (let elt = this._viewElt.parentNode; elt; elt = elt.parentNode) {
+ if (elt.localName == "menubar") {
+ this._nativeView = true;
+ break;
+ }
+ }
+ }
+
+ PlacesViewBase.call(this, aPlace, aOptions);
+ this._onPopupShowing(aPopupShowingEvent);
+}
+
+PlacesMenu.prototype = {
+ __proto__: PlacesViewBase.prototype,
+
+ _removeChild: function PM_removeChild(aChild) {
+ PlacesViewBase.prototype._removeChild.apply(this, arguments);
+ },
+
+ uninit: function PM_uninit() {
+ this._removeEventListeners(
+ this._rootElt,
+ ["popupshowing", "popuphidden"],
+ true
+ );
+ this._removeEventListeners(window, ["unload"], false);
+
+ PlacesViewBase.prototype.uninit.apply(this, arguments);
+ },
+
+ handleEvent: function PM_handleEvent(aEvent) {
+ switch (aEvent.type) {
+ case "unload":
+ this.uninit();
+ break;
+ case "popupshowing":
+ this._onPopupShowing(aEvent);
+ break;
+ case "popuphidden":
+ this._onPopupHidden(aEvent);
+ break;
+ }
+ },
+
+ _onPopupHidden: function PM__onPopupHidden(aEvent) {
+ // Avoid handling popuphidden of inner views.
+ let popup = aEvent.originalTarget;
+ let placesNode = popup._placesNode;
+ if (!placesNode || PlacesUIUtils.getViewForNode(popup) != this) {
+ return;
+ }
+
+ // UI performance: folder queries are cheap, keep the resultnode open
+ // so we don't rebuild its contents whenever the popup is reopened.
+ if (!PlacesUtils.nodeIsFolder(placesNode)) {
+ placesNode.containerOpen = false;
+ }
+
+ // The autoopened attribute is set for folders which have been
+ // automatically opened when dragged over. Turn off this attribute
+ // when the folder closes because it is no longer applicable.
+ popup.removeAttribute("autoopened");
+ popup.removeAttribute("dragstart");
+ },
+};
+
+function PlacesPanelMenuView(aPlace, aViewId, aRootId, aOptions) {
+ this._viewElt = document.getElementById(aViewId);
+ this._rootElt = document.getElementById(aRootId);
+ this._viewElt._placesView = this;
+ this.options = aOptions;
+
+ PlacesViewBase.call(this, aPlace, aOptions);
+}
+
+PlacesPanelMenuView.prototype = {
+ __proto__: PlacesViewBase.prototype,
+
+ uninit: function PAMV_uninit() {
+ PlacesViewBase.prototype.uninit.apply(this, arguments);
+ },
+
+ _insertNewItem: function PAMV__insertNewItem(
+ aChild,
+ aInsertionNode,
+ aBefore = null
+ ) {
+ this._domNodes.delete(aChild);
+
+ let type = aChild.type;
+ let button;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ button = document.createXULElement("toolbarseparator");
+ button.setAttribute("class", "small-separator");
+ } else {
+ button = document.createXULElement("toolbarbutton");
+ button.className = "bookmark-item";
+ if (typeof this.options.extraClasses.entry == "string") {
+ button.classList.add(this.options.extraClasses.entry);
+ }
+ button.setAttribute("label", aChild.title || "");
+ let icon = aChild.icon;
+ if (icon) {
+ button.setAttribute("image", icon);
+ }
+
+ if (PlacesUtils.containerTypes.includes(type)) {
+ button.setAttribute("container", "true");
+
+ if (PlacesUtils.nodeIsQuery(aChild)) {
+ button.setAttribute("query", "true");
+ if (PlacesUtils.nodeIsTagQuery(aChild)) {
+ button.setAttribute("tagContainer", "true");
+ }
+ }
+ } else if (PlacesUtils.nodeIsURI(aChild)) {
+ button.setAttribute(
+ "scheme",
+ PlacesUIUtils.guessUrlSchemeForUI(aChild.uri)
+ );
+ }
+ }
+
+ button._placesNode = aChild;
+ if (!this._domNodes.has(aChild)) {
+ this._domNodes.set(aChild, button);
+ }
+
+ aInsertionNode.insertBefore(button, aBefore);
+ return button;
+ },
+
+ nodeInserted: function PAMV_nodeInserted(
+ aParentPlacesNode,
+ aPlacesNode,
+ aIndex
+ ) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (parentElt != this._rootElt) {
+ return;
+ }
+
+ let children = this._rootElt.children;
+ this._insertNewItem(
+ aPlacesNode,
+ this._rootElt,
+ aIndex < children.length ? children[aIndex] : null
+ );
+ },
+
+ nodeRemoved: function PAMV_nodeRemoved(
+ aParentPlacesNode,
+ aPlacesNode,
+ aIndex
+ ) {
+ let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode);
+ if (parentElt != this._rootElt) {
+ return;
+ }
+
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ this._removeChild(elt);
+ },
+
+ nodeMoved: function PAMV_nodeMoved(
+ aPlacesNode,
+ aOldParentPlacesNode,
+ aOldIndex,
+ aNewParentPlacesNode,
+ aNewIndex
+ ) {
+ let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode);
+ if (parentElt != this._rootElt) {
+ return;
+ }
+
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ this._removeChild(elt);
+ this._rootElt.insertBefore(elt, this._rootElt.children[aNewIndex]);
+ },
+
+ nodeTitleChanged: function PAMV_nodeTitleChanged(aPlacesNode, aNewTitle) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+
+ // There's no UI representation for the root node.
+ if (elt == this._rootElt) {
+ return;
+ }
+
+ PlacesViewBase.prototype.nodeTitleChanged.apply(this, arguments);
+ },
+
+ invalidateContainer: function PAMV_invalidateContainer(aPlacesNode) {
+ let elt = this._getDOMNodeForPlacesNode(aPlacesNode);
+ if (elt != this._rootElt) {
+ return;
+ }
+
+ // Container is the toolbar itself.
+ while (this._rootElt.hasChildNodes()) {
+ this._rootElt.firstChild.remove();
+ }
+
+ let fragment = document.createDocumentFragment();
+ for (let i = 0; i < this._resultNode.childCount; ++i) {
+ this._insertNewItem(this._resultNode.getChild(i), fragment);
+ }
+ this._rootElt.appendChild(fragment);
+ },
+};
+
+this.PlacesPanelview = class extends PlacesViewBase {
+ constructor(container, panelview, place, options = {}) {
+ options.rootElt = container;
+ options.viewElt = panelview;
+ super(place, options);
+ this._viewElt._placesView = this;
+ // We're simulating a popup show, because a panelview may only be shown when
+ // its containing popup is already shown.
+ this._onPopupShowing({ originalTarget: this._rootElt });
+ this._addEventListeners(window, ["unload"]);
+ this._rootElt.setAttribute("context", "placesContext");
+ }
+
+ get events() {
+ if (this._events) {
+ return this._events;
+ }
+ return (this._events = [
+ "click",
+ "command",
+ "dragend",
+ "dragstart",
+ "ViewHiding",
+ "ViewShown",
+ ]);
+ }
+
+ handleEvent(event) {
+ switch (event.type) {
+ case "click":
+ // For middle clicks, fall through to the command handler.
+ if (event.button != 1) {
+ break;
+ }
+ // fall through
+ case "command":
+ this._onCommand(event);
+ break;
+ case "dragend":
+ this._onDragEnd(event);
+ break;
+ case "dragstart":
+ this._onDragStart(event);
+ break;
+ case "unload":
+ this.uninit(event);
+ break;
+ case "ViewHiding":
+ this._onPopupHidden(event);
+ break;
+ case "ViewShown":
+ this._onViewShown(event);
+ break;
+ }
+ }
+
+ _onCommand(event) {
+ event = getRootEvent(event);
+ let button = event.originalTarget;
+ if (!button._placesNode) {
+ return;
+ }
+
+ let modifKey =
+ AppConstants.platform === "macosx" ? event.metaKey : event.ctrlKey;
+ if (!PlacesUIUtils.openInTabClosesMenu && modifKey) {
+ // If 'Recent Bookmarks' in Bookmarks Panel.
+ if (button.parentNode.id == "panelMenu_bookmarksMenu") {
+ button.setAttribute("closemenu", "none");
+ }
+ } else {
+ button.removeAttribute("closemenu");
+ }
+ PlacesUIUtils.openNodeWithEvent(button._placesNode, event);
+ // Unlike left-click, middle-click requires manual menu closing.
+ if (
+ button.parentNode.id != "panelMenu_bookmarksMenu" ||
+ (event.type == "click" &&
+ event.button == 1 &&
+ PlacesUIUtils.openInTabClosesMenu)
+ ) {
+ this.panelMultiView.closest("panel").hidePopup();
+ }
+ }
+
+ _onDragEnd() {
+ this._draggedElt = null;
+ }
+
+ _onDragStart(event) {
+ let draggedElt = event.originalTarget;
+ if (draggedElt.parentNode != this._rootElt || !draggedElt._placesNode) {
+ return;
+ }
+
+ // Activate the view and cache the dragged element.
+ this._draggedElt = draggedElt._placesNode;
+ this._rootElt.focus();
+
+ this._controller.setDataTransfer(event);
+ event.stopPropagation();
+ }
+
+ uninit(event) {
+ this._removeEventListeners(this.panelMultiView, this.events);
+ this._removeEventListeners(window, ["unload"]);
+ delete this.panelMultiView;
+ super.uninit(event);
+ }
+
+ _createDOMNodeForPlacesNode(placesNode) {
+ this._domNodes.delete(placesNode);
+
+ let element;
+ let type = placesNode.type;
+ if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) {
+ element = document.createXULElement("toolbarseparator");
+ } else {
+ if (type != Ci.nsINavHistoryResultNode.RESULT_TYPE_URI) {
+ throw new Error("Unexpected node");
+ }
+
+ element = document.createXULElement("toolbarbutton");
+ element.classList.add(
+ "subviewbutton",
+ "subviewbutton-iconic",
+ "bookmark-item"
+ );
+ element.setAttribute(
+ "scheme",
+ PlacesUIUtils.guessUrlSchemeForUI(placesNode.uri)
+ );
+ element.setAttribute("label", PlacesUIUtils.getBestTitle(placesNode));
+
+ let icon = placesNode.icon;
+ if (icon) {
+ element.setAttribute("image", icon);
+ }
+ }
+
+ element._placesNode = placesNode;
+ if (!this._domNodes.has(placesNode)) {
+ this._domNodes.set(placesNode, element);
+ }
+
+ return element;
+ }
+
+ _setEmptyPopupStatus(panelview, empty = false) {
+ if (!panelview._emptyMenuitem) {
+ let label = PlacesUIUtils.getString("bookmarksMenuEmptyFolder");
+ panelview._emptyMenuitem = document.createXULElement("toolbarbutton");
+ panelview._emptyMenuitem.setAttribute("label", label);
+ panelview._emptyMenuitem.setAttribute("disabled", true);
+ panelview._emptyMenuitem.className = "subviewbutton";
+ if (typeof this.options.extraClasses.entry == "string") {
+ panelview._emptyMenuitem.classList.add(this.options.extraClasses.entry);
+ }
+ }
+
+ if (empty) {
+ panelview.setAttribute("emptyplacesresult", "true");
+ // Don't add the menuitem if there is static content.
+ // We also support external usage for custom crafted panels - which'll have
+ // no markers present.
+ if (
+ !panelview._startMarker ||
+ (!panelview._startMarker.previousElementSibling &&
+ !panelview._endMarker.nextElementSibling)
+ ) {
+ panelview.insertBefore(panelview._emptyMenuitem, panelview._endMarker);
+ }
+ } else {
+ panelview.removeAttribute("emptyplacesresult");
+ try {
+ panelview.removeChild(panelview._emptyMenuitem);
+ } catch (ex) {}
+ }
+ }
+
+ _isPopupOpen() {
+ return PanelView.forNode(this._viewElt).active;
+ }
+
+ _onPopupHidden(event) {
+ let panelview = event.originalTarget;
+ let placesNode = panelview._placesNode;
+ // Avoid handling ViewHiding of inner views
+ if (
+ placesNode &&
+ PlacesUIUtils.getViewForNode(panelview) == this &&
+ // UI performance: folder queries are cheap, keep the resultnode open
+ // so we don't rebuild its contents whenever the popup is reopened.
+ !PlacesUtils.nodeIsFolder(placesNode)
+ ) {
+ placesNode.containerOpen = false;
+ }
+ }
+
+ _onPopupShowing(event) {
+ // If the event came from the root element, this is the first time
+ // we ever get here.
+ if (event.originalTarget == this._rootElt) {
+ // Start listening for events from all panels inside the panelmultiview.
+ this.panelMultiView = this._viewElt.panelMultiView;
+ this._addEventListeners(this.panelMultiView, this.events);
+ }
+ super._onPopupShowing(event);
+ }
+
+ _onViewShown(event) {
+ if (event.originalTarget != this._viewElt) {
+ return;
+ }
+
+ // Because PanelMultiView reparents the panelview internally, the controller
+ // may get lost. In that case we'll append it again, because we certainly
+ // need it later!
+ if (!this.controllers.getControllerCount() && this._controller) {
+ this.controllers.appendController(this._controller);
+ }
+ }
+};