diff --git a/package.json b/package.json index 2ae45962f..737a39e6e 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "scripts": { "build": "surfer build", "build:ui": "surfer build --ui", - "start": "cd engine && python ./mach run --noprofile", + "start": "cd engine && python3 ./mach run --noprofile", "import": "npm run ffprefs && surfer import", "export": "surfer export", "init": "npm run download && npm run import && npm run bootstrap", diff --git a/src/browser/components/tabbrowser/content/tab-js.patch b/src/browser/components/tabbrowser/content/tab-js.patch index 46135a48e..048785825 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 f261f0f2c7945cbdb41e5d1b6067a8db38bef117..32e21e1295aa508a2b8dc6b8a0ea4d678dd81594 100644 +index f261f0f2c7945cbdb41e5d1b6067a8db38bef117..f126879aa9e843c74992aa751795aef3888a6a64 100644 --- a/browser/components/tabbrowser/content/tab.js +++ b/browser/components/tabbrowser/content/tab.js @@ -21,6 +21,7 @@ @@ -42,7 +42,7 @@ index f261f0f2c7945cbdb41e5d1b6067a8db38bef117..32e21e1295aa508a2b8dc6b8a0ea4d67 return; } -@@ -224,9 +227,19 @@ +@@ -224,9 +227,21 @@ } get visible() { @@ -53,9 +53,11 @@ index f261f0f2c7945cbdb41e5d1b6067a8db38bef117..32e21e1295aa508a2b8dc6b8a0ea4d67 + return false; + } + ++ // Selected tabs are always visible ++ if ((this.selected || this.multiselected || this.hasAttribute("folder-active")) && !this.hasAttribute("was-folder-active")) return true; + // Recursively check all parent groups + let currentParent = this.group; -+ while (currentParent && !this.hasAttribute("folder-active")) { ++ while (currentParent) { + if (currentParent.collapsed) { + return false; + } @@ -65,7 +67,7 @@ index f261f0f2c7945cbdb41e5d1b6067a8db38bef117..32e21e1295aa508a2b8dc6b8a0ea4d67 } get hidden() { -@@ -297,7 +310,7 @@ +@@ -297,7 +312,7 @@ return false; } @@ -74,7 +76,7 @@ index f261f0f2c7945cbdb41e5d1b6067a8db38bef117..32e21e1295aa508a2b8dc6b8a0ea4d67 } get lastAccessed() { -@@ -374,8 +387,11 @@ +@@ -374,8 +389,11 @@ } get group() { @@ -88,7 +90,7 @@ index f261f0f2c7945cbdb41e5d1b6067a8db38bef117..32e21e1295aa508a2b8dc6b8a0ea4d67 } return null; } -@@ -469,6 +485,8 @@ +@@ -469,6 +487,8 @@ this.style.MozUserFocus = "ignore"; } else if ( event.target.classList.contains("tab-close-button") || @@ -97,7 +99,7 @@ index f261f0f2c7945cbdb41e5d1b6067a8db38bef117..32e21e1295aa508a2b8dc6b8a0ea4d67 event.target.classList.contains("tab-icon-overlay") || event.target.classList.contains("tab-audio-button") ) { -@@ -523,6 +541,10 @@ +@@ -523,6 +543,10 @@ this.style.MozUserFocus = ""; } @@ -108,7 +110,7 @@ index f261f0f2c7945cbdb41e5d1b6067a8db38bef117..32e21e1295aa508a2b8dc6b8a0ea4d67 on_click(event) { if (event.button != 0) { return; -@@ -571,6 +593,7 @@ +@@ -571,6 +595,7 @@ ) ); } else { @@ -116,7 +118,7 @@ index f261f0f2c7945cbdb41e5d1b6067a8db38bef117..32e21e1295aa508a2b8dc6b8a0ea4d67 gBrowser.removeTab(this, { animate: true, triggeringEvent: event, -@@ -583,6 +606,14 @@ +@@ -583,6 +608,14 @@ // (see tabbrowser-tabs 'click' handler). gBrowser.tabContainer._blockDblClick = true; } @@ -131,7 +133,7 @@ index f261f0f2c7945cbdb41e5d1b6067a8db38bef117..32e21e1295aa508a2b8dc6b8a0ea4d67 } on_dblclick(event) { -@@ -606,6 +637,8 @@ +@@ -606,6 +639,8 @@ animate: true, triggeringEvent: event, }); diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs index b9f6a2892..a60e88748 100644 --- a/src/zen/folders/ZenFolder.mjs +++ b/src/zen/folders/ZenFolder.mjs @@ -9,6 +9,7 @@ @@ -80,6 +81,7 @@ return; } this.#initialized = true; + this._activeTabs = []; this.icon.appendChild(ZenFolder.rawIcon.cloneNode(true)); // Save original values for animations this.icon.querySelectorAll('animate, animateTransform, animateMotion').forEach((anim) => { @@ -209,6 +211,47 @@ get iconURL() { return this.icon.querySelector('image')?.getAttribute('href') || ''; } + + set activeTabs(tabs) { + if (tabs.length) { + this._activeTabs = tabs; + for (let tab of tabs) { + tab.setAttribute('folder-active', 'true'); + } + } else { + for (let tab of this._activeTabs) { + tab.removeAttribute('folder-active'); + } + this._activeTabs = []; + } + } + + get activeTabs() { + return this._activeTabs; + } + + get resetButton() { + return this.labelElement.parentElement.querySelector('.tab-reset-button'); + } + + #unloadAllActiveTabs() { + for (const tab of this.activeTabs) { + const tabResetButton = tab.querySelector('.tab-reset-button'); + if (tabResetButton) { + tabResetButton.click(); + } + } + this.activeTabs = []; + } + + on_click(event) { + if (event.target === this.resetButton) { + event.stopPropagation(); + this.#unloadAllActiveTabs(); + return; + } + super.on_click(event); + } } customElements.define('zen-folder', ZenFolder); diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs index 5aca9dd60..a692bebbe 100644 --- a/src/zen/folders/ZenFolders.mjs +++ b/src/zen/folders/ZenFolders.mjs @@ -224,16 +224,14 @@ folder.group.collapsed = false; } - #onTabSelected(event) { - const tab = event.target; - const prevTab = event.detail.previousTab; - const group = tab?.group; - const isActive = group?.activeGroups?.length > 0; - if (isActive) tab.setAttribute('folder-active', true); - if (prevTab.hasAttribute('folder-active')) prevTab.removeAttribute('folder-active'); - if (tab.group?.collapsed) { - this.expandToSelected(group); - } + #onTabSelected() { + // const tab = event.target; + // const prevTab = event.detail.previousTab; + // const group = tab?.group; + // const isActive = group?.activeGroups?.length > 0; + // if (isActive) tab.setAttribute('folder-active', true); + // TODO: Figure out what to do with this + // if (prevTab.hasAttribute('folder-active')) prevTab.removeAttribute('folder-active'); gBrowser.tabContainer._invalidateCachedTabs(); } @@ -265,8 +263,9 @@ const activeGroup = group.activeGroups; if (activeGroup?.length > 0) { for (const folder of activeGroup) { - folder.removeAttribute('has-active'); - folder.removeAttribute('selected-tab-id'); + if (!folder.activeTabs.length) { + folder.removeAttribute('has-active'); + } this.collapseVisibleTab(folder); this.updateFolderIcon(folder, 'close', false); } @@ -322,28 +321,35 @@ const tabsContainer = group.querySelector('.tab-group-container'); const animations = []; const groupStart = group.querySelector('.zen-tab-group-start'); - let selectedItem = null; - let selectedGroupId = null; - let itemsAfterSelected = []; + let selectedItems = []; + let selectedGroupIds = new Set(); + let activeGroupIds = new Set(); + let itemsToHide = []; - gBrowser.clearMultiSelectedTabs(); + 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; - const items = group.childGroupsAndTabs.map((item) => { - const isSplitView = item.group?.hasAttribute?.('split-view-group'); - const splitGroupId = isSplitView ? item.group.id : null; - if (gBrowser.isTabGroupLabel(item) && !isSplitView) item = item.parentNode; + if (item.multiselected || item.selected) { + selectedItems.push(item); + if (splitGroupId) selectedGroupIds.add(splitGroupId); + if (activeGroupId) activeGroupIds.add(activeGroupId); + } - if (item.hasAttribute('visuallyselected')) { - selectedItem = item; - selectedGroupId = splitGroupId; - } - - return { item, splitGroupId }; - }); + return { item, splitGroupId, activeGroupId }; + }); // Calculate the height we need to hide until we reach the selected item. let heightUntilSelected; - if (selectedItem) { + if (selectedItems.length) { + const selectedItem = selectedItems[0]; const isSplitView = selectedItem.group?.hasAttribute('split-view-group'); const selectedContainer = isSplitView ? selectedItem.group : selectedItem; heightUntilSelected = @@ -356,25 +362,52 @@ heightUntilSelected = window.windowUtils.getBoundsWithoutFlushing(tabsContainer).height; } - let afterSelected = false; - for (let { item, splitGroupId } of items) { - if (item === selectedItem) { - afterSelected = true; - continue; + let selectedIdx = items.length; + if (selectedItems.length) { + for (let i = 0; i < items.length; i++) { + if (selectedItems.includes(items[i].item)) { + selectedIdx = i; + break; + } } - if (selectedGroupId && splitGroupId === selectedGroupId) continue; - if (afterSelected && splitGroupId) item = item.group; - if (afterSelected) itemsAfterSelected.push(item); } - if (selectedItem) { + 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) 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')) + continue; + } + + const itemToHide = splitGroupId ? item.group : item; + if (!itemsToHide.includes(itemToHide)) { + itemsToHide.push(itemToHide); + } + } + + if (selectedItems.length) { group.setAttribute('has-active', 'true'); - selectedItem.setAttribute('folder-active', 'true'); - group.setAttribute('selected-tab-id', selectedItem.getAttribute('zen-pin-id')); - this.setFolderIndentation([selectedItem], group, /* for collapse = */ true); + group.activeTabs = selectedItems; + + selectedItems.forEach((item) => { + this.setFolderIndentation([item], group, /* for collapse = */ true); + }); } - for (const item of itemsAfterSelected) { + itemsToHide.map((item) => { animations.push( gZenUIManager.motion.animate( item, @@ -385,23 +418,26 @@ { duration: 0.1, ease: 'easeInOut' } ) ); - } + }); animations.push(...this.updateFolderIcon(group)); animations.push( gZenUIManager.motion.animate( groupStart, { - marginTop: [0, -(heightUntilSelected + 4 * !selectedItem)], + marginTop: [0, -(heightUntilSelected + 4 * (selectedItems.length === 0 ? 1 : 0))], }, { duration: 0.1, ease: 'easeInOut' } ) ); + this.#animationCount += 1; await Promise.all(animations); // Prevent hiding if we spam the group animations this.#animationCount -= 1; - if (!selectedItem && !this.#animationCount) tabsContainer.setAttribute('hidden', true); + if (selectedItems.length === 0 && !this.#animationCount) { + tabsContainer.setAttribute('hidden', true); + } } async #onTabGroupExpand(event) { @@ -417,14 +453,32 @@ const animations = []; tabsContainer.style.overflow = 'hidden'; if (group.hasAttribute('has-active')) { - const selectedTabId = group.getAttribute('selected-tab-id'); - const selectedTab = group?.querySelector(`tab[zen-pin-id="${selectedTabId}"]`); - // Since the folder is now expanded, we should remove active attribute - // to the tab that was previously visible - selectedTab.removeAttribute('folder-active'); - selectedTab.style.removeProperty('--zen-folder-indent'); + const activeTabs = group.activeTabs; + const folders = new Map(); group.removeAttribute('has-active'); - group.removeAttribute('selected-tab-id'); + for (let tab of activeTabs) { + if (!folders.has(tab?.group?.id)) { + folders.set(tab?.group?.id, tab?.group?.activeGroups?.at(-1)); + } + let activeGroup = folders.get(tab?.group?.id); + // If group has active tabs, we need to update the indentation + 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'); + } + } + } + // Folder has been expanded and has no active tabs + group.activeTabs = []; + + folders.clear(); } const normalizeGroupItems = (items) => { @@ -449,15 +503,46 @@ const itemsToHide = []; for (const activeGroup of activeGroups) { - let selectedTabId = activeGroup.getAttribute('selected-tab-id'); - let selectedTab = activeGroup.querySelector(`tab[zen-pin-id="${selectedTabId}"]`); - // If the selected tab 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); + 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 < activeGroup.childGroupsAndTabs.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 < activeGroup.childGroupsAndTabs.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])); + } + } } - let index = activeGroup.childGroupsAndTabs.indexOf(selectedTab); - let itemsAfter = activeGroup.childGroupsAndTabs.slice(index + 1); - itemsToHide.push(...normalizeGroupItems(itemsAfter)); } groupItems.map((item) => { @@ -752,7 +837,7 @@ } const activeGroup = event.target.parentElement; - if (activeGroup.tabs.filter((tab) => !tab.hasAttribute('zen-empty-tab')).length === 0) { + if (activeGroup.tabs.filter((tab) => this.#shouldAppearOnTabSearch(tab)).length === 0) { // If the group has no tabs, we don't show the popup return; } @@ -839,12 +924,23 @@ }; } + #shouldAppearOnTabSearch(tab) { + // Note that tab.visible and tab.hidden act in different ways. + // We specifically do tab.visible because we don't want appearing + // as 'folder active' in the tab list, it would be rather useless to have + // that option as the user. tab.hidden doesn't actually tell translate + // to `!tab.visible`, it represents the literally state of it having the + // attribute `hidden` set, which doesn't take into account the visibility + // of the tab itself. + return !(tab.visible || tab.hidden || tab.hasAttribute('zen-empty-tab')); + } + #populateTabsList(group) { const tabsList = this.#popup.querySelector('#zen-folder-tabs-list'); tabsList.replaceChildren(); for (const tab of group.tabs) { - if (tab.hidden || tab.hasAttribute('zen-empty-tab')) continue; + if (!this.#shouldAppearOnTabSearch(tab)) continue; const item = document.createElement('div'); item.className = 'folders-tabs-list-item'; @@ -894,6 +990,7 @@ item.addEventListener('click', () => { group.setAttribute('has-active', 'true'); gBrowser.selectedTab = tab; + this.expandToSelected(group); this.#popup.hidePopup(); }); @@ -981,8 +1078,12 @@ if (!gZenPinnedTabManager.expandedSidebarMode) { return; } - const tab = tabs[0]; + let tab = tabs[0]; let isTab = false; + if (tab.group?.hasAttribute('split-view-group')) { + tab = tab.group; + isTab = true; + } if (!groupElem && tab?.group) { groupElem = tab; // So we can set isTab later } @@ -1009,7 +1110,7 @@ const tabLevel = tabToAnimate?.group?.level || 0; const spacing = (level - tabLevel) * baseSpacing; for (const tab of tabs) { - if (gBrowser.isTabGroupLabel(tab)) { + if (gBrowser.isTabGroupLabel(tab) || tab.group?.hasAttribute('split-view-group')) { tab.group.style.setProperty('--zen-folder-indent', `${spacing}px`); continue; } @@ -1043,16 +1144,32 @@ } } - collapseVisibleTab(group, onlyIfActive = false) { + collapseVisibleTab(group, onlyIfActive = false, selectedTab) { if (!group?.isZenFolder) return; - if (onlyIfActive && !group.hasAttribute('has-active')) return; + if (onlyIfActive && group.activeGroups.length && selectedTab) { + for (const activeGroup of group.activeGroups) { + activeGroup.removeAttribute('has-active'); + selectedTab.style.removeProperty('--zen-folder-indent'); + this.collapseVisibleTab(activeGroup, true, selectedTab); + } + } + // 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)) return; const groupStart = group.querySelector('.zen-tab-group-start'); groupStart.setAttribute('old-margin', groupStart.style.marginTop); let itemHeight = 0; for (const item of group.allItems) { itemHeight += item.getBoundingClientRect().height; - if (item.hasAttribute('folder-active') && !item.selected) { + if (item.hasAttribute('folder-active') && (!item.selected || !onlyIfActive)) { item.removeAttribute('folder-active'); if (!onlyIfActive) { item.setAttribute('was-folder-active', 'true'); @@ -1067,13 +1184,19 @@ this.updateFolderIcon(group, 'close', false); } - gZenUIManager.motion.animate( - groupStart, - { - marginTop: newMargin, - }, - { duration: 0.15, ease: 'easeInOut' } - ); + gZenUIManager.motion + .animate( + groupStart, + { + marginTop: newMargin, + }, + { duration: 0.15, ease: 'easeInOut' } + ) + .then(() => { + selectedTab.style.removeProperty('--zen-folder-indent'); + }); + + gBrowser.tabContainer._invalidateCachedVisibleTabs(); } expandVisibleTab(group) { @@ -1099,74 +1222,134 @@ ); groupStart.removeAttribute('old-margin'); groupStart.removeAttribute('new-margin'); + + gBrowser.tabContainer._invalidateCachedVisibleTabs(); } - expandToSelected(group) { - const tabsContainer = group.querySelector('.tab-group-container'); - const animations = []; - const groupStart = group.querySelector('.zen-tab-group-start'); - let selectedItem = null; - let selectedGroupId = null; + async expandToSelected(group) { + if (!group?.isZenFolder) return; - const groupItems = []; - group.childGroupsAndTabs.forEach((item) => { - if (gBrowser.isTabGroupLabel(item)) { - if (item?.group?.hasAttribute('split-view-group')) { - item = item.group; - } else { - item = item.parentNode; + 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); + } + } + + // Always new selected item + let current = selectedItems?.at(-1)?.group; + while (current) { + const activeForGroup = selectedItems.filter((t) => current.contains(t)); + if (activeForGroup.length) { + current.activeTabs = activeForGroup; + + if (current.collapsed) { + const tabsContainer = current.querySelector('.tab-group-container'); + const groupStart = current.querySelector('.zen-tab-group-start'); + + if (tabsContainer.hasAttribute('hidden')) tabsContainer.removeAttribute('hidden'); + + let heightUntilSelected; + if (activeForGroup.length) { + const selectedItem = activeForGroup[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; + } + + animations.push(...this.updateFolderIcon(current, 'close', false)); + animations.push( + gZenUIManager.motion.animate( + groupStart, + { + marginTop: [0, -(heightUntilSelected + 4 * (selectedItems.length === 0 ? 1 : 0))], + }, + { duration: 0.1, ease: 'easeInOut' } + ) + ); + } + + for (const tab of activeForGroup) { + this.setFolderIndentation([tab], current, /* for collapse = */ true); } } - groupItems.push(item); - }); + current = current.group; + } - groupItems.map((item) => { - animations.push( - gZenUIManager.motion.animate( - item, - { - opacity: 1, - height: 'auto', - }, - { duration: 0.1, ease: 'easeInOut' } - ) - ); - }); + 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 items = group.childGroupsAndTabs.map((item) => { - const isSplitView = item.group?.hasAttribute?.('split-view-group'); - const splitGroupId = isSplitView ? item.group.id : null; - if (gBrowser.isTabGroupLabel(item) && !isSplitView) item = item.parentNode; - if (item.selected) { - selectedItem = item; - selectedGroupId = splitGroupId; + 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); } - return { item, splitGroupId }; - }); + } if (tabsContainer.hasAttribute('hidden')) { tabsContainer.removeAttribute('hidden'); } - const curMarginTop = parseInt(groupStart.style.marginTop) || 0; - - animations.push( - gZenUIManager.motion.animate( - groupStart, - { - marginTop: [curMarginTop, 0], - }, - { duration: 0.15, ease: 'easeInOut' } - ) - ); - - for (let { item, splitGroupId } of items) { - if (item === selectedItem || (selectedGroupId && splitGroupId === selectedGroupId)) { - continue; - } - - if (item && splitGroupId) item = item.group; + for (const item of groupItems) { + animations.push( + gZenUIManager.motion.animate( + item, + { + opacity: 1, + height: '', + }, + { duration: 0.1, ease: 'easeInOut' } + ) + ); + } + for (const item of itemsToHide) { animations.push( gZenUIManager.motion.animate( item, @@ -1179,12 +1362,35 @@ ); } - selectedItem.setAttribute('folder-active', 'true'); - group.setAttribute('selected-tab-id', selectedItem.getAttribute('zen-pin-id')); + let curMarginTop = parseInt(groupStart.style.marginTop) || 0; + animations.push( + gZenUIManager.motion + .animate( + groupStart, + { + marginTop: [curMarginTop, 0], + }, + { duration: 0.1, ease: 'linear' } + ) + .then(() => { + tabsContainer.style.overflow = ''; + }) + ); - animations.push(...this.updateFolderIcon(group, 'close', false)); + animations.push(...this.updateFolderIcon(group)); - return Promise.all(animations); + 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) { @@ -1413,10 +1619,12 @@ /** * Ungroup a tab from all the active groups it belongs to. - * @param {MozTabbrowserTab} tab The tab to ungroup. + * @param {MozTabbrowserTab[]} tabs The tab to ungroup. */ - ungroupTabFromActiveGroups(tab) { - gBrowser.ungroupTabsUntilNoActive(tab); + ungroupTabsFromActiveGroups(tabs) { + for (const tab of tabs) { + gBrowser.ungroupTabsUntilNoActive(tab); + } } /** diff --git a/src/zen/folders/zen-folders.css b/src/zen/folders/zen-folders.css index 68efcdc2d..42b0b99c4 100644 --- a/src/zen/folders/zen-folders.css +++ b/src/zen/folders/zen-folders.css @@ -10,7 +10,7 @@ tab-group[split-view-group] { transition: var(--zen-tabbox-element-indent-transition); #tabbrowser-tabs[movingtab] & { - transition: var(--zen-tabbox-element-indent-transition); + transition: var(--tab-dragover-transition), var(--zen-tabbox-element-indent-transition); } } --zen-split-view-active-tab-bg: color-mix( @@ -187,7 +187,7 @@ zen-folder { color-mix(in srgb, var(--zen-primary-color), black 20%) ); --zen-folder-stroke: light-dark( - color-mix(in srgb, var(--zen-primary-color) 70%, black), + color-mix(in srgb, var(--zen-primary-color) 60%, black), color-mix(in srgb, var(--zen-colors-primary) 20%, var(--toolbox-textcolor)) ); @@ -243,7 +243,8 @@ zen-folder { padding-block-end: 0 !important; margin: 0 !important; height: calc(var(--tab-block-margin) * 2 + var(--tab-min-height)); - padding-inline: var(--tab-group-label-padding); + padding-inline-start: var(--tab-group-label-padding); + padding-inline-end: calc(var(--tab-group-label-padding) * 2); align-items: center; font-weight: 600; @@ -339,6 +340,17 @@ zen-folder { overflow-y: clip; } } + + &[has-active] > .tab-group-label-container { + & .tab-reset-button { + display: flex; + opacity: 0; + } + + &:hover .tab-reset-button { + opacity: 1; + } + } } /* Tabs popup */ diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs index def370a5b..320f13d33 100644 --- a/src/zen/tabs/ZenPinnedTabManager.mjs +++ b/src/zen/tabs/ZenPinnedTabManager.mjs @@ -776,7 +776,11 @@ }, 3000); }); } - await gZenFolders.collapseVisibleTab(selectedTab.group, /* only if active */ true); + await gZenFolders.collapseVisibleTab( + selectedTab.group, + /* only if active */ true, + selectedTab + ); await gBrowser.explicitUnloadTabs([selectedTab]); selectedTab.removeAttribute('discarded'); } @@ -1350,8 +1354,8 @@ gZenWorkspaces.activeWorkspaceIndicator?.removeAttribute('open'); } - if (draggedTab) { - gZenFolders.ungroupTabFromActiveGroups(draggedTab); + if (draggedTab?._dragData?.movingTabs) { + gZenFolders.ungroupTabsFromActiveGroups(draggedTab._dragData.movingTabs); } let shouldAddDragOverElement = false; diff --git a/src/zen/tests/folders/browser.toml b/src/zen/tests/folders/browser.toml index 54eaad4d6..1de0d7f26 100644 --- a/src/zen/tests/folders/browser.toml +++ b/src/zen/tests/folders/browser.toml @@ -15,5 +15,7 @@ support-files = [ ["browser_folder_max_subfolders.js"] ["browser_folder_empty_tab.js"] ["browser_folder_multiselected.js"] +["browser_folder_visible_tabs.js"] +["browser_folder_level_checks.js"] ["browser_folder_issue_9885.js"] diff --git a/src/zen/tests/folders/browser_folder_level_checks.js b/src/zen/tests/folders/browser_folder_level_checks.js new file mode 100644 index 000000000..ac1ddeddf --- /dev/null +++ b/src/zen/tests/folders/browser_folder_level_checks.js @@ -0,0 +1,21 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +add_task(async function test_Issue_9885() { + const subfolder = await gZenFolders.createFolder([], { + renameFolder: false, + label: 'subfolder', + }); + const parent = await gZenFolders.createFolder([], { + renameFolder: false, + label: 'parent', + }); + parent.tabs[0].after(subfolder); + + Assert.equal(parent.level, 0, 'Parent folder should be at level 0'); + Assert.equal(subfolder.level, 1, 'Subfolder should be at level 1'); + + await removeFolder(parent); +}); diff --git a/src/zen/tests/folders/browser_folder_multiselected.js b/src/zen/tests/folders/browser_folder_multiselected.js index 757f60841..77d912179 100644 --- a/src/zen/tests/folders/browser_folder_multiselected.js +++ b/src/zen/tests/folders/browser_folder_multiselected.js @@ -19,7 +19,7 @@ add_task(async function test_Folder_Multiselected_Tabs() { await collapseEvent; ok(!tab2.multiselected, 'Tab 2 should not be multiselected'); - Assert.greater(gBrowser.multiSelectedTabsCount, 0, 'There should be 1 multiselected tab'); + Assert.equal(gBrowser.multiSelectedTabsCount, 0, 'There should be 1 multiselected tab'); for (const t of [tab1, tab2]) { BrowserTestUtils.removeTab(t); diff --git a/src/zen/tests/folders/browser_folder_visible_tabs.js b/src/zen/tests/folders/browser_folder_visible_tabs.js new file mode 100644 index 000000000..45ca8210d --- /dev/null +++ b/src/zen/tests/folders/browser_folder_visible_tabs.js @@ -0,0 +1,78 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +'use strict'; + +add_task(async function test_Not_Visible_Collapsed() { + const tab = BrowserTestUtils.addTab(gBrowser, 'data:text/html,tab1'); + const folder = await gZenFolders.createFolder([tab]); + + Assert.equal( + folder.tabs.length, + 2, + 'Subfolder contains the tab and the empty tab created by Zen Folders' + ); + ok(tab.visible, 'Tab is visible in the folder'); + + folder.collapsed = true; + ok(!tab.visible, 'Tab is not visible in the folder when collapsed'); + await removeFolder(folder); +}); + +add_task(async function test_Visible_Selected() { + const originalTab = gBrowser.selectedTab; + const tab = BrowserTestUtils.addTab(gBrowser, 'data:text/html,tab1'); + const folder = await gZenFolders.createFolder([tab]); + + Assert.equal( + folder.tabs.length, + 2, + 'Subfolder contains the tab and the empty tab created by Zen Folders' + ); + ok(tab.visible, 'Tab is visible in the folder'); + gBrowser.selectedTab = tab; + folder.collapsed = true; + ok(tab.visible, 'Tab is visible in the folder when collapsed'); + ok(tab.hasAttribute('folder-active'), 'Tab is marked as active in the folder when selected'); + ok( + tab.group.hasAttribute('has-active'), + 'Tab group is marked as active when the tab is selected' + ); + Assert.deepEqual( + tab.group.activeTabs, + [tab], + 'Tab is included in the active tabs of the group when selected' + ); + + gBrowser.selectedTab = originalTab; + await removeFolder(folder); +}); + +add_task(async function test_Visible_Not_Selected() { + const originalTab = gBrowser.selectedTab; + const tab = BrowserTestUtils.addTab(gBrowser, 'data:text/html,tab1'); + const folder = await gZenFolders.createFolder([tab]); + + Assert.equal( + folder.tabs.length, + 2, + 'Subfolder contains the tab and the empty tab created by Zen Folders' + ); + ok(tab.visible, 'Tab is visible in the folder'); + gBrowser.selectedTab = tab; + folder.collapsed = true; + gBrowser.selectedTab = originalTab; + ok(tab.visible, 'Tab is visible in the folder when collapsed'); + ok(tab.hasAttribute('folder-active'), 'Tab is marked as active in the folder when selected'); + ok( + tab.group.hasAttribute('has-active'), + 'Tab group is marked as active when the tab is selected' + ); + Assert.deepEqual( + tab.group.activeTabs, + [tab], + 'Tab is included in the active tabs of the group when selected' + ); + + await removeFolder(folder); +});