/* 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 all the views. * It should not be instanced directly, use a derived class instead. */ class PlacesViewBase { /** * @param {string} placesUrl * The query string associated with the view. * @param {DOMElement} rootElt * The root element for the view. * @param {DOMElement} viewElt * The view element. */ constructor(placesUrl, rootElt, viewElt) { this._rootElt = rootElt; this._viewElt = viewElt; let appendClass = this._rootElt.getAttribute("appendclasstochildren"); if (appendClass) { this._appendClassToChildren = appendClass; } // Do initialization in subclass now that `this` exists. this._init?.(); this._controller = new PlacesController(this); this.place = placesUrl; this._viewElt.controllers.appendController(this._controller); } // The xul element that holds the entire view. _viewElt = null; 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; static interfaces = [ Ci.nsINavHistoryResultObserver, Ci.nsISupportsWeakReference, ]; 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; } } /** * Gets the DOM node used for the given places node. * * @param {object} aPlacesNode * a places result node. * @param {boolean} aAllowMissing * whether the node may be missing * @returns {object|null} The associated DOM node. * @throws if there is no DOM node set for aPlacesNode. */ _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 singleClickOpens() { return true; } 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. let popupNode = PlacesUIUtils.lastContextMenuTriggerNode; if (popupNode && (popupNode == "menupopup" || !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 popupNode = PlacesUIUtils.lastContextMenuTriggerNode; if ( !popupNode._placesNode || popupNode._placesNode == this._resultNode || popupNode._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({ parentGuid: PlacesUtils.getConcreteItemGuid(container), index, orientation, tagName, }); } buildContextMenu(aPopup) { this._contextMenuShown = aPopup; window.updateCommands("places"); // Ensure that an existing "Show Other Bookmarks" item is removed before adding it // again. let existingOtherBookmarksItem = aPopup.querySelector( "#show-other-bookmarks_PersonalToolbar" ); existingOtherBookmarksItem?.remove(); let manageBookmarksMenu = aPopup.querySelector( "#placesContext_showAllBookmarks" ); // Add the View menu for the Bookmarks Toolbar and "Show Other Bookmarks" menu item // if the click originated from the Bookmarks Toolbar. let existingSubmenu = aPopup.querySelector("#toggle_PersonalToolbar"); existingSubmenu?.remove(); let bookmarksToolbar = document.getElementById("PersonalToolbar"); if (bookmarksToolbar?.contains(aPopup.triggerNode)) { manageBookmarksMenu.removeAttribute("hidden"); let menu = BookmarkingUI.buildBookmarksToolbarSubmenu(bookmarksToolbar); aPopup.insertBefore(menu, manageBookmarksMenu); if ( aPopup.triggerNode.id === "OtherBookmarks" || aPopup.triggerNode.id === "PlacesChevron" || aPopup.triggerNode.id === "PlacesToolbarItems" || aPopup.triggerNode.parentNode.id === "PlacesToolbarItems" ) { let otherBookmarksMenuItem = BookmarkingUI.buildShowOtherBookmarksMenuItem(); if (otherBookmarksMenuItem) { aPopup.insertBefore(otherBookmarksMenuItem, menu.nextElementSibling); } } } else { manageBookmarksMenu.setAttribute("hidden", "true"); } return this.controller.buildContextMenu(aPopup); } 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(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(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(aChild) { aChild.remove(); } _setEmptyPopupStatus(aPopup, aEmpty) { if (!aPopup._emptyMenuitem) { aPopup._emptyMenuitem = document.createXULElement("menuitem"); aPopup._emptyMenuitem.setAttribute("disabled", true); aPopup._emptyMenuitem.className = "bookmark-item"; document.l10n.setAttributes( aPopup._emptyMenuitem, "places-empty-bookmarks-folder" ); if (this._appendClassToChildren) { aPopup._emptyMenuitem.classList.add(this._appendClassToChildren); } } 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(aPlacesNode) { this._domNodes.delete(aPlacesNode); let element; let type = aPlacesNode.type; if (type == Ci.nsINavHistoryResultNode.RESULT_TYPE_SEPARATOR) { element = document.createXULElement("menuseparator"); } 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 (this._appendClassToChildren) { element.classList.add(this._appendClassToChildren); } 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(aNewChild, aInsertionNode, aBefore = null) { let element = this._createDOMNodeForPlacesNode(aNewChild); if (element.localName == "menuitem" || element.localName == "menu") { if (this._appendClassToChildren) { element.classList.add(this._appendClassToChildren); } } aInsertionNode.insertBefore(element, aBefore); return element; } 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(aPlacesNode, aURIString) { let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true); // There's no DOM node, thus there's nothing to be done when the URI changes. if (!elt) { return; } // Here we need the . if (elt.localName == "menupopup") { elt = elt.parentNode; } elt.setAttribute( "scheme", PlacesUIUtils.guessUrlSchemeForUI(aPlacesNode.uri) ); } nodeIconChanged(aPlacesNode) { let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true); // There's no UI representation for the root node, or there's no DOM node, // thus there's nothing to be done when the icon changes. if (!elt || elt == this._rootElt) { return; } // Here we need the . 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(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 . 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(aParentPlacesNode, aPlacesNode, aIndex) { let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); let elt = this._getDOMNodeForPlacesNode(aPlacesNode); // Here we need the . if (elt.localName == "menupopup") { elt = elt.parentNode; } if (parentElt._built) { parentElt.removeChild(elt); // Figure out if we need to show the "" 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. skipHistoryDetailsNotifications = true; nodeHistoryDetailsChanged() {} nodeTagsChanged() {} nodeDateAddedChanged() {} nodeLastModifiedChanged() {} nodeKeywordChanged() {} sortingChanged() {} batching() {} 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( 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 . 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(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 * The element to check. * @returns {boolean} */ _isPopupOpen(elt) { return !!elt.parentNode.open; } 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() { 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 {object} aPopup * a Places popup. */ _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) { // 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 (this._appendClassToChildren) { aPopup._endOptOpenAllInTabs.classList.add(this._appendClassToChildren); } aPopup._endOptOpenAllInTabs.setAttribute( "oncommand", "PlacesUIUtils.openMultipleLinksInTabs(this.parentNode._placesNode, event, " + "PlacesUIUtils.getViewForNode(this));" ); aPopup._endOptOpenAllInTabs.setAttribute( "label", gNavigatorBundle.getString("menuOpenAllInTabs.label") ); aPopup.appendChild(aPopup._endOptOpenAllInTabs); } } _ensureMarkers(aPopup) { if (aPopup._startMarker) { return; } // Places nodes are appended between _startMarker and _endMarker, that // are hidden menuseparators. By default they take the whole panel... aPopup._startMarker = document.createXULElement("menuseparator"); aPopup._startMarker.hidden = true; aPopup.insertBefore(aPopup._startMarker, aPopup.firstElementChild); aPopup._endMarker = document.createXULElement("menuseparator"); aPopup._endMarker.hidden = true; aPopup.appendChild(aPopup._endMarker); // ...but there can be static content before or after the places nodes, thus // we move the markers to the right position, by checking for static content // at the beginning of the view, and for an element with "afterplacescontent" // attribute. // TODO: In the future we should just use a container element. let firstNonStaticNodeFound = false; for (let child of aPopup.children) { if (child.hasAttribute("afterplacescontent")) { aPopup.insertBefore(aPopup._endMarker, child); break; } // Check for the first Places node that is not a view. if (child._placesNode && !child._placesView && !firstNonStaticNodeFound) { firstNonStaticNodeFound = true; aPopup.insertBefore(aPopup._startMarker, child); } } if (!firstNonStaticNodeFound) { // Just put the start marker before the end marker. aPopup.insertBefore(aPopup._startMarker, aPopup._endMarker); } } _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(aObject, aEventNames, aCapturing = false) { for (let i = 0; i < aEventNames.length; i++) { aObject.addEventListener(aEventNames[i], this, aCapturing); } } _removeEventListeners(aObject, aEventNames, aCapturing = false) { for (let i = 0; i < aEventNames.length; i++) { aObject.removeEventListener(aEventNames[i], this, aCapturing); } } } /** * Toolbar View implementation. */ class PlacesToolbar extends PlacesViewBase { constructor(placesUrl, rootElt, viewElt) { let startTime = Date.now(); super(placesUrl, rootElt, 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 ); } Services.telemetry .getHistogramById("FX_BOOKMARKS_TOOLBAR_INIT_MS") .add(Date.now() - startTime); } // Called by PlacesViewBase so we can init properties that class // initialization depends on. PlacesViewBase will assign this.place which // calls which sets `this.result` through its places observer, which changes // containerOpen, which calls invalidateContainer(), which calls rebuild(), // which needs `_overFolder`, `_chevronPopup` and various other things to // exist. _init() { this._overFolder = { elt: null, openTimer: null, hoverTime: 350, closeTimer: null, }; // Add some smart getters for our elements. let thisView = this; [ ["_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._updatingNodesVisibility = false; } _cbEvents = [ "dragstart", "dragover", "dragleave", "dragend", "drop", "mousemove", "mouseover", "mouseout", "mousedown", ]; QueryInterface = ChromeUtils.generateQI([ "nsINamed", "nsITimerCallback", ...PlacesViewBase.interfaces, ]); 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(); } super.uninit(); } _openedMenuButton = null; _allowPopupShowing = true; promiseRebuilt() { return this._rebuilding?.promise; } 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); } await new Promise(resolve => window.requestAnimationFrame(resolve)); if (!this._isAlive) { return; } 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().catch(console.error); } _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() { // 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(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(aEvent) { if (aEvent.target != this._otherBookmarksPopup) { return; } if (!this._otherBookmarks._placesView) { this._otherBookmarks._placesView = new PlacesMenu( aEvent, "place:parent=" + PlacesUtils.bookmarks.unfiledGuid ); } } 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 "dragleave": this._onDragLeave(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(aEvent) { // Ignore events not aimed at ourselves, as well as purely vertical ones: return aEvent.target == aEvent.currentTarget && aEvent.detail > 0; } _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() { this.updateNodesVisibility(); this._chevron.collapsed = true; } 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.removeProperty("visibility"); } } // 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(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; } super.nodeInserted(aParentPlacesNode, aPlacesNode, aIndex); } 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 . 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; } super.nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex); } 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 . 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; } super.nodeMoved( aPlacesNode, aOldParentPlacesNode, aOldIndex, aNewParentPlacesNode, aNewIndex ); } 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; } super.nodeTitleChanged(aPlacesNode, aNewTitle); // Here we need the . 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(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. let instance = (this._rebuildingInstance = {}); if (!this._rebuilding) { this._rebuilding = PromiseUtils.defer(); } this._rebuild() .catch(console.error) .finally(() => { if (instance == this._rebuildingInstance) { this._rebuilding.resolve(); this._rebuilding = null; } }); return; } super.invalidateContainer(aPlacesNode); } _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. * * @param {object} aEvent * The associated event. * @returns {object} * - 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(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({ 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({ 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({ 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({ 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({ 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({ parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode), orientation: Ci.nsITreeView.DROP_BEFORE, }); dropPoint.beforeIndex = -1; } return dropPoint; } _setTimer(aTime) { let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); timer.initWithCallback(this, aTime, timer.TYPE_ONE_SHOT); return timer; } get name() { return "PlacesToolbar"; } notify(aTimer) { if (aTimer == this._updateNodesVisibilityTimer) { this._updateNodesVisibilityTimer = null; this._updateNodesVisibilityTimerCallback(); } 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(aEvent) { let button = aEvent.target; if ( button.parentNode == this._rootElt && button._placesNode && PlacesUtils.nodeIsURI(button._placesNode) ) { window.XULBrowserWindow.setOverLink(aEvent.target._placesNode.uri); } } _onMouseOut(aEvent) { window.XULBrowserWindow.setOverLink(""); } _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; } } if (target._placesNode?.uri) { PlacesUIUtils.setupSpeculativeConnection(target._placesNode.uri, window); } } _cleanupDragDetails() { // Called on dragend and drop. PlacesControllerDragHelper.currentDropTarget = null; this._draggedElt = null; this._dropIndicator.collapsed = true; } _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(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 (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(aEvent) { PlacesControllerDragHelper.currentDropTarget = aEvent.target; let dropPoint = this._getDropPoint(aEvent); if (dropPoint && dropPoint.ip) { PlacesControllerDragHelper.onDrop( dropPoint.ip, aEvent.dataTransfer ).catch(console.error); aEvent.preventDefault(); } this._cleanupDragDetails(); aEvent.stopPropagation(); } _onDragLeave(aEvent) { PlacesControllerDragHelper.currentDropTarget = null; this._dropIndicator.collapsed = true; // If we hovered over a folder, close it now. if (this._overFolder.elt) { this._overFolder.closeTimer = this._setTimer(this._overFolder.hoverTime); } } _onDragEnd(aEvent) { this._cleanupDragDetails(); } _onPopupShowing(aEvent) { if (!this._allowPopupShowing) { this._allowPopupShowing = true; aEvent.preventDefault(); return; } let parent = aEvent.target.parentNode; if (parent.localName == "toolbarbutton") { this._openedMenuButton = parent; } super._onPopupShowing(aEvent); } _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(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. * */ class PlacesMenu extends PlacesViewBase { /** * * @param {Event} popupShowingEvent * The event associated with opening the menu. * @param {string} placesUrl * The query associated with the view on the menu. */ constructor(popupShowingEvent, placesUrl) { super( placesUrl, popupShowingEvent.target, // popupShowingEvent.target.parentNode // ); this._addEventListeners( this._rootElt, ["popupshowing", "popuphidden"], true ); this._addEventListeners(window, ["unload"], false); this._addEventListeners(this._rootElt, ["mousedown"], 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; } } } this._onPopupShowing(popupShowingEvent); } _init() { this._viewElt._placesView = this; } _removeChild(aChild) { super._removeChild(aChild); } uninit() { this._removeEventListeners( this._rootElt, ["popupshowing", "popuphidden"], true ); this._removeEventListeners(window, ["unload"], false); this._removeEventListeners(this._rootElt, ["mousedown"], false); super.uninit(); } handleEvent(aEvent) { switch (aEvent.type) { case "unload": this.uninit(); break; case "popupshowing": this._onPopupShowing(aEvent); break; case "popuphidden": this._onPopupHidden(aEvent); break; case "mousedown": this._onMouseDown(aEvent); break; } } _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"); } // We don't have a facility for catch "mousedown" events on the native // Mac menus because Mac doesn't expose it _onMouseDown(aEvent) { let target = aEvent.target; if (target._placesNode?.uri) { PlacesUIUtils.setupSpeculativeConnection(target._placesNode.uri, window); } } } // This is used from CustomizableWidgets.sys.mjs using a `window` reference, // so we have to expose this on the global. this.PlacesPanelview = class PlacesPanelview extends PlacesViewBase { constructor(placeUrl, rootElt, viewElt) { super(placeUrl, rootElt, viewElt); 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) { panelview._emptyMenuitem = document.createXULElement("toolbarbutton"); panelview._emptyMenuitem.setAttribute("disabled", true); panelview._emptyMenuitem.className = "subviewbutton"; document.l10n.setAttributes( panelview._emptyMenuitem, "places-empty-bookmarks-folder" ); if (this._appendClassToChildren) { panelview._emptyMenuitem.classList.add(this._appendClassToChildren); } } 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); } } };