mirror of
https://github.com/zen-browser/desktop.git
synced 2025-09-05 19:08:18 +00:00
feat: Multiselected tabs support for folders, p=#9941
* feat: Multiselected tabs support * refactor(folders): Store active tabs as direct references in folders and better handling collapsed items * fix: Refine tab visibility and state management * feat: Add ungroup for multiple selected tabs drag and drop, b=no-bug, c=folders, tabs * fix: Fixed wrong calculation of indentation and other small refactoring, b=no-bug, c=common, folders, tabs * fix: Fixed restoring split views, b=no-bug, c=folders, workspaces * test: Added tests for visible tab, b=no-bug, c=folders, tests, tabs * feat: Folder active tabs are always visible, b=no-bug, c=folders * fix: Fixed duplicate tabs appearing on the tab search list, b=no-bug, c=folders * feat: Added support for active folder unload, b=no-bug, c=folders * fix: first implementation `expandToSelected` * fix: Formatting * fix: Some minor fixes, b=no-bug, c=folders, tabs * fix: Improve collapse visible tab function, b=no-bug, c=folders * test: Added folder level tests, b=no-bug, c=folders, tests --------- Signed-off-by: mr. m <91018726+mr-cheffy@users.noreply.github.com> Co-authored-by: mr. m <91018726+mr-cheffy@users.noreply.github.com> Co-authored-by: Mr. M <mr.m@tuta.com>
This commit is contained in:
@@ -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",
|
||||
|
@@ -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,
|
||||
});
|
||||
|
@@ -9,6 +9,7 @@
|
||||
<hbox class="tab-group-label-container" pack="center">
|
||||
<html:div class="tab-group-folder-icon"/>
|
||||
<label class="tab-group-label" role="button"/>
|
||||
<image class="tab-reset-button reset-icon" role="button" keyNav="false"/>
|
||||
</hbox>
|
||||
<html:div class="tab-group-container">
|
||||
<html:div class="zen-tab-group-start" />
|
||||
@@ -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);
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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 */
|
||||
|
@@ -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;
|
||||
|
@@ -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"]
|
||||
|
21
src/zen/tests/folders/browser_folder_level_checks.js
Normal file
21
src/zen/tests/folders/browser_folder_level_checks.js
Normal file
@@ -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);
|
||||
});
|
@@ -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);
|
||||
|
78
src/zen/tests/folders/browser_folder_visible_tabs.js
Normal file
78
src/zen/tests/folders/browser_folder_visible_tabs.js
Normal file
@@ -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);
|
||||
});
|
Reference in New Issue
Block a user