mirror of
https://github.com/zen-browser/desktop.git
synced 2026-03-30 20:31:53 +00:00
283 lines
9.5 KiB
JavaScript
283 lines
9.5 KiB
JavaScript
// This Source Code Form is subject to the terms of the Mozilla Public
|
|
// License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
{
|
|
class ZenFolder extends MozTabbrowserTabGroup {
|
|
#initialized = false;
|
|
|
|
static markup = `
|
|
<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" data-l10n-id="zen-folders-unload-all-tooltip"/>
|
|
</hbox>
|
|
<html:div class="tab-group-container">
|
|
<html:div class="zen-tab-group-start" />
|
|
</html:div>
|
|
<vbox class="tab-group-overflow-count-container" pack="center">
|
|
<label class="tab-group-overflow-count" role="button" />
|
|
</vbox>
|
|
`;
|
|
|
|
static rawIcon = new DOMParser().parseFromString(
|
|
`
|
|
<svg width="28" height="28" viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
|
|
<defs>
|
|
<linearGradient gradientUnits="userSpaceOnUse" x1="14" y1="5.625" x2="14" y2="22.375" id="gradient-0">
|
|
<stop offset="0" style="stop-color: rgb(255, 255, 255)"/>
|
|
<stop offset="1" style="stop-color: rgb(0% 0% 0%)"/>
|
|
</linearGradient>
|
|
<linearGradient gradientUnits="userSpaceOnUse" x1="14" y1="9.625" x2="14" y2="22.375" id="gradient-1">
|
|
<stop offset="0" style="stop-color: rgb(255, 255, 255)"/>
|
|
<stop offset="1" style="stop-color: rgb(0% 0% 0%)"/>
|
|
</linearGradient>
|
|
</defs>
|
|
<!--Back Folder (path)-->
|
|
<path class="back" d="M8 5.625H11.9473C12.4866 5.625 13.0105 5.80861 13.4316 6.14551L14.2881 6.83105C14.9308 7.34508 15.7298 7.625 16.5527 7.625H20C21.3117 7.625 22.375 8.68832 22.375 10V20C22.375 21.3117 21.3117 22.375 20 22.375H8C6.68832 22.375 5.625 21.3117 5.625 20V8C5.625 6.68832 6.68832 5.625 8 5.625Z" style="fill: var(--zen-folder-behind-bgcolor);">
|
|
</path>
|
|
<path class="back" d="M8 5.625H11.9473C12.4866 5.625 13.0105 5.80861 13.4316 6.14551L14.2881 6.83105C14.9308 7.34508 15.7298 7.625 16.5527 7.625H20C21.3117 7.625 22.375 8.68832 22.375 10V20C22.375 21.3117 21.3117 22.375 20 22.375H8C6.68832 22.375 5.625 21.3117 5.625 20V8C5.625 6.68832 6.68832 5.625 8 5.625Z" style="stroke-width: 1.5px; stroke: var(--zen-folder-stroke); fill: url(#gradient-0); fill-opacity: 0.1;">
|
|
</path>
|
|
<!--Front Folder (rect)-->
|
|
<rect class="front" x="5.625" y="9.625" width="16.75" height="12.75" rx="2.375" style="fill: var(--zen-folder-front-bgcolor);">
|
|
</rect>
|
|
<rect class="front" x="5.625" y="9.625" width="16.75" height="12.75" rx="2.375" style="stroke-width: 1.5px; stroke: var(--zen-folder-stroke); fill: url(#gradient-1); fill-opacity: 0.1;">
|
|
</rect>
|
|
<!--Icon (g)-->
|
|
<g class="icon" style="fill: var(--zen-folder-stroke);">
|
|
<image href="" height="10" width="10"/>
|
|
</g>
|
|
<!--End Icon (g)-->
|
|
<g class="dots" style="fill: var(--zen-folder-stroke);">
|
|
<ellipse cx="10" cy="16" rx="1.25" ry="1.25"/>
|
|
<ellipse cx="14" cy="16" rx="1.25" ry="1.25"/>
|
|
<ellipse cx="18" cy="16" rx="1.25" ry="1.25"/>
|
|
</g>
|
|
</svg>`,
|
|
'image/svg+xml'
|
|
).documentElement;
|
|
|
|
constructor() {
|
|
super();
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this.labelElement.pinned = true;
|
|
if (this.#initialized) {
|
|
return;
|
|
}
|
|
this.#initialized = true;
|
|
this._activeTabs = [];
|
|
this.icon.appendChild(ZenFolder.rawIcon.cloneNode(true));
|
|
|
|
this.labelElement.parentElement.setAttribute('context', 'zenFolderActions');
|
|
|
|
this.labelElement.onRenameFinished = (newLabel) => {
|
|
this.name = newLabel.trim() || 'Folder';
|
|
const event = new CustomEvent('ZenFolderRenamed', {
|
|
bubbles: true,
|
|
});
|
|
this.dispatchEvent(event);
|
|
};
|
|
|
|
if (this.collapsed) {
|
|
this.querySelector('.tab-group-container').setAttribute('hidden', true);
|
|
}
|
|
}
|
|
|
|
get icon() {
|
|
return this.querySelector('.tab-group-folder-icon');
|
|
}
|
|
|
|
/**
|
|
* Returns the group this folder belongs to.
|
|
* @returns {MozTabbrowserTabGroup|null} The group this folder belongs to, or null if it is not part of a group.
|
|
**/
|
|
get group() {
|
|
if (gBrowser.isTabGroup(this.parentElement?.parentElement)) {
|
|
return this.parentElement.parentElement;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
get isZenFolder() {
|
|
return true;
|
|
}
|
|
|
|
get activeGroups() {
|
|
let activeGroups = [];
|
|
let currentGroup = this;
|
|
if (currentGroup?.hasAttribute('has-active')) activeGroups.push(currentGroup);
|
|
while (currentGroup?.group) {
|
|
currentGroup = currentGroup?.group;
|
|
if (currentGroup?.hasAttribute('has-active')) {
|
|
activeGroups.push(currentGroup);
|
|
}
|
|
}
|
|
return activeGroups;
|
|
}
|
|
|
|
get childActiveGroups() {
|
|
return Array.from(this.querySelectorAll('zen-folder[has-active]'));
|
|
}
|
|
|
|
rename() {
|
|
if (!document.documentElement.hasAttribute('zen-sidebar-expanded')) {
|
|
return;
|
|
}
|
|
gZenVerticalTabsManager.renameTabStart({
|
|
target: this.labelElement,
|
|
explicit: true,
|
|
});
|
|
}
|
|
|
|
createSubfolder() {
|
|
// We need to expand all parent folders
|
|
let currentFolder = this;
|
|
do {
|
|
currentFolder.collapsed = false;
|
|
currentFolder = currentFolder.group;
|
|
} while (currentFolder);
|
|
gZenFolders.createFolder([], {
|
|
renameFolder: !gZenUIManager.testingEnabled,
|
|
label: 'Subfolder',
|
|
insertAfter: this.querySelector('.tab-group-container').lastElementChild,
|
|
});
|
|
}
|
|
|
|
async unpackTabs() {
|
|
this.collapsed = false;
|
|
for (let tab of this.allItems.reverse()) {
|
|
tab = tab.group.hasAttribute('split-view-group') ? tab.group : tab;
|
|
if (tab.hasAttribute('zen-empty-tab')) {
|
|
await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
|
|
gBrowser.removeTab(tab);
|
|
} else {
|
|
gBrowser.ungroupTab(tab);
|
|
}
|
|
}
|
|
}
|
|
|
|
async delete() {
|
|
for (const tab of this.allItemsRecursive) {
|
|
await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
|
|
if (tab.hasAttribute('zen-empty-tab')) {
|
|
// Manually remove the empty tabs as removeTabs() inside removeTabGroup
|
|
// does ignore them.
|
|
gBrowser.removeTab(tab);
|
|
}
|
|
}
|
|
await gBrowser.removeTabGroup(this, { isUserTriggered: true });
|
|
}
|
|
|
|
get allItemsRecursive() {
|
|
const items = [];
|
|
for (const item of this.allItems) {
|
|
if (item.isZenFolder) {
|
|
items.push(item, ...item.allItemsRecursive);
|
|
} else {
|
|
items.push(item);
|
|
}
|
|
}
|
|
return items;
|
|
}
|
|
|
|
get allItems() {
|
|
return [...this.querySelector('.tab-group-container').children].filter(
|
|
(child) => !child.classList.contains('zen-tab-group-start')
|
|
);
|
|
}
|
|
|
|
get pinned() {
|
|
return this.isZenFolder;
|
|
}
|
|
|
|
/**
|
|
* Intentionally ignore attempts to change the pinned state.
|
|
* ZenFolder instances determine their "pinned" status based on their type (isZenFolder)
|
|
* and do not support being pinned or unpinned via this setter.
|
|
* This no-op setter ensures compatibility with interfaces expecting a pinned property,
|
|
* while preserving the invariant that ZenFolders cannot have their pinned state changed externally.
|
|
*/
|
|
set pinned(value) {}
|
|
|
|
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 {
|
|
const folders = new Map();
|
|
for (let tab of this._activeTabs) {
|
|
const group = tab?.group?.hasAttribute('split-view-group')
|
|
? tab?.group?.group
|
|
: tab?.group;
|
|
if (!folders.has(group?.id)) {
|
|
folders.set(group?.id, group?.activeGroups?.at(-1));
|
|
}
|
|
let activeGroup = folders.get(group?.id);
|
|
if (!activeGroup) {
|
|
tab.removeAttribute('folder-active');
|
|
tab.style.removeProperty('--zen-folder-indent');
|
|
}
|
|
}
|
|
this._activeTabs = [];
|
|
folders.clear();
|
|
}
|
|
}
|
|
|
|
get activeTabs() {
|
|
return this._activeTabs;
|
|
}
|
|
|
|
get resetButton() {
|
|
return this.labelElement.parentElement.querySelector('.tab-reset-button');
|
|
}
|
|
|
|
unloadAllTabs(event) {
|
|
this.#unloadAllActiveTabs(event, /* noClose */ true);
|
|
}
|
|
|
|
async #unloadAllActiveTabs(event, noClose = false) {
|
|
await gZenPinnedTabManager.onCloseTabShortcut(event, this.tabs, {
|
|
noClose,
|
|
alwaysUnload: true,
|
|
folderToUnload: this,
|
|
});
|
|
this.activeTabs = [];
|
|
}
|
|
|
|
on_click(event) {
|
|
if (event.target === this.resetButton) {
|
|
event.stopPropagation();
|
|
this.unloadAllTabs(event);
|
|
return;
|
|
}
|
|
super.on_click(event);
|
|
}
|
|
|
|
/**
|
|
* Get the root most collapsed folder in the tree.
|
|
* @returns {ZenFolder|null} The root most collapsed folder, or null if none are collapsed.
|
|
*/
|
|
get rootMostCollapsedFolder() {
|
|
let current = this;
|
|
let rootMost = null;
|
|
do {
|
|
if (current.collapsed) {
|
|
rootMost = current;
|
|
}
|
|
current = current.group;
|
|
} while (current);
|
|
return rootMost;
|
|
}
|
|
}
|
|
|
|
customElements.define('zen-folder', ZenFolder);
|
|
}
|