From 49a83678ba06a52a293e9b70950b8c6a7ce430cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kristijan=20Ribari=C4=87?= Date: Sat, 2 Nov 2024 20:09:34 +0100 Subject: [PATCH] feat(essentials): Add Zen Essentials Toolbar This commit introduces a new "Zen Essentials" toolbar to the sidebar, providing quick access to frequently used features. - The toolbar is based on the `PlacesViewBase` class and uses the `PlacesUtils` API to manage bookmarks. - It includes a dedicated folder for Zen Essentials bookmarks, ensuring they're organized and easily accessible. - The toolbar's visibility can be controlled by the `zen.essentials.enabled` preference. - The toolbar supports drag-and-drop functionality for organizing bookmarks. - The toolbar items are hidden if they are in another workspace. - The toolbar supports hover interactions, highlighting the active bookmark on mouseover. - The toolbar items can be opened in a glance by pressing `Ctrl`, `Alt`, `Shift`, or `Meta` depending on the user's preference. - The bookmark toolbar automatically updates if the workspace changes. --- src/ZenEssentialsToolbar.mjs | 806 +++++++++++++++++++++++++++++++++++ src/ZenGlanceManager.mjs | 32 ++ src/ZenSidebarManager.mjs | 21 + src/ZenWorkspaces.mjs | 225 +++++----- 4 files changed, 982 insertions(+), 102 deletions(-) create mode 100644 src/ZenEssentialsToolbar.mjs diff --git a/src/ZenEssentialsToolbar.mjs b/src/ZenEssentialsToolbar.mjs new file mode 100644 index 0000000..14446a0 --- /dev/null +++ b/src/ZenEssentialsToolbar.mjs @@ -0,0 +1,806 @@ + +class ZenEssentialsToolbar extends PlacesViewBase { + constructor(placesUrl, rootElt, viewElt) { + // We'll initialize the places URL after ensuring the folder exists + super(null, rootElt, viewElt); + // Do initialization of properties that don't depend on Places + this._init(); + this._initPlacesFolder(); + } + + get _accordionHeader() { + return document.getElementById("essentials-accordion-header"); + } + + async _initPlacesFolder() { + try { + const ESSENTIALS_GUID = "pfgqteRgY-Wr"; // Fixed GUID for the folder + + // First try to fetch by GUID since it's more efficient + let folder = await PlacesUtils.bookmarks.fetch(ESSENTIALS_GUID ); + + if (!folder) { + // If not found by GUID, try creating with our specific GUID + // try { + folder = await PlacesUtils.bookmarks.insert({ + type: PlacesUtils.bookmarks.TYPE_FOLDER, + guid: ESSENTIALS_GUID, + title: "Zen Essentials", + parentGuid: PlacesUtils.bookmarks.menuGuid + }); + } + + // Ensure the folder is in the right place with the right title + if (folder.parentGuid !== PlacesUtils.bookmarks.menuGuid || + folder.title !== "Zen Essentials") { + await PlacesUtils.bookmarks.update({ + guid: folder.guid, + title: "Zen Essentials", + parentGuid: PlacesUtils.bookmarks.menuGuid, + index: folder.index + }); + } + + if(!folder) { + console.error("Failed to initialize ZenEssentials folder"); + return; + } + + // Now that we have the folder, set up the places URL + this.place = `place:parent=${folder.guid}`; + + // Initialize view event listeners and setup + this._setupEventListeners(); + + } catch (ex) { + console.error("Failed to initialize ZenEssentials folder:", ex); + } + } + + _updateEssentialsVisibility() { + // Get current preference value + this.isEssentialsEnabled = Services.prefs.getBoolPref("zen.essentials.enabled", false); + + // Update visibility + if (this._rootElt) { + this._rootElt.hidden = !this.isEssentialsEnabled; + } + + if (this._accordionHeader) { + this._accordionHeader.style.display = this.isEssentialsEnabled ? "flex" : "none"; + } + } + + // Called by PlacesViewBase during initialization + _init() { + this._prefObserver = (() => { + this._updateEssentialsVisibility(); + }).bind(this); + + // Get initial preference value and set up observer + Services.prefs.addObserver("zen.essentials.enabled", this._prefObserver); + + // Initialize visibility based on current pref value + this.isEssentialsEnabled = Services.prefs.getBoolPref("zen.essentials.enabled", false); + this._updateEssentialsVisibility(); + this._overFolder = { + elt: null, + openTimer: null, + hoverTime: 350, + closeTimer: null, + }; + + let thisView = this; + [ + ["_dropIndicator", "EssentialsToolbarDropIndicator"], + ].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); + }); + }); + + // Initialize base properties + this._viewElt._placesView = this; + this._dragRoot = this._viewElt; + } + + _setupEventListeners() { + // Add standard event listeners + this._addEventListeners(this._dragRoot, this._cbEvents, false); + this._addEventListeners(this._rootElt, ["popupshowing", "popuphidden"], true); + this._addEventListeners(window, ["unload"], false); + } + + _cbEvents = [ + "dragstart", + "dragover", + "dragleave", + "dragend", + "drop", + "mousemove", + "mouseover", + "mouseout", + "mousedown", + ]; + + QueryInterface = ChromeUtils.generateQI([ + "nsINamed", + "nsITimerCallback", + ...PlacesViewBase.interfaces, + ]); + + uninit() { + if (this._prefObserver) { + Services.prefs.removeObserver("zen.essentials.enabled", this._prefObserver); + this._prefObserver = null; + } + if (this._dragRoot) { + this._removeEventListeners(this._dragRoot, this._cbEvents, false); + } + this._removeEventListeners( + this._rootElt, + ["popupshowing", "popuphidden"], + true + ); + this._removeEventListeners(window, ["unload"], false); + + super.uninit(); + } + + _allowPopupShowing = true; + + + + get _isAlive() { + return this._resultNode && this._rootElt; + } + + async _rebuild() { + if (this._overFolder.elt) { + this._clearOverFolder(); + } + + while (this._rootElt.hasChildNodes()) { + this._rootElt.firstChild.remove(); + } + + let cc = this._resultNode.childCount; + if (cc > 0) { + for (let i = 0; i < cc; i++) { + this._insertNewItem(this._resultNode.getChild(i), this._rootElt); + } + } + } + + _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"); + popup.classList.add("toolbar-menupopup"); + 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.hidden = ZenWorkspaces.isBookmarkInAnotherWorkspace(aChild); + button.addEventListener("command", gZenGlanceManager.openGlanceForBookmark.bind(gZenGlanceManager)); + } + } + + 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; + } + + handleEvent(aEvent) { + switch (aEvent.type) { + case "unload": + this.uninit(); + 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."); + } + } + + + + nodeInserted(aParentPlacesNode, aPlacesNode, aIndex) { + let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); + if (parentElt == this._rootElt) { + let children = this._rootElt.children; + if (aIndex > children.length) { + return; + } + + this._insertNewItem( + aPlacesNode, + this._rootElt, + children[aIndex] || null + ); + return; + } + + super.nodeInserted(aParentPlacesNode, aPlacesNode, aIndex); + } + + nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex) { + let parentElt = this._getDOMNodeForPlacesNode(aParentPlacesNode); + if (parentElt == this._rootElt) { + let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true); + if (!elt) { + return; + } + + if (elt.localName == "menupopup") { + elt = elt.parentNode; + } + + this._removeChild(elt); + if (this._resultNode.childCount > this._rootElt.children.length) { + this._insertNewItem( + this._resultNode.getChild(this._rootElt.children.length), + this._rootElt + ); + } + return; + } + + super.nodeRemoved(aParentPlacesNode, aPlacesNode, aIndex); + } + + nodeMoved(aPlacesNode, aOldParentPlacesNode, aOldIndex, aNewParentPlacesNode, aNewIndex) { + let parentElt = this._getDOMNodeForPlacesNode(aNewParentPlacesNode); + if (parentElt == this._rootElt) { + let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true); + if (elt) { + if (elt.localName == "menupopup") { + elt = elt.parentNode; + } + this._removeChild(elt); + } + + this._insertNewItem( + aPlacesNode, + this._rootElt, + this._rootElt.children[aNewIndex] + ); + return; + } + + super.nodeMoved( + aPlacesNode, + aOldParentPlacesNode, + aOldIndex, + aNewParentPlacesNode, + aNewIndex + ); + } + + nodeTitleChanged(aPlacesNode, aNewTitle) { + let elt = this._getDOMNodeForPlacesNode(aPlacesNode, true); + if (!elt || elt == this._rootElt) { + return; + } + + super.nodeTitleChanged(aPlacesNode, aNewTitle); + + if (elt.localName == "menupopup") { + elt = elt.parentNode; + } + } + + 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 = Promise.withResolvers(); + } + 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; + } + } + + /** + * Determines the drop target while dragging over the vertical toolbar. + * + * @param {object} aEvent + * The drag 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.nodeIsFolderOrShortcut(this._resultNode)) { + return null; + } + + let dropPoint = { ip: null, beforeIndex: null, folderElt: null }; + let elt = aEvent.target; + + // If we're not dragging over a child element, handle dropping at the end + if (!elt._placesNode || elt == this._rootElt || elt.localName == "menupopup") { + dropPoint.ip = new PlacesInsertionPoint({ + parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode), + orientation: Ci.nsITreeView.DROP_BEFORE + }); + dropPoint.beforeIndex = -1; + + // Find the closest child based on vertical position + for (let i = 0; i < this._rootElt.children.length; i++) { + let childRect = this._rootElt.children[i].getBoundingClientRect(); + if (aEvent.clientY <= childRect.top) { + dropPoint.beforeIndex = i; + dropPoint.ip.index = i; + break; + } + } + return dropPoint; + } + + // Get target element's position info + let eltRect = elt.getBoundingClientRect(); + let eltIndex = Array.prototype.indexOf.call(this._rootElt.children, elt); + + // Handle dropping on folders + if (PlacesUtils.nodeIsFolderOrShortcut(elt._placesNode) && + !PlacesUIUtils.isFolderReadOnly(elt._placesNode)) { + + // Define drop zones: top 25%, middle 50%, bottom 25% + let topThreshold = eltRect.top + (eltRect.height * 0.25); + let bottomThreshold = eltRect.bottom - (eltRect.height * 0.25); + + if (aEvent.clientY < topThreshold) { + // Drop before folder + dropPoint.ip = new PlacesInsertionPoint({ + parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode), + index: eltIndex, + orientation: Ci.nsITreeView.DROP_BEFORE + }); + dropPoint.beforeIndex = eltIndex; + } else if (aEvent.clientY > bottomThreshold) { + // Drop after 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 { + // Drop inside 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 { + // Handle dropping around non-folder items + let midPoint = eltRect.top + (eltRect.height / 2); + + if (aEvent.clientY < midPoint) { + // Drop before item + dropPoint.ip = new PlacesInsertionPoint({ + parentGuid: PlacesUtils.getConcreteItemGuid(this._resultNode), + index: eltIndex, + orientation: Ci.nsITreeView.DROP_BEFORE + }); + dropPoint.beforeIndex = eltIndex; + } else { + // Drop after item + 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; + } + } + + 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 "ZenEssentialsToolbar"; + } + + notify(aTimer) { + if (aTimer == this._overFolder.openTimer) { + this._overFolder.elt.menupopup.setAttribute("autoopened", "true"); + this._overFolder.elt.open = true; + this._overFolder.openTimer = null; + } else if (aTimer == this._overFolder.closeTimer) { + let currentPlacesNode = PlacesControllerDragHelper.currentDropTarget; + let inHierarchy = false; + while (currentPlacesNode) { + if (currentPlacesNode == this._rootElt) { + inHierarchy = true; + break; + } + currentPlacesNode = currentPlacesNode.parentNode; + } + if (inHierarchy) { + this._overFolder.elt = null; + } + 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() { + 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; + } + } + PlacesUIUtils.maybeSpeculativeConnectOnMouseDown(aEvent); + } + + _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) { + let overElt = dropPoint.folderElt; + 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() { + 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() { + 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.nodeIsFolderOrShortcut(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; + } + } +} \ No newline at end of file diff --git a/src/ZenGlanceManager.mjs b/src/ZenGlanceManager.mjs index 9c401bb..4987d3c 100644 --- a/src/ZenGlanceManager.mjs +++ b/src/ZenGlanceManager.mjs @@ -294,6 +294,38 @@ }); }, 300); } + + openGlanceForBookmark(event) { + const activationMethod = Services.prefs.getStringPref('zen.glance.activation-method', 'ctrl'); + + if (activationMethod === 'ctrl' && !event.ctrlKey) { + return; + } else if (activationMethod === 'alt' && !event.altKey) { + return; + } else if (activationMethod === 'shift' && !event.shiftKey) { + return; + } else if (activationMethod === 'meta' && !event.metaKey) { + return; + }else if (activationMethod === 'mantain' || typeof activationMethod === 'undefined') { + return; + } + + event.preventDefault(); + event.stopPropagation(); + + const rect = event.target.getBoundingClientRect(); + const data = { + url: event.target._placesNode.uri, + x: rect.left, + y: rect.top, + width: rect.width, + height: rect.height, + }; + + this.openGlance(data); + + return false; + } } window.gZenGlanceManager = new ZenGlanceManager(); diff --git a/src/ZenSidebarManager.mjs b/src/ZenSidebarManager.mjs index 1103b1b..367bacc 100644 --- a/src/ZenSidebarManager.mjs +++ b/src/ZenSidebarManager.mjs @@ -783,6 +783,27 @@ class ZenBrowserManagerSidebar extends ZenDOMOperatedFeature { const url = gContextMenu.linkURL || gContextMenu.target.ownerDocument.location.href; this._createNewPanel(url); } + + toggleEssentialsAccordion(header) { + const content = document.getElementById('EssentialsToolbarItems'); + const isExpanded = header.hasAttribute('expanded'); + + if (isExpanded) { + // Collapse + header.removeAttribute('expanded'); + content.style.maxHeight = null; + content.style.opacity = '0'; + setTimeout(() => { + content.removeAttribute('expanded'); + }, 300); + } else { + // Expand + header.setAttribute('expanded', 'true'); + content.setAttribute('expanded', 'true'); + content.style.maxHeight = content.scrollHeight + "px"; + content.style.opacity = '1'; + } + } } window.gZenBrowserManagerSidebar = new ZenBrowserManagerSidebar(); diff --git a/src/ZenWorkspaces.mjs b/src/ZenWorkspaces.mjs index adbbad9..016305a 100644 --- a/src/ZenWorkspaces.mjs +++ b/src/ZenWorkspaces.mjs @@ -13,17 +13,17 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { } this.ownerWindow = window; XPCOMUtils.defineLazyPreferenceGetter( - this, - 'shouldShowIconStrip', - 'zen.workspaces.show-icon-strip', - true, - this._expandWorkspacesStrip.bind(this) + this, + 'shouldShowIconStrip', + 'zen.workspaces.show-icon-strip', + true, + this._expandWorkspacesStrip.bind(this) ); XPCOMUtils.defineLazyPreferenceGetter( - this, - 'shouldForceContainerTabsToWorkspace', - 'zen.workspaces.force-container-workspace', - true + this, + 'shouldForceContainerTabsToWorkspace', + 'zen.workspaces.force-container-workspace', + true ); XPCOMUtils.defineLazyPreferenceGetter( this, @@ -60,9 +60,9 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { const lastChangeTimestamp = await ZenWorkspacesStorage.getLastChangeTimestamp(); if ( - !this._workspaceCache || - !this._workspaceCache.lastChangeTimestamp || - lastChangeTimestamp > this._workspaceCache.lastChangeTimestamp + !this._workspaceCache || + !this._workspaceCache.lastChangeTimestamp || + lastChangeTimestamp > this._workspaceCache.lastChangeTimestamp ) { await this._propagateWorkspaceData(); @@ -79,9 +79,9 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { if (typeof this._shouldHaveWorkspaces === 'undefined') { let docElement = document.documentElement; this._shouldHaveWorkspaces = !( - docElement.hasAttribute('privatebrowsingmode') || - docElement.getAttribute('chromehidden').includes('toolbar') || - docElement.getAttribute('chromehidden').includes('menubar') + docElement.hasAttribute('privatebrowsingmode') || + docElement.getAttribute('chromehidden').includes('toolbar') || + docElement.getAttribute('chromehidden').includes('menubar') ); return this._shouldHaveWorkspaces; } @@ -213,7 +213,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { } _kIcons = JSON.parse(Services.prefs.getStringPref('zen.workspaces.icons')).map((icon) => - typeof Intl.Segmenter !== 'undefined' ? new Intl.Segmenter().segment(icon).containing().segment : Array.from(icon)[0] + typeof Intl.Segmenter !== 'undefined' ? new Intl.Segmenter().segment(icon).containing().segment : Array.from(icon)[0] ); _initializeWorkspaceCreationIcons() { @@ -348,7 +348,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { get shouldShowContainers() { return ( - Services.prefs.getBoolPref('privacy.userContext.ui.enabled') && ContextualIdentityService.getPublicIdentities().length > 0 + Services.prefs.getBoolPref('privacy.userContext.ui.enabled') && ContextualIdentityService.getPublicIdentities().length > 0 ); } @@ -367,7 +367,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { element.setAttribute('default', 'true'); } const containerGroup = browser.ContextualIdentityService.getPublicIdentities().find( - (container) => container.userContextId === workspace.containerTabId + (container) => container.userContextId === workspace.containerTabId ); if (containerGroup) { element.classList.add('identity-color-' + containerGroup.color); @@ -378,76 +378,76 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { } element.addEventListener( - 'dragstart', - function (event) { - if (this.isReorderModeOn(browser)) { - this.draggedElement = element; - event.dataTransfer.effectAllowed = 'move'; - event.dataTransfer.setData('text/plain', element.getAttribute('zen-workspace-id')); - element.classList.add('dragging'); - } else { - event.preventDefault(); - } - }.bind(browser.ZenWorkspaces) - ); - - element.addEventListener( - 'dragover', - function (event) { - if (this.isReorderModeOn(browser) && this.draggedElement) { - event.preventDefault(); - event.dataTransfer.dropEffect = 'move'; - } - }.bind(browser.ZenWorkspaces) - ); - - element.addEventListener( - 'dragenter', - function (event) { - if (this.isReorderModeOn(browser) && this.draggedElement) { - element.classList.add('dragover'); - } - }.bind(browser.ZenWorkspaces) - ); - - element.addEventListener( - 'dragleave', - function (event) { - element.classList.remove('dragover'); - }.bind(browser.ZenWorkspaces) - ); - - element.addEventListener( - 'drop', - async function (event) { - event.preventDefault(); - element.classList.remove('dragover'); - if (this.isReorderModeOn(browser)) { - const draggedWorkspaceId = event.dataTransfer.getData('text/plain'); - const targetWorkspaceId = element.getAttribute('zen-workspace-id'); - if (draggedWorkspaceId !== targetWorkspaceId) { - await this.moveWorkspace(draggedWorkspaceId, targetWorkspaceId); + 'dragstart', + function (event) { + if (this.isReorderModeOn(browser)) { + this.draggedElement = element; + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData('text/plain', element.getAttribute('zen-workspace-id')); + element.classList.add('dragging'); + } else { + event.preventDefault(); } + }.bind(browser.ZenWorkspaces) + ); + + element.addEventListener( + 'dragover', + function (event) { + if (this.isReorderModeOn(browser) && this.draggedElement) { + event.preventDefault(); + event.dataTransfer.dropEffect = 'move'; + } + }.bind(browser.ZenWorkspaces) + ); + + element.addEventListener( + 'dragenter', + function (event) { + if (this.isReorderModeOn(browser) && this.draggedElement) { + element.classList.add('dragover'); + } + }.bind(browser.ZenWorkspaces) + ); + + element.addEventListener( + 'dragleave', + function (event) { + element.classList.remove('dragover'); + }.bind(browser.ZenWorkspaces) + ); + + element.addEventListener( + 'drop', + async function (event) { + event.preventDefault(); + element.classList.remove('dragover'); + if (this.isReorderModeOn(browser)) { + const draggedWorkspaceId = event.dataTransfer.getData('text/plain'); + const targetWorkspaceId = element.getAttribute('zen-workspace-id'); + if (draggedWorkspaceId !== targetWorkspaceId) { + await this.moveWorkspace(draggedWorkspaceId, targetWorkspaceId); + } + if (this.draggedElement) { + this.draggedElement.classList.remove('dragging'); + this.draggedElement = null; + } + } + }.bind(browser.ZenWorkspaces) + ); + + element.addEventListener( + 'dragend', + function (event) { if (this.draggedElement) { this.draggedElement.classList.remove('dragging'); this.draggedElement = null; } - } - }.bind(browser.ZenWorkspaces) - ); - - element.addEventListener( - 'dragend', - function (event) { - if (this.draggedElement) { - this.draggedElement.classList.remove('dragging'); - this.draggedElement = null; - } - const workspaceElements = browser.document.querySelectorAll('.zen-workspace-button'); - for (const elem of workspaceElements) { - elem.classList.remove('dragover'); - } - }.bind(browser.ZenWorkspaces) + const workspaceElements = browser.document.querySelectorAll('.zen-workspace-button'); + for (const elem of workspaceElements) { + elem.classList.remove('dragover'); + } + }.bind(browser.ZenWorkspaces) ); let childs = browser.MozXULElement.parseXULToFragment(` @@ -470,18 +470,18 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { childs.querySelector('.zen-workspace-name').textContent = workspace.name; if (containerGroup) { childs.querySelector('.zen-workspace-container').textContent = ContextualIdentityService.getUserContextLabel( - containerGroup.userContextId + containerGroup.userContextId ); } childs.querySelector('.zen-workspace-actions').addEventListener( - 'command', - ((event) => { - let button = event.target; - this._contextMenuId = button.closest('toolbarbutton[zen-workspace-id]').getAttribute('zen-workspace-id'); - const popup = button.ownerDocument.getElementById('zenWorkspaceActionsMenu'); - popup.openPopup(button, 'after_end'); - }).bind(browser.ZenWorkspaces) + 'command', + ((event) => { + let button = event.target; + this._contextMenuId = button.closest('toolbarbutton[zen-workspace-id]').getAttribute('zen-workspace-id'); + const popup = button.ownerDocument.getElementById('zenWorkspaceActionsMenu'); + popup.openPopup(button, 'after_end'); + }).bind(browser.ZenWorkspaces) ); element.appendChild(childs); element.onclick = (async () => { @@ -870,8 +870,8 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { let button = document.getElementById('PanelUI-zen-workspaces-edit-save'); let name = this._workspaceEditInput.value; if ( - name === this._workspaceEditInput.getAttribute('data-initial-value') && - icon === this._workspaceEditIconsContainer.getAttribute('data-initial-value') + name === this._workspaceEditInput.getAttribute('data-initial-value') && + icon === this._workspaceEditIconsContainer.getAttribute('data-initial-value') ) { button.setAttribute('disabled', 'true'); return; @@ -903,8 +903,8 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { let firstTab = undefined; for (let tab of gBrowser.tabs) { if ( - (tab.getAttribute('zen-workspace-id') === window.uuid && !(tab.pinned && !shouldAllowPinnedTabs)) || - !tab.hasAttribute('zen-workspace-id') + (tab.getAttribute('zen-workspace-id') === window.uuid && !(tab.pinned && !shouldAllowPinnedTabs)) || + !tab.hasAttribute('zen-workspace-id') ) { if (!firstTab && (onInit || !tab.pinned)) { firstTab = tab; @@ -941,6 +941,17 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { for (let listener of this._changeListeners ?? []) { listener(window); } + // reset bookmark toolbars + const placesToolbar = document.getElementById("PlacesToolbar"); + if(placesToolbar?._placesView) { + placesToolbar._placesView.invalidateContainer(placesToolbar._placesView._resultNode); + } + + const essentialsToolbar = document.getElementById("EssentialsToolbar"); + if(essentialsToolbar?._placesView) { + essentialsToolbar._placesView.invalidateContainer(essentialsToolbar._placesView._resultNode); + } + this._inChangingWorkspace = false; } @@ -1026,13 +1037,13 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { async updateContextMenu(_) { console.assert(this._contextMenuId, 'No context menu ID set'); document - .querySelector(`#PanelUI-zen-workspaces [zen-workspace-id="${this._contextMenuId}"] .zen-workspace-actions`) - .setAttribute('active', 'true'); + .querySelector(`#PanelUI-zen-workspaces [zen-workspace-id="${this._contextMenuId}"] .zen-workspace-actions`) + .setAttribute('active', 'true'); const workspaces = await this._workspaces(); let deleteMenuItem = document.getElementById('context_zenDeleteWorkspace'); if ( - workspaces.workspaces.length <= 1 || - workspaces.workspaces.find((workspace) => workspace.uuid === this._contextMenuId).default + workspaces.workspaces.length <= 1 || + workspaces.workspaces.find((workspace) => workspace.uuid === this._contextMenuId).default ) { deleteMenuItem.setAttribute('disabled', 'true'); } else { @@ -1046,7 +1057,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { } let openMenuItem = document.getElementById('context_zenOpenWorkspace'); if ( - workspaces.workspaces.find((workspace) => workspace.uuid === this._contextMenuId && this.isWorkspaceActive(workspace)) + workspaces.workspaces.find((workspace) => workspace.uuid === this._contextMenuId && this.isWorkspaceActive(workspace)) ) { openMenuItem.setAttribute('disabled', 'true'); } else { @@ -1070,7 +1081,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { onContextMenuClose() { let target = document.querySelector( - `#PanelUI-zen-workspaces [zen-workspace-id="${this._contextMenuId}"] .zen-workspace-actions` + `#PanelUI-zen-workspaces [zen-workspace-id="${this._contextMenuId}"] .zen-workspace-actions` ); if (target) { target.removeAttribute('active'); @@ -1108,7 +1119,7 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { let workspaceIndex = workspaces.workspaces.indexOf(activeWorkspace); // note: offset can be negative let nextWorkspace = - workspaces.workspaces[(workspaceIndex + offset + workspaces.workspaces.length) % workspaces.workspaces.length]; + workspaces.workspaces[(workspaceIndex + offset + workspaces.workspaces.length) % workspaces.workspaces.length]; await this.changeWorkspace(nextWorkspace); } @@ -1194,4 +1205,14 @@ var ZenWorkspaces = new (class extends ZenMultiWindowFeature { const workspaceToSwitch = workspaces.workspaces[index]; await this.changeWorkspace(workspaceToSwitch); } + + isBookmarkInAnotherWorkspace(bookmark) { + let tags = bookmark.tags; + // if any tag starts with "_workspace_id" and the workspace id doesnt match the active workspace id, return null + if (tags) { + for (let tag of tags.split(",")) { + return !!(tag.startsWith("zen_workspace_") && this.getActiveWorkspaceFromCache()?.uuid !== tag.split("_")[2]); + } + } + } })();