diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index ec789ce49..5bce0148b 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 fd2465046407261e8c29b4cd3d56122d232e701c..937068562975a26834572c408e9a75453bdf49cb 100644 +index fd2465046407261e8c29b4cd3d56122d232e701c..590a86d09b359f75fee12303d963a7ee68fee0f3 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -62,7 +62,7 @@ index fd2465046407261e8c29b4cd3d56122d232e701c..937068562975a26834572c408e9a7545 + } + + // 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) { diff --git a/src/browser/components/tabbrowser/content/tabgroup-js.patch b/src/browser/components/tabbrowser/content/tabgroup-js.patch index 5c7ca9e35..34d48759b 100644 --- a/src/browser/components/tabbrowser/content/tabgroup-js.patch +++ b/src/browser/components/tabbrowser/content/tabgroup-js.patch @@ -1,5 +1,5 @@ diff --git a/browser/components/tabbrowser/content/tabgroup.js b/browser/components/tabbrowser/content/tabgroup.js -index c0cb11590d6dfbcf6fa49ef5e10c6d3877191d1f..503e97a4ec87625b154627aa5839fb4f8d050f86 100644 +index c0cb11590d6dfbcf6fa49ef5e10c6d3877191d1f..cd98bd2401e4618b003c184108e8532174e2a7cf 100644 --- a/browser/components/tabbrowser/content/tabgroup.js +++ b/browser/components/tabbrowser/content/tabgroup.js @@ -13,10 +13,12 @@ @@ -18,10 +18,18 @@ index c0cb11590d6dfbcf6fa49ef5e10c6d3877191d1f..503e97a4ec87625b154627aa5839fb4f -@@ -57,20 +59,28 @@ +@@ -57,20 +59,36 @@ } connectedCallback() { ++ if (this._lastGroup && this._lastGroup !== this.group) { ++ this._lastGroup.dispatchEvent( ++ new CustomEvent("FolderUngrouped", { ++ bubbles: true, ++ detail: this, ++ }) ++ ); ++ } + if (this.group && this._lastGroup != this.group) { + this._lastGroup = this.group; + this.group.dispatchEvent( @@ -51,7 +59,7 @@ index c0cb11590d6dfbcf6fa49ef5e10c6d3877191d1f..503e97a4ec87625b154627aa5839fb4f this._initialized = true; this.saveOnWindowClose = true; -@@ -99,11 +109,14 @@ +@@ -99,11 +117,14 @@ this.#labelElement.addEventListener("mouseover", this); this.#labelElement.addEventListener("mouseout", this); @@ -71,7 +79,7 @@ index c0cb11590d6dfbcf6fa49ef5e10c6d3877191d1f..503e97a4ec87625b154627aa5839fb4f this.#updateLabelAriaAttributes(); this.#updateCollapsedAriaAttributes(); -@@ -129,6 +142,8 @@ +@@ -129,6 +150,8 @@ // mounts after getting created by `Tabbrowser.adoptTabGroup`. this.#wasCreatedByAdoption = false; } @@ -80,7 +88,7 @@ index c0cb11590d6dfbcf6fa49ef5e10c6d3877191d1f..503e97a4ec87625b154627aa5839fb4f resetDefaultGroupName = () => { this.#defaultGroupName = ""; -@@ -213,7 +228,10 @@ +@@ -213,7 +236,10 @@ } }); } @@ -92,7 +100,7 @@ index c0cb11590d6dfbcf6fa49ef5e10c6d3877191d1f..503e97a4ec87625b154627aa5839fb4f } get color() { -@@ -307,6 +325,9 @@ +@@ -307,6 +333,9 @@ } set collapsed(val) { @@ -102,7 +110,7 @@ index c0cb11590d6dfbcf6fa49ef5e10c6d3877191d1f..503e97a4ec87625b154627aa5839fb4f if (!!val == this.collapsed) { return; } -@@ -364,7 +385,6 @@ +@@ -364,7 +393,6 @@ tabGroupName, }) .then(result => { @@ -110,7 +118,7 @@ index c0cb11590d6dfbcf6fa49ef5e10c6d3877191d1f..503e97a4ec87625b154627aa5839fb4f }); } -@@ -383,7 +403,57 @@ +@@ -383,7 +411,57 @@ * @returns {MozTabbrowserTab[]} */ get tabs() { @@ -169,7 +177,7 @@ index c0cb11590d6dfbcf6fa49ef5e10c6d3877191d1f..503e97a4ec87625b154627aa5839fb4f } /** -@@ -442,7 +512,6 @@ +@@ -442,7 +520,6 @@ addTabs(tabs, metricsContext) { for (let tab of tabs) { if (tab.pinned) { @@ -177,7 +185,7 @@ index c0cb11590d6dfbcf6fa49ef5e10c6d3877191d1f..503e97a4ec87625b154627aa5839fb4f } let tabToMove = this.ownerGlobal === tab.ownerGlobal -@@ -505,7 +574,7 @@ +@@ -505,7 +582,7 @@ */ on_click(event) { let isToggleElement = @@ -186,7 +194,7 @@ index c0cb11590d6dfbcf6fa49ef5e10c6d3877191d1f..503e97a4ec87625b154627aa5839fb4f event.target === this.#overflowCountLabel; if (isToggleElement && event.button === 0) { event.preventDefault(); -@@ -570,5 +639,6 @@ +@@ -570,5 +647,6 @@ } } diff --git a/src/browser/components/tabbrowser/content/tabs-js.patch b/src/browser/components/tabbrowser/content/tabs-js.patch index 9e6585b09..e7124ee8c 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 c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39cb1923c0 100644 +index c7557dad38db9ef02b981c46de9595df77cb67db..c2001aac234d1575498df82db21da2374efe9697 100644 --- a/browser/components/tabbrowser/content/tabs.js +++ b/browser/components/tabbrowser/content/tabs.js @@ -44,6 +44,9 @@ @@ -70,7 +70,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 if (collapseTabGroupDuringDrag) { tab.group.collapsed = true; -+ gZenFolders.collapseVisibleTab(tab.group); ++ gZenFolders.animateGroupMove(tab.group); } } } @@ -85,7 +85,17 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 if ( (dropEffect == "move" || dropEffect == "copy") && document == draggedTab.ownerDocument && -@@ -1196,6 +1206,18 @@ +@@ -1142,7 +1152,8 @@ + isTabGroupLabel(draggedTab) && + draggedTab._dragData?.expandGroupOnDrop + ) { +- draggedTab.group.collapsed = false; ++ draggedTab.group.collapsed = draggedTab.group.hasAttribute("has-active"); ++ gZenFolders.animateGroupMove(draggedTab.group, true); + } + } + +@@ -1196,6 +1207,18 @@ this._tabDropIndicator.hidden = true; event.stopPropagation(); @@ -104,7 +114,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 if (draggedTab && dropEffect == "copy") { let duplicatedDraggedTab; let duplicatedTabs = []; -@@ -1220,8 +1242,9 @@ +@@ -1220,8 +1243,9 @@ let translateOffsetY = oldTranslateY % tabHeight; let newTranslateX = oldTranslateX - translateOffsetX; let newTranslateY = oldTranslateY - translateOffsetY; @@ -116,7 +126,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 if (this.#isContainerVerticalPinnedGrid(draggedTab)) { // Update both translate axis for pinned vertical expanded tabs -@@ -1237,8 +1260,8 @@ +@@ -1237,8 +1261,8 @@ } } else { let tabs = this.ariaFocusableItems.slice( @@ -127,7 +137,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 ); let size = this.verticalMode ? "height" : "width"; let screenAxis = this.verticalMode ? "screenY" : "screenX"; -@@ -1287,11 +1310,13 @@ +@@ -1287,11 +1311,13 @@ this.dragToPinPromoCard, ]; let shouldPin = @@ -141,7 +151,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 isTab(draggedTab) && draggedTab.pinned && this.arrowScrollbox.contains(event.target); -@@ -1309,6 +1334,7 @@ +@@ -1309,6 +1335,7 @@ (oldTranslateY && oldTranslateY != newTranslateY); } else if (this.verticalMode) { shouldTranslate &&= oldTranslateY && oldTranslateY != newTranslateY; @@ -149,7 +159,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 } else { shouldTranslate &&= oldTranslateX && oldTranslateX != newTranslateX; } -@@ -1503,6 +1529,7 @@ +@@ -1503,6 +1530,7 @@ let nextItem = this.ariaFocusableItems[newIndex]; let tabGroup = isTab(nextItem) && nextItem.group; @@ -157,7 +167,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 gBrowser.loadTabs(urls, { inBackground, replace, -@@ -1541,6 +1568,16 @@ +@@ -1541,6 +1569,16 @@ } this.#resetTabsAfterDrop(draggedTab.ownerDocument); @@ -174,7 +184,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 if ( dt.mozUserCancelled || dt.dropEffect != "none" || -@@ -1707,7 +1744,6 @@ +@@ -1707,7 +1745,6 @@ this.toggleAttribute("overflow", true); this._updateCloseButtons(); @@ -182,7 +192,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 document .getElementById("tab-preview-panel") -@@ -1765,7 +1801,7 @@ +@@ -1765,7 +1802,7 @@ } get newTabButton() { @@ -191,7 +201,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 } get verticalMode() { -@@ -1781,6 +1817,7 @@ +@@ -1781,6 +1818,7 @@ } get overflowing() { @@ -199,7 +209,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 return this.hasAttribute("overflow"); } -@@ -1789,31 +1826,51 @@ +@@ -1789,31 +1827,51 @@ if (this.#allTabs) { return this.#allTabs; } @@ -267,7 +277,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 } /** -@@ -1880,29 +1937,23 @@ +@@ -1880,29 +1938,23 @@ let elementIndex = 0; @@ -306,7 +316,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 } } -@@ -1914,6 +1965,7 @@ +@@ -1914,6 +1966,7 @@ _invalidateCachedTabs() { this.#allTabs = null; this._invalidateCachedVisibleTabs(); @@ -314,7 +324,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 } _invalidateCachedVisibleTabs() { -@@ -1929,8 +1981,8 @@ +@@ -1929,8 +1982,8 @@ #isContainerVerticalPinnedGrid(tab) { return ( this.verticalMode && @@ -325,7 +335,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 !this.expandOnHover ); } -@@ -1946,7 +1998,7 @@ +@@ -1946,7 +1999,7 @@ if (node == null) { // We have a container for non-tab elements at the end of the scrollbox. @@ -334,7 +344,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 } node.before(tab); -@@ -2041,7 +2093,7 @@ +@@ -2041,7 +2094,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. @@ -343,7 +353,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 const newTab2 = this.newTabButton; const newTabVertical = document.getElementById( "vertical-tabs-newtab-button" -@@ -2139,8 +2191,10 @@ +@@ -2139,8 +2192,10 @@ */ _handleTabSelect(aInstant) { let selectedTab = this.selectedItem; @@ -354,7 +364,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 selectedTab._notselectedsinceload = false; } -@@ -2149,7 +2203,7 @@ +@@ -2149,7 +2204,7 @@ * @param {boolean} [shouldScrollInstantly=false] */ #ensureTabIsVisible(tab, shouldScrollInstantly = false) { @@ -363,7 +373,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 if (arrowScrollbox.overflowing) { arrowScrollbox.ensureElementIsVisible(tab, shouldScrollInstantly); } -@@ -2288,6 +2342,16 @@ +@@ -2288,6 +2343,16 @@ when the tab is first selected to be dragged. */ #updateTabStylesOnDrag(tab) { @@ -380,7 +390,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 let isPinned = tab.pinned; let numPinned = gBrowser.pinnedTabCount; let allTabs = this.ariaFocusableItems; -@@ -2540,7 +2604,7 @@ +@@ -2540,7 +2605,7 @@ return; } @@ -389,7 +399,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 let directionX = screenX > dragData.animLastScreenX; let directionY = screenY > dragData.animLastScreenY; -@@ -2549,6 +2613,8 @@ +@@ -2549,6 +2614,8 @@ let { width: tabWidth, height: tabHeight } = draggedTab.getBoundingClientRect(); @@ -398,7 +408,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 let shiftSizeX = tabWidth * movingTabs.length; let shiftSizeY = tabHeight; dragData.tabWidth = tabWidth; -@@ -2585,8 +2651,8 @@ +@@ -2585,8 +2652,8 @@ let lastBoundX = lastTabInRow.screenX + lastTabInRow.getBoundingClientRect().width - @@ -409,7 +419,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 translateX = Math.min(Math.max(translateX, firstBoundX), lastBoundX); translateY = Math.min(Math.max(translateY, firstBoundY), lastBoundY); -@@ -2743,13 +2809,18 @@ +@@ -2743,13 +2810,18 @@ this.#clearDragOverGroupingTimer(); @@ -432,7 +442,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 if (this.#rtlMode) { tabs.reverse(); -@@ -2760,7 +2831,7 @@ +@@ -2760,7 +2832,7 @@ let screenAxis = this.verticalMode ? "screenY" : "screenX"; let size = this.verticalMode ? "height" : "width"; let translateAxis = this.verticalMode ? "translateY" : "translateX"; @@ -441,7 +451,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 let tabSize = this.verticalMode ? tabHeight : tabWidth; let translateX = event.screenX - dragData.screenX; let translateY = event.screenY - dragData.screenY; -@@ -2776,6 +2847,12 @@ +@@ -2776,6 +2848,12 @@ ); let lastMovingTab = movingTabs.at(-1); let firstMovingTab = movingTabs[0]; @@ -454,7 +464,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 let endEdge = ele => ele[screenAxis] + bounds(ele)[size]; let lastMovingTabScreen = endEdge(lastMovingTab); let firstMovingTabScreen = firstMovingTab[screenAxis]; -@@ -2790,6 +2867,11 @@ +@@ -2790,6 +2868,11 @@ let endBound = this.#rtlMode ? endEdge(this) - lastMovingTabScreen : periphery[screenAxis] - 1 - lastMovingTabScreen; @@ -466,7 +476,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 translate = Math.min(Math.max(translate, startBound), endBound); // Center the tab under the cursor if the tab is not under the cursor while dragging -@@ -2979,6 +3061,8 @@ +@@ -2979,6 +3062,8 @@ }; let dropElement = getOverlappedElement(); @@ -475,7 +485,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 let newDropElementIndex; if (dropElement) { -@@ -3060,7 +3144,7 @@ +@@ -3060,7 +3145,7 @@ ? Services.prefs.getIntPref( "browser.tabs.dragDrop.moveOverThresholdPercent" ) / 100 @@ -484,7 +494,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 moveOverThreshold = Math.min(1, Math.max(0, moveOverThreshold)); let shouldMoveOver = overlapPercent > moveOverThreshold; if (logicalForward && shouldMoveOver) { -@@ -3093,6 +3177,7 @@ +@@ -3093,6 +3178,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 ( @@ -492,7 +502,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 isTabGroupLabel(draggedTab) && dropElement?.group && (!dropElement.group.collapsed || -@@ -3119,20 +3204,13 @@ +@@ -3119,20 +3205,13 @@ let isOutOfBounds = isPinned ? dropElement.elementIndex >= numPinned : dropElement.elementIndex < numPinned; @@ -517,7 +527,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 let groupingDelay = Services.prefs.getIntPref( "browser.tabs.dragDrop.createGroup.delayMS" ); -@@ -3140,6 +3218,7 @@ +@@ -3140,6 +3219,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 = @@ -525,7 +535,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 !movingTabsSet.has(dropElement) && isTab(dropElement) && !dropElement?.group && -@@ -3148,6 +3227,7 @@ +@@ -3148,6 +3228,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 = @@ -533,7 +543,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 isTabGroupLabel(dropElement) && dropElement.group.collapsed && overlapPercent > dragOverGroupingThreshold; -@@ -3192,19 +3272,14 @@ +@@ -3192,19 +3273,14 @@ dropElement = dropElementGroup; colorCode = undefined; } else if (isTabGroupLabel(dropElement)) { @@ -561,7 +571,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 } this.#setDragOverGroupColor(colorCode); this.toggleAttribute("movingtab-addToGroup", colorCode); -@@ -3223,11 +3298,11 @@ +@@ -3223,11 +3299,11 @@ dragData.dropElement = dropElement; dragData.dropBefore = dropBefore; dragData.animDropElementIndex = newDropElementIndex; @@ -575,7 +585,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 continue; } -@@ -3346,12 +3421,14 @@ +@@ -3346,12 +3422,14 @@ element?.removeAttribute("dragover-groupTarget"); } @@ -592,7 +602,7 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 for (let item of this.ariaFocusableItems) { this.#resetGroupTarget(item); -@@ -3394,7 +3471,7 @@ +@@ -3394,16 +3472,15 @@ tab.style.left = ""; tab.style.top = ""; tab.style.maxWidth = ""; @@ -601,8 +611,9 @@ index c7557dad38db9ef02b981c46de9595df77cb67db..4c5972fceb4cf46718e994200bf48d39 } for (let label of draggedTabDocument.getElementsByClassName( "tab-group-label-container" -@@ -3403,7 +3480,7 @@ - label.style.height = ""; + )) { + label.style.width = ""; +- label.style.height = ""; label.style.left = ""; label.style.top = ""; - label.removeAttribute("dragtarget"); diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs index f740056b3..ac556143e 100644 --- a/src/zen/folders/ZenFolder.mjs +++ b/src/zen/folders/ZenFolder.mjs @@ -163,6 +163,7 @@ } async unpackTabs() { + this.collapsed = false; for (let tab of this.allItems.reverse()) { tab = tab.group.hasAttribute('split-view-group') ? tab.group : tab; if (tab.hasAttribute('zen-empty-tab')) { @@ -228,10 +229,21 @@ tab.setAttribute('folder-active', 'true'); } } else { + const folders = new Map(); for (let tab of this._activeTabs) { - tab.removeAttribute('folder-active'); + 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) { + tab.removeAttribute('folder-active'); + } } this._activeTabs = []; + folders.clear(); } } diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index a702b6d6e..6cd9427f8 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -195,6 +195,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); @@ -222,6 +223,13 @@ const tab = event.detail; const group = tab.group; group.pinned = tab.pinned; + const isActiveFolder = group?.activeGroups?.length > 0; + + if (isActiveFolder) { + group.activeTabs = [...new Set([...group.activeTabs, tab])].sort( + (a, b) => a._tPos > b._tPos + ); + } if (group.hasAttribute('split-view-group') && group.hasAttribute('zen-pinned-changed')) { // zen-pinned-changed remove it and set it to had-zen-pinned-changed to keep @@ -238,7 +246,24 @@ on_FolderGrouped(event) { if (this._sessionRestoring) return; const folder = event.detail; - folder.group.collapsed = false; + const parentFolder = event.target; + const isActiveFolder = parentFolder?.activeGroups?.length > 0; + const isSplitView = folder.hasAttribute('split-view-group'); + if (isActiveFolder && isSplitView) { + parentFolder.activeTabs = [...new Set([...parentFolder.activeTabs, ...folder.tabs])].sort( + (a, b) => a._tPos > b._tPos + ); + } + 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) { @@ -255,7 +280,7 @@ } collapsedRoot.setAttribute('has-active', 'true'); - await this.expandToSelected(collapsedRoot); + await this.animateSelect(collapsedRoot); gBrowser.tabContainer._invalidateCachedTabs(); } @@ -272,25 +297,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', false); - } - } + + await this.animateUnload(group, tab, true); } on_TabGroupCreate(event) { @@ -338,148 +353,7 @@ 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) { @@ -487,187 +361,7 @@ 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) { @@ -1251,256 +945,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', false); - } - - 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', false); - } - - 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', false)); - 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', false); @@ -1796,6 +1240,483 @@ 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) { + 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) { + return; + } + // Prevent hiding if we spam the group animations + this.#animationCount -= 1; + if (!selectedTabs.length && !this.#animationCount) { + tabsContainer.setAttribute('hidden', true); + } + + this.styleCleanup(itemsToHide); + } + + async animateExpand(group) { + 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 = Array.from(group.querySelectorAll('zen-folder[has-active]')); + + 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 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) { + 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'); + + tabsContainer.offsetHeight; + tabsContainer.setAttribute('hidden', true); + + const heightUntilSelected = this.#calculateHeightShift(tabsContainer, []); + + animations.push( + ...this.updateFolderIcon(folder, 'close', false), + ...this.#createAnimation( + groupStart, + { + marginTop: -(heightUntilSelected + 4), + }, + { duration: 0.12, ease: 'easeInOut' } + ) + ); + } + } + + tabToUnload.removeAttribute('folder-active'); + if (isSplitView) { + tabToUnload = tabToUnload.group; + } + + tabToUnload.style.removeProperty('--zen-folder-indent'); + + if (!ungroup) { + animations.push( + ...this.#createAnimation( + tabToUnload, + { + opacity: 0, + height: 0, + }, + { + duration: 0.12, + ease: 'easeInOut', + } + ) + ); + } + + this.#animationCount += 1; + await Promise.all(animations); + 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'); + + 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; + } + } + + if (selectedTabs.length) { + for (let i = 0; i < groupItems.length; i++) { + const { item, splitViewId } = groupItems[i]; + + // 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 + itemsToHide.forEach((item) => { + item.style.opacity = 0; + item.style.height = 0; + }); + this.styleCleanup(selectedTabs); + + animations.push( + ...this.#createAnimation( + itemsToHide, + { + opacity: 0, + height: 0, + }, + { + duration: 0.12, + ease: 'easeInOut', + } + ), + ...this.#createAnimation( + selectedTabs, + { + opacity: '', + height: '', + }, + { + 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 f0673244d..b046b9dc8 100644 --- a/src/zen/folders/zen-folders.css +++ b/src/zen/folders/zen-folders.css @@ -348,7 +348,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 52ef1456c..ed0dfa570 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -821,11 +821,10 @@ }, 3000); }); } - await gZenFolders.collapseVisibleTab( - selectedTab.group, - /* only if active */ true, - selectedTab - ); + const group = selectedTab.group?.hasAttribute('split-view-group') + ? selectedTab.group.group + : selectedTab.group; + await gZenFolders.animateUnload(group, selectedTab); let tabsToUnload = [selectedTab]; if (selectedTab.group?.hasAttribute('split-view-group')) { tabsToUnload = selectedTab.group.tabs; @@ -1251,7 +1250,7 @@ let isVisible = true; let parent = item.group; while (parent) { - if (parent.collapsed && !parent.hasAttribute('has-active')) { + if (!parent.visible) { isVisible = false; break; }