diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 50543e0eb..997098854 100644 --- a/src/browser/components/tabbrowser/content/tab-js.patch +++ b/src/browser/components/tabbrowser/content/tab-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tab.js b/browser/components/tabbrowser/content/tab.js -index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..5b0f46642e36fd3e15d13a8dbc633c7a9751f8aa 100644 +index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..12988986c4cf00990c1d1b2e4be362efc001afdd 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -65,7 +65,7 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..5b0f46642e36fd3e15d13a8dbc633c7a + } + + // Selected tabs are always visible -+ if ((this.selected || this.multiselected || this.hasAttribute("folder-active")) && !this.hasAttribute("was-folder-active")) return true; ++ if (this.selected || this.multiselected || this.hasAttribute("folder-active")) return true; + // Recursively check all parent groups + let currentParent = this.group; + while (currentParent) { @@ -138,7 +138,7 @@ index 425aaf8c8e4adf1507eb0d8ded671f8295544b04..5b0f46642e36fd3e15d13a8dbc633c7a + gZenPinnedTabManager._onTabResetPinButton(event, this, 'reset'); + gBrowser.tabContainer._blockDblClick = true; + } else if (event.target.classList.contains("tab-reset-button")) { -+ gZenPinnedTabManager._onCloseTabShortcut(event, this); ++ gZenPinnedTabManager.onCloseTabShortcut(event, this); + gBrowser.tabContainer._blockDblClick = true; + } } diff --git a/src/browser/components/tabbrowser/content/tabs-js.patch b/src/browser/components/tabbrowser/content/tabs-js.patch index 8318bbc10..c1c09ac70 100644 --- a/src/browser/components/tabbrowser/content/tabs-js.patch +++ b/src/browser/components/tabbrowser/content/tabs-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabs.js b/browser/components/tabbrowser/content/tabs.js -index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b19b606e9 100644 +index 1425607ef87d6c28fb676e722617edfb51ba12a1..8431ed0ab80e1e08fa622633d86e52915a44c9b1 100644 --- a/browser/components/tabbrowser/content/tabs.js +++ b/browser/components/tabbrowser/content/tabs.js @@ -44,6 +44,9 @@ @@ -70,7 +70,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b if (collapseTabGroupDuringDrag) { tab.group.collapsed = true; -+ gZenFolders.collapseVisibleTab(tab.group); ++ gZenFolders.animateGroupMove(tab.group); } } } @@ -85,7 +85,17 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b if ( (dropEffect == "move" || dropEffect == "copy") && document == draggedTab.ownerDocument && -@@ -1208,6 +1218,18 @@ +@@ -1154,7 +1164,8 @@ + isTabGroupLabel(draggedTab) && + draggedTab._dragData?.expandGroupOnDrop + ) { +- draggedTab.group.collapsed = false; ++ draggedTab.group.collapsed = draggedTab.group.hasAttribute("has-active"); ++ gZenFolders.animateGroupMove(draggedTab.group, true); + } + } + +@@ -1208,6 +1219,18 @@ this._tabDropIndicator.hidden = true; event.stopPropagation(); @@ -104,7 +114,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b if (draggedTab && dropEffect == "copy") { let duplicatedDraggedTab; let duplicatedTabs = []; -@@ -1232,8 +1254,9 @@ +@@ -1232,8 +1255,9 @@ let translateOffsetY = oldTranslateY % tabHeight; let newTranslateX = oldTranslateX - translateOffsetX; let newTranslateY = oldTranslateY - translateOffsetY; @@ -116,7 +126,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b if (this.#isContainerVerticalPinnedGrid(draggedTab)) { // Update both translate axis for pinned vertical expanded tabs -@@ -1249,8 +1272,8 @@ +@@ -1249,8 +1273,8 @@ } } else { let tabs = this.ariaFocusableItems.slice( @@ -127,7 +137,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b ); let size = this.verticalMode ? "height" : "width"; let screenAxis = this.verticalMode ? "screenY" : "screenX"; -@@ -1299,11 +1322,13 @@ +@@ -1299,11 +1323,13 @@ this.dragToPinPromoCard, ]; let shouldPin = @@ -141,7 +151,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b isTab(draggedTab) && draggedTab.pinned && this.arrowScrollbox.contains(event.target); -@@ -1321,6 +1346,7 @@ +@@ -1321,6 +1347,7 @@ (oldTranslateY && oldTranslateY != newTranslateY); } else if (this.verticalMode) { shouldTranslate &&= oldTranslateY && oldTranslateY != newTranslateY; @@ -149,7 +159,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b } else { shouldTranslate &&= oldTranslateX && oldTranslateX != newTranslateX; } -@@ -1515,6 +1541,7 @@ +@@ -1515,6 +1542,7 @@ let nextItem = this.ariaFocusableItems[newIndex]; let tabGroup = isTab(nextItem) && nextItem.group; @@ -157,7 +167,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b gBrowser.loadTabs(urls, { inBackground, replace, -@@ -1553,6 +1580,17 @@ +@@ -1553,6 +1581,17 @@ } this.#resetTabsAfterDrop(draggedTab.ownerDocument); @@ -175,7 +185,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b if ( dt.mozUserCancelled || dt.dropEffect != "none" || -@@ -1719,7 +1757,6 @@ +@@ -1719,7 +1758,6 @@ this.toggleAttribute("overflow", true); this._updateCloseButtons(); @@ -183,7 +193,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b document .getElementById("tab-preview-panel") -@@ -1777,7 +1814,7 @@ +@@ -1777,7 +1815,7 @@ } get newTabButton() { @@ -192,7 +202,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b } get verticalMode() { -@@ -1793,6 +1830,7 @@ +@@ -1793,6 +1831,7 @@ } get overflowing() { @@ -200,7 +210,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b return this.hasAttribute("overflow"); } -@@ -1806,29 +1844,54 @@ +@@ -1806,29 +1845,54 @@ if (pinnedChildren?.at(-1)?.id == "pinned-tabs-container-periphery") { pinnedChildren.pop(); } @@ -265,7 +275,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b } /** -@@ -1895,29 +1958,23 @@ +@@ -1895,29 +1959,23 @@ let elementIndex = 0; @@ -304,7 +314,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b } } -@@ -1929,6 +1986,7 @@ +@@ -1929,6 +1987,7 @@ _invalidateCachedTabs() { this.#allTabs = null; this._invalidateCachedVisibleTabs(); @@ -312,7 +322,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b } _invalidateCachedVisibleTabs() { -@@ -1944,8 +2002,8 @@ +@@ -1944,8 +2003,8 @@ #isContainerVerticalPinnedGrid(tab) { return ( this.verticalMode && @@ -323,7 +333,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b !this.expandOnHover ); } -@@ -1961,7 +2019,7 @@ +@@ -1961,7 +2020,7 @@ if (node == null) { // We have a container for non-tab elements at the end of the scrollbox. @@ -332,7 +342,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b } node.before(tab); -@@ -2056,7 +2114,7 @@ +@@ -2056,7 +2115,7 @@ // There are separate "new tab" buttons for horizontal tabs toolbar, vertical tabs and // for when the tab strip is overflowed (which is shared by vertical and horizontal tabs); // Attach the long click popup to all of them. @@ -341,7 +351,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b const newTab2 = this.newTabButton; const newTabVertical = document.getElementById( "vertical-tabs-newtab-button" -@@ -2156,8 +2214,10 @@ +@@ -2156,8 +2215,10 @@ */ _handleTabSelect(aInstant) { let selectedTab = this.selectedItem; @@ -352,7 +362,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b selectedTab._notselectedsinceload = false; } -@@ -2166,7 +2226,7 @@ +@@ -2166,7 +2227,7 @@ * @param {boolean} [shouldScrollInstantly=false] */ #ensureTabIsVisible(tab, shouldScrollInstantly = false) { @@ -361,7 +371,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b if (arrowScrollbox?.overflowing) { arrowScrollbox.ensureElementIsVisible(tab, shouldScrollInstantly); } -@@ -2305,6 +2365,16 @@ +@@ -2305,6 +2366,16 @@ when the tab is first selected to be dragged. */ #updateTabStylesOnDrag(tab) { @@ -378,7 +388,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b let isPinned = tab.pinned; let numPinned = gBrowser.pinnedTabCount; let allTabs = this.ariaFocusableItems; -@@ -2578,7 +2648,7 @@ +@@ -2578,7 +2649,7 @@ return; } @@ -387,7 +397,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b let directionX = screenX > dragData.animLastScreenX; let directionY = screenY > dragData.animLastScreenY; -@@ -2587,6 +2657,8 @@ +@@ -2587,6 +2658,8 @@ let { width: tabWidth, height: tabHeight } = draggedTab.getBoundingClientRect(); @@ -396,7 +406,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b let shiftSizeX = tabWidth * movingTabs.length; let shiftSizeY = tabHeight; dragData.tabWidth = tabWidth; -@@ -2623,8 +2695,8 @@ +@@ -2623,8 +2696,8 @@ let lastBoundX = lastTabInRow.screenX + lastTabInRow.getBoundingClientRect().width - @@ -407,7 +417,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b translateX = Math.min(Math.max(translateX, firstBoundX), lastBoundX); translateY = Math.min(Math.max(translateY, firstBoundY), lastBoundY); -@@ -2782,13 +2854,18 @@ +@@ -2782,13 +2855,18 @@ this.#clearDragOverGroupingTimer(); this.#clearPinnedDropIndicatorTimer(); @@ -430,7 +440,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b if (this.#rtlMode) { tabs.reverse(); -@@ -2799,7 +2876,7 @@ +@@ -2799,7 +2877,7 @@ let screenAxis = this.verticalMode ? "screenY" : "screenX"; let size = this.verticalMode ? "height" : "width"; let translateAxis = this.verticalMode ? "translateY" : "translateX"; @@ -439,7 +449,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b let tabSize = this.verticalMode ? tabHeight : tabWidth; let translateX = event.screenX - dragData.screenX; let translateY = event.screenY - dragData.screenY; -@@ -2815,6 +2892,12 @@ +@@ -2815,6 +2893,12 @@ ); let lastMovingTab = movingTabs.at(-1); let firstMovingTab = movingTabs[0]; @@ -452,7 +462,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b let endEdge = ele => ele[screenAxis] + bounds(ele)[size]; let lastMovingTabScreen = endEdge(lastMovingTab); let firstMovingTabScreen = firstMovingTab[screenAxis]; -@@ -2829,6 +2912,11 @@ +@@ -2829,6 +2913,11 @@ let endBound = this.#rtlMode ? endEdge(this) - lastMovingTabScreen : periphery[screenAxis] - 1 - lastMovingTabScreen; @@ -464,7 +474,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b translate = Math.min(Math.max(translate, startBound), endBound); // Center the tab under the cursor if the tab is not under the cursor while dragging -@@ -3018,6 +3106,8 @@ +@@ -3018,6 +3107,8 @@ }; let dropElement = getOverlappedElement(); @@ -473,7 +483,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b let newDropElementIndex; if (dropElement) { -@@ -3099,7 +3189,7 @@ +@@ -3099,7 +3190,7 @@ ? Services.prefs.getIntPref( "browser.tabs.dragDrop.moveOverThresholdPercent" ) / 100 @@ -482,7 +492,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b moveOverThreshold = Math.min(1, Math.max(0, moveOverThreshold)); let shouldMoveOver = overlapPercent > moveOverThreshold; if (logicalForward && shouldMoveOver) { -@@ -3132,6 +3222,7 @@ +@@ -3132,6 +3223,7 @@ // If dragging a group over another group, don't make it look like it is // possible to drop the dragged group inside the other group. if ( @@ -490,7 +500,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b isTabGroupLabel(draggedTab) && dropElement?.group && (!dropElement.group.collapsed || -@@ -3158,20 +3249,13 @@ +@@ -3158,20 +3250,13 @@ let isOutOfBounds = isPinned ? dropElement.elementIndex >= numPinned : dropElement.elementIndex < numPinned; @@ -515,7 +525,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b let groupingDelay = Services.prefs.getIntPref( "browser.tabs.dragDrop.createGroup.delayMS" ); -@@ -3179,6 +3263,7 @@ +@@ -3179,6 +3264,7 @@ // When dragging tab(s) over an ungrouped tab, signal to the user // that dropping the tab(s) will create a new tab group. let shouldCreateGroupOnDrop = @@ -523,7 +533,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b !movingTabsSet.has(dropElement) && isTab(dropElement) && !dropElement?.group && -@@ -3187,6 +3272,7 @@ +@@ -3187,6 +3273,7 @@ // When dragging tab(s) over a collapsed tab group label, signal to the // user that dropping the tab(s) will add them to the group. let shouldDropIntoCollapsedTabGroup = @@ -531,7 +541,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b isTabGroupLabel(dropElement) && dropElement.group.collapsed && overlapPercent > dragOverGroupingThreshold; -@@ -3231,19 +3317,14 @@ +@@ -3231,19 +3318,14 @@ dropElement = dropElementGroup; colorCode = undefined; } else if (isTabGroupLabel(dropElement)) { @@ -559,7 +569,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b } this.#setDragOverGroupColor(colorCode); this.toggleAttribute("movingtab-addToGroup", colorCode); -@@ -3262,11 +3343,11 @@ +@@ -3262,11 +3344,11 @@ dragData.dropElement = dropElement; dragData.dropBefore = dropBefore; dragData.animDropElementIndex = newDropElementIndex; @@ -573,7 +583,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b continue; } -@@ -3388,12 +3469,14 @@ +@@ -3388,12 +3470,14 @@ element?.removeAttribute("dragover-groupTarget"); } @@ -590,7 +600,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b for (let item of this.ariaFocusableItems) { this.#resetGroupTarget(item); -@@ -3440,7 +3523,7 @@ +@@ -3440,7 +3524,7 @@ tab.style.left = ""; tab.style.top = ""; tab.style.maxWidth = ""; @@ -599,7 +609,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b } for (let label of draggedTabDocument.getElementsByClassName( "tab-group-label-container" -@@ -3450,7 +3533,7 @@ +@@ -3450,7 +3534,7 @@ label.style.left = ""; label.style.top = ""; label.style.maxWidth = ""; @@ -608,7 +618,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b } let periphery = draggedTabDocument.getElementById( "tabbrowser-arrowscrollbox-periphery" -@@ -3522,7 +3605,7 @@ +@@ -3522,7 +3606,7 @@ let postTransitionCleanup = () => { movingTab._moveTogetherSelectedTabsData.animate = false; }; @@ -617,7 +627,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b postTransitionCleanup(); } else { let onTransitionEnd = transitionendEvent => { -@@ -3686,7 +3769,7 @@ +@@ -3686,7 +3770,7 @@ } _notifyBackgroundTab(aTab) { @@ -626,7 +636,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b return; } -@@ -3795,7 +3878,10 @@ +@@ -3795,7 +3879,10 @@ #getDragTarget(event, { ignoreSides = false } = {}) { let { target } = event; while (target) { @@ -638,7 +648,7 @@ index 1425607ef87d6c28fb676e722617edfb51ba12a1..62431aa1c78c8327edf2c8c93472cb8b break; } target = target.parentNode; -@@ -3812,6 +3898,9 @@ +@@ -3812,6 +3899,9 @@ return null; } } diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs index 470cc6127..f6ac00a86 100644 --- a/src/zen/folders/ZenFolder.mjs +++ b/src/zen/folders/ZenFolder.mjs @@ -117,6 +117,10 @@ return activeGroups; } + get childActiveGroups() { + return Array.from(this.querySelectorAll('zen-folder[has-active]')); + } + rename() { if (!document.documentElement.hasAttribute('zen-sidebar-expanded')) { return; @@ -240,16 +244,18 @@ } async #unloadAllActiveTabs(event, noClose = false) { - for (const tab of this.tabs) { - await gZenPinnedTabManager._onCloseTabShortcut(event, tab, { noClose }); - } + await gZenPinnedTabManager.onCloseTabShortcut(event, this.tabs, { + noClose, + alwaysUnload: true, + folderToUnload: this, + }); this.activeTabs = []; } on_click(event) { if (event.target === this.resetButton) { event.stopPropagation(); - this.#unloadAllActiveTabs(event); + this.unloadAllTabs(event); return; } super.on_click(event); diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index a3c91abf0..b04c2a787 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -194,6 +194,7 @@ window.addEventListener('TabGroupExpand', this); window.addEventListener('TabGroupCollapse', this); window.addEventListener('FolderGrouped', this); + window.addEventListener('FolderUngrouped', this); window.addEventListener('TabSelect', this); window.addEventListener('TabOpen', this); const onNewFolder = this.#onNewFolder.bind(this); @@ -255,6 +256,15 @@ parentFolder.collapsed = isActiveFolder; } + on_FolderUngrouped(event) { + if (this._sessionRestoring) return; + const parentFolder = event.target; + const folder = event.detail; + for (const tab of folder.tabs) { + this.animateUnload(parentFolder, tab, true); + } + } + async on_TabSelect(event) { const tab = gZenGlanceManager.getTabOrGlanceParent(event.target); let group = tab?.group; @@ -269,7 +279,7 @@ } collapsedRoot.setAttribute('has-active', 'true'); - await this.expandToSelected(collapsedRoot); + await this.animateSelect(collapsedRoot); gBrowser.tabContainer._invalidateCachedTabs(); } @@ -286,26 +296,15 @@ } } - on_TabUngrouped(event) { + async on_TabUngrouped(event) { const tab = event.detail; const group = event.target; - tab.removeAttribute('folder-active'); if (group.hasAttribute('split-view-group') && tab.hasAttribute('had-zen-pinned-changed')) { tab.setAttribute('zen-pinned-changed', true); tab.removeAttribute('had-zen-pinned-changed'); } - const activeGroup = group.activeGroups; - if (activeGroup?.length > 0) { - for (const folder of activeGroup) { - folder.activeTabs = folder.activeTabs.filter((tab) => tab.hasAttribute('folder-active')); - if (!folder.activeTabs.length) { - folder.removeAttribute('has-active'); - } - this.collapseVisibleTab(folder, true); - this.updateFolderIcon(folder, 'close'); - } - } + await this.animateUnload(group, tab, true); } on_TabGroupCreate(event) { @@ -352,336 +351,14 @@ const group = event.target; if (!group.isZenFolder) return; - this.cancelPopupTimer(); - const isForCollapseVisible = event.forCollapseVisible; - const canInheritMarginTop = event.canInheritMarginTop; - - const tabsContainer = group.querySelector('.tab-group-container'); - const animations = []; - const groupStart = group.querySelector('.zen-tab-group-start'); - let selectedItems = []; - let selectedGroupIds = new Set(); - let activeGroupIds = new Set(); - let itemsToHide = []; - - const items = group.childGroupsAndTabs - .filter((item) => !item.hasAttribute('zen-empty-tab')) - .map((item) => { - const isSplitView = item.group?.hasAttribute?.('split-view-group'); - const lastActiveGroup = !isSplitView - ? item?.group?.activeGroups?.at(-1) - : item?.group?.group?.activeGroups?.at(-1); - const activeGroupId = lastActiveGroup?.id; - const splitGroupId = isSplitView ? item.group.id : null; - if (gBrowser.isTabGroupLabel(item) && !isSplitView) item = item.parentNode; - - if ( - isForCollapseVisible - ? group.activeTabs.includes(item) - : item.multiselected || item.selected - ) { - selectedItems.push(item); - if (splitGroupId) selectedGroupIds.add(splitGroupId); - if (activeGroupId) activeGroupIds.add(activeGroupId); - } - - return { item, splitGroupId, activeGroupId }; - }); - - // Calculate the height we need to hide until we reach the selected item. - let heightUntilSelected = groupStart.style.marginTop - ? Math.abs(parseInt(groupStart.style.marginTop.slice(0, -2))) - : 0; - if (!isForCollapseVisible) { - if (selectedItems.length) { - if (!canInheritMarginTop) { - heightUntilSelected = 0; - } - const selectedItem = selectedItems[0]; - const isSplitView = selectedItem.group?.hasAttribute('split-view-group'); - const selectedContainer = isSplitView ? selectedItem.group : selectedItem; - heightUntilSelected += - window.windowUtils.getBoundsWithoutFlushing(selectedContainer).top - - window.windowUtils.getBoundsWithoutFlushing(groupStart).bottom; - if (isSplitView) { - heightUntilSelected -= 2; - } - } else { - heightUntilSelected += window.windowUtils.getBoundsWithoutFlushing(tabsContainer).height; - } - } - - let selectedIdx = items.length; - if (selectedItems.length) { - for (let i = 0; i < items.length; i++) { - if (selectedItems.includes(items[i].item)) { - selectedIdx = i; - break; - } - } - } - - for (let i = 0; i < items.length; i++) { - const { item, splitGroupId, activeGroupId } = items[i]; - - // Dont hide items before the first selected tab - if (selectedIdx >= 0 && i < selectedIdx && !isForCollapseVisible) continue; - - // Skip selected items - if (selectedItems.includes(item)) continue; - - // Skip items from selected split-view groups - if (splitGroupId && selectedGroupIds.has(splitGroupId)) continue; - - // Skip items from selected active groups - if (activeGroupId && activeGroupIds.has(activeGroupId)) { - // If item is tab-group-label-container we should hide it. - // Other items between tab-group-labe-container and folder-active tab should be visible cuz they are hidden by margin-top - if ( - item.parentElement.id !== activeGroupId && - !item.hasAttribute('folder-active') && - !isForCollapseVisible - ) { - continue; - } - } - - const itemToHide = splitGroupId ? item.group : item; - if (!itemsToHide.includes(itemToHide)) { - itemsToHide.push(itemToHide); - } - } - - if (selectedItems.length) { - group.setAttribute('has-active', 'true'); - group.activeTabs = selectedItems; - - selectedItems.forEach((item) => { - this.setFolderIndentation([item], group, /* for collapse = */ true); - }); - } - - itemsToHide.map((item) => { - animations.push( - gZenUIManager.motion.animate( - item, - { - opacity: 0, - height: 0, - }, - { duration: 0.12, ease: 'easeInOut' } - ) - ); - }); - - animations.push(...this.updateFolderIcon(group)); - const startMargin = -(heightUntilSelected + 4 * (selectedItems.length === 0 ? 1 : 0)); - animations.push( - gZenUIManager.motion.animate( - groupStart, - { - marginTop: startMargin, - }, - { duration: 0.12, ease: 'easeInOut' } - ) - ); - - gBrowser.tabContainer._invalidateCachedVisibleTabs(); - this.#animationCount += 1; - await Promise.all(animations); - // Prevent hiding if we spam the group animations - this.#animationCount -= 1; - if (selectedItems.length === 0 && !this.#animationCount) { - tabsContainer.setAttribute('hidden', true); - } + await this.animateCollapse(group); } async on_TabGroupExpand(event) { const group = event.target; if (!group.isZenFolder) return; - this.cancelPopupTimer(); - - const tabsContainer = group.querySelector('.tab-group-container'); - tabsContainer.removeAttribute('hidden'); - - const groupStart = group.querySelector('.zen-tab-group-start'); - const animations = []; - tabsContainer.style.overflow = 'hidden'; - - const normalizeGroupItems = (items) => { - const processed = []; - items - .filter((item) => !item.hasAttribute('zen-empty-tab')) - .forEach((item) => { - if (gBrowser.isTabGroupLabel(item)) { - if (item?.group?.hasAttribute('split-view-group')) { - item = item.group; - } else { - item = item.parentElement; - } - } - processed.push(item); - }); - return processed; - }; - - const activeGroups = group.querySelectorAll('zen-folder[has-active]'); - const groupItems = normalizeGroupItems(group.childGroupsAndTabs); - const itemsToHide = []; - - // TODO: It is necessary to correctly set marginTop for groups with has-active - for (const activeGroup of activeGroups) { - const activeGroupItems = activeGroup.childGroupsAndTabs; - let selectedTabs = activeGroup.activeTabs; - let selectedGroupIds = new Set(); - - selectedTabs.forEach((tab) => { - if (tab?.group?.hasAttribute('split-view-group')) { - selectedGroupIds.add(tab.group.id); - } - }); - - if (selectedTabs.length) { - let selectedIdx = -1; - for (let i = 0; i < activeGroupItems.length; i++) { - const item = activeGroup.childGroupsAndTabs[i]; - let selectedTab = item; - - // If the item is in a split view group, we need to get the last tab - if (selectedTab?.group?.hasAttribute('split-view-group')) { - selectedTab = selectedTab.group.tabs.at(-1); - } - - if (selectedTabs.includes(selectedTab) || selectedTabs.includes(item)) { - selectedIdx = i; - break; - } - } - - if (selectedIdx >= 0) { - for (let i = selectedIdx; i < activeGroupItems.length; i++) { - const item = activeGroup.childGroupsAndTabs[i]; - - if (selectedTabs.includes(item)) continue; - - const isSplitView = item.group?.hasAttribute?.('split-view-group'); - const splitGroupId = isSplitView ? item.group.id : null; - if (splitGroupId && selectedGroupIds.has(splitGroupId)) continue; - - itemsToHide.push(...normalizeGroupItems([item])); - } - } - } - } - - groupItems.map((item) => { - animations.push( - gZenUIManager.motion.animate( - item, - { - opacity: 1, - height: '', - }, - { duration: 0.12, ease: 'easeInOut' } - ) - ); - }); - - itemsToHide.map((item) => { - animations.push( - gZenUIManager.motion.animate( - item, - { - opacity: 0, - height: 0, - }, - { duration: 0.12, ease: 'easeInOut' } - ) - ); - }); - - animations.push(...this.updateFolderIcon(group)); - animations.push( - gZenUIManager.motion - .animate( - groupStart, - { - marginTop: 0, - }, - { - duration: 0.12, - ease: 'easeInOut', - } - ) - .then(() => { - tabsContainer.style.overflow = ''; - if (group.hasAttribute('has-active')) { - const activeTabs = group.activeTabs; - const folders = new Map(); - group.removeAttribute('has-active'); - for (let tab of activeTabs) { - const group = tab?.group?.hasAttribute('split-view-group') - ? tab?.group?.group - : tab?.group; - if (!folders.has(group?.id)) { - folders.set(group?.id, group?.activeGroups?.at(-1)); - } - let activeGroup = folders.get(group?.id); - // If group has active tabs, we need to update the indentation - if (activeGroup) { - const activeGroupStart = activeGroup.querySelector('.zen-tab-group-start'); - const selectedTabs = activeGroup.activeTabs; - if (selectedTabs.length > 0) { - const selectedItem = selectedTabs[0]; - const isSplitView = selectedItem.group?.hasAttribute('split-view-group'); - const selectedContainer = isSplitView ? selectedItem.group : selectedItem; - - const heightUntilSelected = - window.windowUtils.getBoundsWithoutFlushing(selectedContainer).top - - window.windowUtils.getBoundsWithoutFlushing(activeGroupStart).bottom; - - const adjustedHeight = isSplitView - ? heightUntilSelected - 2 - : heightUntilSelected; - activeGroupStart.style.marginTop = - -(adjustedHeight + 4 * (selectedTabs.length === 0 ? 1 : 0)) + 'px'; - } - this.setFolderIndentation([tab], activeGroup, /* for collapse = */ true); - } else { - // Since the folder is now expanded, we should remove active attribute - // to the tab that was previously visible - tab.removeAttribute('folder-active'); - if (tab.group?.hasAttribute('split-view-group')) { - tab.group.style.removeProperty('--zen-folder-indent'); - } else { - tab.style.removeProperty('--zen-folder-indent'); - } - } - } - - folders.clear(); - } - // Folder has been expanded and has no active tabs - group.activeTabs = []; - }) - ); - - this.#animationCount += 1; - await Promise.all(animations); - this.#animationCount -= 1; - if (this.#animationCount) { - return; - } - groupItems.forEach((item) => { - // Cleanup just in case - item.style.opacity = ''; - item.style.height = ''; - }); - itemsToHide.forEach((item) => { - item.style.opacity = ''; - item.style.height = ''; - }); + await this.animateExpand(group); } #onNewFolder(event) { @@ -1208,256 +885,6 @@ } } - collapseVisibleTab(group, onlyIfActive = false, selectedTab = null) { - let tabsToCollapse = [selectedTab]; - if (group?.hasAttribute('split-view-group') && selectedTab && onlyIfActive) { - tabsToCollapse = group.tabs; - group = group.group; - } - if (!group?.isZenFolder) return; - - if (selectedTab) { - selectedTab.style.removeProperty('--zen-folder-indent'); - } - // We ignore if the flag is set to avoid infinite recursion - if (onlyIfActive && group.activeGroups.length && selectedTab) { - onlyIfActive = true; - group = group.activeGroups[group.activeGroups.length - 1]; - } - // Only continue from here if we have the active tab for this group. - // This is important so we dont set the margin to the wrong group. - // Example: - // folder1 - // ├─ folder2 - // └─── tab - // When we collapse folder1 ONLY and reset tab since it's `active`, pinned - // manager gives originally the direct group of `tab`, which is `folder2`. - // But we should be setting the margin only on `folder1`. - if (!group.activeTabs.includes(selectedTab) && selectedTab) return; - group._prevActiveTabs = group.activeTabs; - for (const item of group._prevActiveTabs) { - if (selectedTab ? tabsToCollapse.includes(item) : !item.selected || !onlyIfActive) { - item.removeAttribute('folder-active'); - group.activeTabs = group.activeTabs.filter((t) => t !== item); - if (!onlyIfActive) { - item.setAttribute('was-folder-active', 'true'); - } - } - } - - if (group.activeTabs.length === 0) { - group.removeAttribute('has-active'); - this.updateFolderIcon(group, 'close'); - } - - return this.on_TabGroupCollapse({ - target: group, - forCollapseVisible: true, - targetTab: selectedTab, - }).then(() => { - if (selectedTab) { - selectedTab.style.removeProperty('--zen-folder-indent'); - } - }); - } - - expandVisibleTab(group) { - if (!group?.isZenFolder) return; - - group.activeTabs = group._prevActiveTabs || []; - for (const tab of group.activeTabs) { - if (tab.hasAttribute('was-folder-active')) { - tab.setAttribute('folder-active', 'true'); - tab.removeAttribute('was-folder-active'); - } - } - - if (group.activeTabs.length === 0) { - group.removeAttribute('has-active'); - this.updateFolderIcon(group, 'close'); - } - - this.on_TabGroupExpand({ target: group, forExpandVisible: true }); - - gBrowser.tabContainer._invalidateCachedVisibleTabs(); - } - - async expandToSelected(group) { - if (!group?.isZenFolder) return; - - this.cancelPopupTimer?.(); - - const tabsContainer = group.querySelector('.tab-group-container'); - const groupStart = group.querySelector('.zen-tab-group-start'); - const animations = []; - - const normalizeGroupItems = (items) => { - const processed = []; - items - .filter((item) => !item.hasAttribute('zen-empty-tab')) - .forEach((item) => { - if (gBrowser.isTabGroupLabel(item)) { - if (item?.group?.hasAttribute('split-view-group')) { - item = item.group; - } else { - item = item.parentElement; - } - } - processed.push(item); - }); - return processed; - }; - - const selectedItems = []; - const groupItems = normalizeGroupItems(group.childGroupsAndTabs); - - for (const item of groupItems) { - if (item.hasAttribute('folder-active') || item.selected) { - selectedItems.push(item); - } - } - - for (const tab of selectedItems) { - let current = tab?.group?.hasAttribute('split-view-group') ? tab.group.group : tab?.group; - while (current) { - const activeForGroup = selectedItems.filter((t) => current.contains(t)); - if (activeForGroup.length) { - if (current.collapsed) { - if (current.hasAttribute('has-active')) { - // It is important to keep the sequence of elements as in the DOM - current.activeTabs = [...new Set([...current.activeTabs, ...activeForGroup])].sort( - (a, b) => { - const position = a.compareDocumentPosition(b); - if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1; - if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1; - return 0; - } - ); - } else { - current.setAttribute('has-active', 'true'); - current.activeTabs = activeForGroup; - } - - // If selectedItems does not have a tab, it is necessary to add it - current.activeTabs.forEach((tab) => { - if (!selectedItems.includes(tab)) { - selectedItems.push(tab); - } - }); - const tabsContainer = current.querySelector('.tab-group-container'); - const groupStart = current.querySelector('.zen-tab-group-start'); - const curMarginTop = parseInt(groupStart.style.marginTop) || 0; - - if (tabsContainer.hasAttribute('hidden')) tabsContainer.removeAttribute('hidden'); - - animations.push(...this.updateFolderIcon(current, 'close')); - animations.push( - gZenUIManager.motion.animate( - groupStart, - { - marginTop: [curMarginTop, 0], - }, - { duration: 0.12, ease: 'easeInOut' } - ) - ); - for (const tab of activeForGroup) { - this.setFolderIndentation( - [tab], - current, - /* for collapse = */ true, - /* animate = */ false - ); - } - } - } - current = current.group; - } - } - - const selectedItemsSet = new Set(); - const selectedGroupIds = new Set(); - for (const tab of selectedItems) { - const isSplit = tab?.group?.hasAttribute?.('split-view-group'); - if (isSplit) selectedGroupIds.add(tab.group.id); - const container = isSplit ? tab.group : tab; - selectedItemsSet.add(container); - } - - const itemsToHide = []; - for (const item of groupItems) { - const isSplit = item.group?.hasAttribute?.('split-view-group'); - const splitId = isSplit ? item.group.id : null; - const itemElem = isSplit ? item.group : item; - - if (selectedItemsSet.has(itemElem)) continue; - if (splitId && selectedGroupIds.has(splitId)) continue; - - if (!itemElem.hasAttribute?.('folder-active')) { - if (!itemsToHide.includes(itemElem)) itemsToHide.push(itemElem); - } - } - - if (tabsContainer.hasAttribute('hidden')) { - tabsContainer.removeAttribute('hidden'); - } - - for (const item of groupItems) { - animations.push( - gZenUIManager.motion.animate( - item, - { - opacity: 1, - height: '', - }, - { duration: 0.12, ease: 'easeInOut' } - ) - ); - } - - for (const item of itemsToHide) { - animations.push( - gZenUIManager.motion.animate( - item, - { - opacity: 0, - height: 0, - }, - { duration: 0.12, ease: 'easeInOut' } - ) - ); - } - - let curMarginTop = parseInt(groupStart.style.marginTop) || 0; - animations.push( - gZenUIManager.motion - .animate( - groupStart, - { - marginTop: [curMarginTop, 0], - }, - { duration: 0.12, ease: 'easeInOut' } - ) - .then(() => { - tabsContainer.style.overflow = ''; - }) - ); - - animations.push(...this.updateFolderIcon(group)); - - this.#animationCount = (this.#animationCount || 0) + 1; - await Promise.all(animations); - this.#animationCount -= 1; - - for (const item of groupItems) { - item.style.opacity = ''; - item.style.height = ''; - } - for (const item of itemsToHide) { - item.style.opacity = ''; - item.style.height = ''; - } - } - #groupInit(group, stateData) { // Setup zen-folder icon to the correct position this.updateFolderIcon(group, 'auto'); @@ -1760,6 +1187,537 @@ return { dropElement, colorCode, dropBefore }; } + + #normalizeGroupItems(items) { + return items + .filter((item) => !item.hasAttribute('zen-empty-tab')) + .map((item) => { + if (gBrowser.isTabGroup(item)) { + item = item.firstChild; + } else if (gBrowser.isTabGroupLabel(item)) { + if (item?.group?.hasAttribute('split-view-group')) { + item = item.group; + } else { + item = item.parentElement; + } + } + return item; + }); + } + + #collectGroupItems(group, opts = {}) { + const { selectedTabs = [], splitViewIds = new Set(), activeFoldersIds = new Set() } = opts; + const folders = new Map(); + return group.childGroupsAndTabs + .filter((item) => !item.hasAttribute('zen-empty-tab')) + .map((item) => { + const isSplitView = item.group?.hasAttribute?.('split-view-group'); + const group = isSplitView ? item.group.group : item.group; + if (!folders.has(group?.id)) { + folders.set(group?.id, group?.activeGroups[0]); + } + const lastActiveFolder = folders.get(group?.id); + const activeFolderId = lastActiveFolder?.id; + const splitViewId = isSplitView ? item?.group?.id : null; + + if (item.multiselected || item.selected || item.hasAttribute('folder-active')) { + selectedTabs.push(item); + if (splitViewId) splitViewIds.add(splitViewId); + if (activeFolderId) activeFoldersIds.add(activeFolderId); + } + + if (gBrowser.isTabGroupLabel(item)) { + if (isSplitView) { + item = item.group; + } else { + item = item.parentElement; + } + } + + return { item, splitViewId, activeFolderId }; + }); + } + + #createAnimation(items, targetState, opts, callback = () => {}) { + items = Array.isArray(items) ? items : [items]; + return items.map((item) => + gZenUIManager.motion.animate(item, targetState, opts).then(callback) + ); + } + + #calculateHeightShift(tabsContainer, selectedTabs) { + let heightShift = 0; + if (selectedTabs.length) { + return heightShift; + } else { + heightShift += window.windowUtils.getBoundsWithoutFlushing(tabsContainer).height; + } + return heightShift; + } + + async animateCollapse(group) { + this.cancelPopupTimer(); + + const animations = []; + const selectedTabs = []; + const splitViewIds = new Set(); + const activeFoldersIds = new Set(); + const itemsToHide = []; + + const tabsContainer = group.querySelector('.tab-group-container'); + const groupStart = group.querySelector('.zen-tab-group-start'); + + const groupItems = this.#collectGroupItems(group, { + selectedTabs, + splitViewIds, + activeFoldersIds, + }); + const heightUntilSelected = this.#calculateHeightShift(tabsContainer, selectedTabs); + + if (selectedTabs.length) { + for (let i = 0; i < groupItems.length; i++) { + const { item, splitViewId, activeFolderId } = groupItems[i]; + + // Skip selected items + if (selectedTabs.includes(item)) continue; + + // Skip items from selected split-view groups + if (splitViewId && splitViewIds.has(splitViewId)) continue; + + // Skip items from selected active groups + if (activeFolderId && activeFoldersIds.has(activeFolderId)) { + // If item is tab-group-label-container we should hide it. + // Other items between tab-group-labe-container and folder-active tab should be visible cuz they are hidden by margin-top + if (item.parentElement.id !== activeFolderId && !item.hasAttribute('folder-active')) { + continue; + } + } + + if (!itemsToHide.includes(item)) { + itemsToHide.push(item); + } + } + + group.setAttribute('has-active', 'true'); + group.activeTabs = selectedTabs; + + selectedTabs.forEach((tab) => { + this.setFolderIndentation([tab], group, /* for collapse = */ true); + }); + } + + animations.push( + ...this.#createAnimation( + itemsToHide, + { opacity: 0, height: 0 }, + { duration: 0.12, ease: 'easeInOut' } + ), + ...this.updateFolderIcon(group), + ...this.#createAnimation( + groupStart, + { + marginTop: -(heightUntilSelected + 4 * (selectedTabs.length === 0 ? 1 : 0)), + }, + { duration: 0.12, ease: 'easeInOut' } + ) + ); + + gBrowser.tabContainer._invalidateCachedVisibleTabs(); + this.#animationCount += 1; + await Promise.all(animations); + if (this.#animationCount) { + this.#animationCount -= 1; + return; + } + // Prevent hiding if we spam the group animations + if (!selectedTabs.length && !this.#animationCount) { + tabsContainer.setAttribute('hidden', true); + } + + this.styleCleanup(itemsToHide); + } + + async animateExpand(group) { + this.cancelPopupTimer(); + + const animations = []; + const itemsToHide = []; + + const tabsContainer = group.querySelector('.tab-group-container'); + tabsContainer.removeAttribute('hidden'); + tabsContainer.style.overflow = 'hidden'; + + const groupStart = group.querySelector('.zen-tab-group-start'); + const itemsToShow = this.#normalizeGroupItems(group.childGroupsAndTabs); + const activeFolders = group.childActiveGroups; + + for (const folder of activeFolders) { + const splitViewIds = new Set(); + const selectedTabs = folder.activeTabs; + + const activeFoldersIds = new Set(); + const activeFolderItems = this.#collectGroupItems(folder, { + splitViewIds, + activeFoldersIds, + }); + + if (selectedTabs.length) { + for (let i = 0; i < activeFolderItems.length; i++) { + const { item, splitViewId, activeFolderId } = activeFolderItems[i]; + + // Skip selected items + if (selectedTabs.includes(item)) continue; + + // Skip items from selected split-view groups + if (splitViewId && splitViewIds.has(splitViewId)) continue; + + if (activeFolderId && activeFoldersIds.has(activeFolderId)) { + const folder = item.parentElement; + if ( + gBrowser.isTabGroup(folder) && + folder.id !== activeFolderId && + item.hasAttribute('folder-active') + ) { + continue; + } + } + + if (!itemsToHide.includes(item)) { + itemsToHide.push(item); + } + } + } + } + + const afterMarginTop = () => { + tabsContainer.style.overflow = ''; + if (group.hasAttribute('has-active')) { + const activeTabs = group.activeTabs; + const folders = new Map(); + group.removeAttribute('has-active'); + for (let tab of activeTabs) { + const group = tab?.group?.hasAttribute('split-view-group') + ? tab?.group?.group + : tab?.group; + if (!folders.has(group?.id)) { + folders.set(group?.id, group?.activeGroups?.at(-1)); + } + let activeGroup = folders.get(group?.id); + if (activeGroup) { + this.setFolderIndentation([tab], activeGroup, /* for collapse = */ true); + } else { + // Since the folder is now expanded, we should remove active attribute + // to the tab that was previously visible + tab.removeAttribute('folder-active'); + if (tab.group?.hasAttribute('split-view-group')) { + tab.group.style.removeProperty('--zen-folder-indent'); + } else { + tab.style.removeProperty('--zen-folder-indent'); + } + } + } + folders.clear(); + } + // Folder has been expanded and has no active tabs + group.activeTabs = []; + }; + + animations.push( + ...this.#createAnimation( + itemsToShow, + { opacity: '', height: '' }, + { duration: 0.12, ease: 'easeInOut' } + ), + ...this.#createAnimation( + itemsToHide, + { opacity: 0, height: 0 }, + { duration: 0.12, ease: 'easeInOut' } + ), + ...this.updateFolderIcon(group), + ...this.#createAnimation( + groupStart, + { + marginTop: 0, + }, + { duration: 0.12, ease: 'easeInOut' }, + afterMarginTop + ) + ); + + this.#animationCount += 1; + await Promise.all(animations); + this.#animationCount -= 1; + + // Cleanup + this.styleCleanup(itemsToShow); + this.styleCleanup(itemsToHide); + } + + async animateUnloadAll(group) { + const animations = []; + + const activeGroups = [group, ...group.childActiveGroups]; + for (const folder of activeGroups) { + folder.removeAttribute('has-active'); + folder.activeTabs = []; + const groupItems = this.#normalizeGroupItems(folder.allItems); + const tabsContainer = folder.querySelector('.tab-group-container'); + + this.styleCleanup(groupItems); + + const groupStart = folder.querySelector('.zen-tab-group-start'); + + // Trigger a reflow + tabsContainer.offsetHeight; + + const heightUntilSelected = this.#calculateHeightShift(tabsContainer, []); + + // Collect animations for this specific folder becoming inactive + animations.push( + ...this.updateFolderIcon(folder, 'close', false), + ...this.#createAnimation( + groupStart, + { + marginTop: -(heightUntilSelected + 4), + }, + { duration: 0.12, ease: 'easeInOut' } + ) + ); + } + + this.#animationCount += 1; + await Promise.all(animations); + this.#animationCount -= 1; + gBrowser.tabContainer._invalidateCachedTabs(); + } + + async animateUnload(group, tabToUnload, ungroup = false) { + const isSplitView = tabToUnload.group?.hasAttribute('split-view-group'); + if ((!group?.isZenFolder || !isSplitView) && !tabToUnload.hasAttribute('folder-active')) + return; + const animations = []; + + const activeGroups = group.activeGroups; + for (const folder of activeGroups) { + folder.activeTabs = folder.activeTabs.filter((tab) => tab !== tabToUnload); + + if (folder.activeTabs.length === 0) { + animations.push(async () => { + folder.removeAttribute('has-active'); + const groupItems = this.#normalizeGroupItems(folder.allItems); + const tabsContainer = folder.querySelector('.tab-group-container'); + + this.styleCleanup(groupItems); + + const groupStart = folder.querySelector('.zen-tab-group-start'); + + // Trigger a reflow + tabsContainer.offsetHeight; + tabsContainer.setAttribute('hidden', true); + + const heightUntilSelected = this.#calculateHeightShift(tabsContainer, []); + + // Collect animations for this specific folder becoming inactive + const folderAnimation = [ + ...this.updateFolderIcon(folder, 'close', false), + ...this.#createAnimation( + groupStart, + { + marginTop: -(heightUntilSelected + 4), + }, + { duration: 0.12, ease: 'easeInOut' } + ), + ]; + await Promise.all(folderAnimation); + }); + } + } + + tabToUnload.removeAttribute('folder-active'); + if (isSplitView) { + tabToUnload = tabToUnload.group; + } + + tabToUnload.style.removeProperty('--zen-folder-indent'); + + let tabUnloadAnimations = []; + if (!ungroup) { + tabUnloadAnimations = this.#createAnimation( + tabToUnload, + { + opacity: 0, + height: 0, + }, + { + duration: 0.12, + ease: 'easeInOut', + } + ); + } + + // Manage global animation count + this.#animationCount += 1; + + // Await the tab unload animation first + await Promise.all(tabUnloadAnimations); + await Promise.all(animations.map((item) => (typeof item === 'function' ? item() : item))); + this.#animationCount -= 1; + gBrowser.tabContainer._invalidateCachedTabs(); + } + + async animateSelect(group) { + if (!group?.isZenFolder) return; + + this.cancelPopupTimer(); + + const animations = []; + const selectedTabs = []; + const splitViewIds = new Set(); + const itemsToHide = []; + + const groupItems = this.#collectGroupItems(group, { + selectedTabs, + splitViewIds, + }); + + for (const tab of selectedTabs) { + let currentGroup = tab?.group?.hasAttribute('split-view-group') + ? tab.group.group + : tab?.group; + while (currentGroup) { + const activeTabs = selectedTabs.filter((t) => currentGroup.tabs.includes(t)); + if (activeTabs.length) { + if (currentGroup.collapsed) { + if (currentGroup.hasAttribute('has-active')) { + // It is important to keep the sequence of elements as in the DOM + currentGroup.activeTabs = [ + ...new Set([...currentGroup.activeTabs, ...activeTabs]), + ].sort((a, b) => a._tPos > b._tPos); + } else { + currentGroup.setAttribute('has-active', 'true'); + currentGroup.activeTabs = activeTabs; + } + + const tabsContainer = currentGroup.querySelector('.tab-group-container'); + const groupStart = currentGroup.querySelector('.zen-tab-group-start'); + tabsContainer.style.overflow = 'clip'; + + if (tabsContainer.hasAttribute('hidden')) tabsContainer.removeAttribute('hidden'); + + const afterMarginTop = () => { + tabsContainer.style.overflow = ''; + }; + + animations.push( + ...this.updateFolderIcon(currentGroup, 'close', false), + ...this.#createAnimation( + groupStart, + { + marginTop: 0, + }, + { duration: 0.12, ease: 'easeInOut' }, + afterMarginTop + ) + ); + for (const tab of activeTabs) { + this.setFolderIndentation( + [tab], + currentGroup, + /* for collapse = */ true, + /* animate = */ false + ); + } + } + } + currentGroup = currentGroup.group; + } + } + + const itemsToShow = []; + if (selectedTabs.length) { + for (let i = 0; i < groupItems.length; i++) { + const { item, splitViewId } = groupItems[i]; + + itemsToShow.push(item); + + // Skip selected items + if (selectedTabs.includes(item)) continue; + + // Skip items from selected split-view groups + if (splitViewId && splitViewIds.has(splitViewId)) continue; + + if (!item.hasAttribute?.('folder-active')) { + if (!itemsToHide.includes(item)) itemsToHide.push(item); + } + } + } + + // FIXME: This is a hack to fix the animations not working properly + this.styleCleanup(itemsToShow); + itemsToHide.forEach((item) => { + item.style.opacity = 0; + item.style.height = 0; + }); + + animations.push( + ...this.#createAnimation( + itemsToShow, + { + opacity: '', + height: '', + }, + { + duration: 0.12, + ease: 'easeInOut', + } + ), + ...this.#createAnimation( + itemsToHide, + { + opacity: 0, + height: 0, + }, + { + duration: 0.12, + ease: 'easeInOut', + } + ) + ); + + this.#animationCount += 1; + await Promise.all(animations); + this.#animationCount -= 1; + if (this.#animationCount) { + return; + } + + // Cleanup + this.styleCleanup(itemsToHide); + this.styleCleanup(selectedTabs); + } + + animateGroupMove(group, expand = false) { + if (!group?.isZenFolder) return; + const groupStart = group.querySelector('.zen-tab-group-start'); + const tabsContainer = group.querySelector('.tab-group-container'); + const heightContainer = expand ? 0 : this.#calculateHeightShift(tabsContainer, []); + tabsContainer.style.overflow = expand ? '' : 'clip'; + + this.#createAnimation( + groupStart, + { + marginTop: expand ? 0 : -(heightContainer + 4), + }, + { duration: 0.12, ease: 'easeInOut' } + ); + } + + styleCleanup(items) { + items.forEach((item) => { + item.style.removeProperty('opacity'); + item.style.removeProperty('height'); + }); + } } window.gZenFolders = new nsZenFolders(); diff --git a/src/zen/folders/zen-folders.css b/src/zen/folders/zen-folders.css index d841303f1..6d4618795 100644 --- a/src/zen/folders/zen-folders.css +++ b/src/zen/folders/zen-folders.css @@ -207,7 +207,7 @@ zen-folder { display: flex; } - & > .tab-group-container { + &:not([has-active]) > .tab-group-container { overflow-y: clip; } } diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index a33df17d0..a9dd070ee 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -639,7 +639,7 @@ async _onTabClick(e) { const tab = e.target?.closest('tab'); if (e.button === 1 && tab) { - await this._onCloseTabShortcut(e, tab, { + await this.onCloseTabShortcut(e, tab, { closeIfPending: Services.prefs.getBoolPref( 'zen.pinned-tab-manager.wheel-close-if-pending' ), @@ -769,7 +769,7 @@ let cmdClose = document.getElementById('cmd_close'); if (cmdClose) { - cmdClose.addEventListener('command', this._onCloseTabShortcut.bind(this)); + cmdClose.addEventListener('command', this.onCloseTabShortcut.bind(this)); } } @@ -788,20 +788,38 @@ await ZenPinnedTabsStorage.savePin(pin, notifyObservers); } - async _onCloseTabShortcut( + async onCloseTabShortcut( event, selectedTab = gBrowser.selectedTab, { behavior = lazy.zenPinnedTabCloseShortcutBehavior, noClose = false, closeIfPending = false, + alwaysUnload = false, + folderToUnload = null, } = {} ) { try { - if (!selectedTab?.pinned) { + const tabs = Array.isArray(selectedTab) ? selectedTab : [selectedTab]; + const pinnedTabs = [ + ...new Set( + tabs + .flatMap((tab) => { + if (tab.group?.hasAttribute('split-view-group')) { + return tab.group.tabs; + } + return tab; + }) + .filter((tab) => tab?.pinned) + ), + ]; + + if (!pinnedTabs.length) { return; } + const selectedTabs = pinnedTabs.filter((tab) => tab.selected); + event.stopPropagation(); event.preventDefault(); @@ -809,66 +827,82 @@ behavior = 'unload-switch'; } + if (alwaysUnload && ['close', 'reset', 'switch', 'reset-switch'].includes(behavior)) { + behavior = behavior.contains('reset') ? 'reset-unload-switch' : 'unload-switch'; + } + switch (behavior) { - case 'close': - this._removePinnedAttributes(selectedTab, true); - gBrowser.removeTab(selectedTab, { animate: true }); + case 'close': { + for (const tab of pinnedTabs) { + this._removePinnedAttributes(tab, true); + gBrowser.removeTab(tab, { animate: true }); + } break; + } case 'reset-unload-switch': case 'unload-switch': case 'reset-switch': case 'switch': if (behavior.includes('unload')) { - if (selectedTab.hasAttribute('glance-id')) { - // We have a glance tab inside the tab we are trying to unload, - // before we used to just ignore it but now we need to fully close - // it as well. - gZenGlanceManager.manageTabClose(selectedTab.glanceTab); - await new Promise((resolve) => { - let hasRan = false; - const onGlanceClose = () => { - hasRan = true; - resolve(); - }; - window.addEventListener('GlanceClose', onGlanceClose, { once: true }); - // Set a timeout to resolve the promise if the event doesn't fire. - // We do this to prevent any future issues where glance woudnt close such as - // glance requering to ask for permit unload. - setTimeout(() => { - if (!hasRan) { - console.warn('GlanceClose event did not fire within 3 seconds'); + for (const tab of pinnedTabs) { + if (tab.hasAttribute('glance-id')) { + // We have a glance tab inside the tab we are trying to unload, + // before we used to just ignore it but now we need to fully close + // it as well. + gZenGlanceManager.manageTabClose(tab.glanceTab); + await new Promise((resolve) => { + let hasRan = false; + const onGlanceClose = () => { + hasRan = true; resolve(); - } - }, 3000); - }); + }; + window.addEventListener('GlanceClose', onGlanceClose, { once: true }); + // Set a timeout to resolve the promise if the event doesn't fire. + // We do this to prevent any future issues where glance woudnt close such as + // glance requering to ask for permit unload. + setTimeout(() => { + if (!hasRan) { + console.warn('GlanceClose event did not fire within 3 seconds'); + resolve(); + } + }, 3000); + }); + } + const isSpltView = tab.group?.hasAttribute('split-view-group'); + const group = isSpltView ? tab.group.group : tab.group; + if (!folderToUnload && tab.hasAttribute('folder-active')) { + await gZenFolders.animateUnload(group, tab); + } } - await gZenFolders.collapseVisibleTab( - selectedTab.group, - /* only if active */ true, - selectedTab - ); - let tabsToUnload = [selectedTab]; - if (selectedTab.group?.hasAttribute('split-view-group')) { - tabsToUnload = selectedTab.group.tabs; + if (folderToUnload) { + await gZenFolders.animateUnloadAll(folderToUnload); } - const allAreUnloaded = tabsToUnload.every( + const allAreUnloaded = pinnedTabs.every( (tab) => tab.hasAttribute('pending') && !tab.hasAttribute('zen-essential') ); - if (allAreUnloaded && closeIfPending) { - return await this._onCloseTabShortcut(event, selectedTab, { behavior: 'close' }); + for (const tab of pinnedTabs) { + if (allAreUnloaded && closeIfPending) { + return await this._onCloseTabShortcut(event, tab, { behavior: 'close' }); + } + } + await gBrowser.explicitUnloadTabs(pinnedTabs); + for (const tab of pinnedTabs) { + tab.removeAttribute('discarded'); } - await gBrowser.explicitUnloadTabs(tabsToUnload); - selectedTab.removeAttribute('discarded'); } - if (selectedTab.selected) { - this._handleTabSwitch(selectedTab); + if (selectedTabs.length) { + this._handleTabSwitch(selectedTabs[0]); } if (behavior.includes('reset')) { - this._resetTabToStoredState(selectedTab); + for (const tab of pinnedTabs) { + this._resetTabToStoredState(tab); + } } break; case 'reset': - this._resetTabToStoredState(selectedTab); + for (const tab of pinnedTabs) { + this._resetTabToStoredState(tab); + } break; default: return; @@ -1279,7 +1313,7 @@ let isVisible = true; let parent = item.group; while (parent) { - if (parent.collapsed && !parent.hasAttribute('has-active')) { + if (!parent.visible) { isVisible = false; break; }