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);
+});