diff --git a/src/browser/themes/shared/zen-icons/lin/add-to-dictionary.svg b/src/browser/themes/shared/zen-icons/lin/add-to-dictionary.svg
deleted file mode 100644
index b73c7cffa8..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/add-to-dictionary.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/audio-save.svg b/src/browser/themes/shared/zen-icons/lin/audio-save.svg
deleted file mode 100644
index ca397dd27f..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/audio-save.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/checkmark.svg b/src/browser/themes/shared/zen-icons/lin/checkmark.svg
deleted file mode 100644
index c24575d4af..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/checkmark.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/close-all.svg b/src/browser/themes/shared/zen-icons/lin/close-all.svg
deleted file mode 100644
index c80f58b308..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/close-all.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/close.svg b/src/browser/themes/shared/zen-icons/lin/close.svg
index 7c535fa685..c4d0be066f 100644
--- a/src/browser/themes/shared/zen-icons/lin/close.svg
+++ b/src/browser/themes/shared/zen-icons/lin/close.svg
@@ -2,4 +2,4 @@
# 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/.
-
+
diff --git a/src/browser/themes/shared/zen-icons/lin/edit-redo.svg b/src/browser/themes/shared/zen-icons/lin/edit-redo.svg
deleted file mode 100644
index 85ca4753ba..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/edit-redo.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/edit-select-all.svg b/src/browser/themes/shared/zen-icons/lin/edit-select-all.svg
deleted file mode 100644
index 137a567ae5..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/edit-select-all.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/edit-undo.svg b/src/browser/themes/shared/zen-icons/lin/edit-undo.svg
deleted file mode 100644
index dd708e475d..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/edit-undo.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/edit.svg b/src/browser/themes/shared/zen-icons/lin/edit.svg
deleted file mode 100644
index a879756fc9..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/edit.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/ext-link.svg b/src/browser/themes/shared/zen-icons/lin/ext-link.svg
deleted file mode 100644
index 861778ca0a..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/ext-link.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/firefox.svg b/src/browser/themes/shared/zen-icons/lin/firefox.svg
deleted file mode 100644
index 09cd36756c..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/firefox.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/heart-circle-fill.svg b/src/browser/themes/shared/zen-icons/lin/heart-circle-fill.svg
new file mode 100644
index 0000000000..e349c7d4c1
--- /dev/null
+++ b/src/browser/themes/shared/zen-icons/lin/heart-circle-fill.svg
@@ -0,0 +1,5 @@
+#filter dumbComments emptyLines substitution
+# 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/.
+
\ No newline at end of file
diff --git a/src/browser/themes/shared/zen-icons/lin/image-copy.svg b/src/browser/themes/shared/zen-icons/lin/image-copy.svg
deleted file mode 100644
index bde7794b0a..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/image-copy.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/image-open.svg b/src/browser/themes/shared/zen-icons/lin/image-open.svg
deleted file mode 100644
index 611a6d3569..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/image-open.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/image-save.svg b/src/browser/themes/shared/zen-icons/lin/image-save.svg
deleted file mode 100644
index e1ebb853be..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/image-save.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/inspect.svg b/src/browser/themes/shared/zen-icons/lin/inspect.svg
deleted file mode 100644
index c04dac0a2e..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/inspect.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/media-loop.svg b/src/browser/themes/shared/zen-icons/lin/media-loop.svg
deleted file mode 100644
index 4e560fc4aa..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/media-loop.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/media-pip.svg b/src/browser/themes/shared/zen-icons/lin/media-pip.svg
deleted file mode 100644
index f9e7ca263d..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/media-pip.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/media-speed.svg b/src/browser/themes/shared/zen-icons/lin/media-speed.svg
deleted file mode 100644
index e6544b8a5a..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/media-speed.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/menu-bar.svg b/src/browser/themes/shared/zen-icons/lin/menu-bar.svg
deleted file mode 100644
index 49152bb9b9..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/menu-bar.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/move-tab.svg b/src/browser/themes/shared/zen-icons/lin/move-tab.svg
deleted file mode 100644
index d4f361352e..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/move-tab.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/paste-and-go.svg b/src/browser/themes/shared/zen-icons/lin/paste-and-go.svg
deleted file mode 100644
index 5680448de9..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/paste-and-go.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/report.svg b/src/browser/themes/shared/zen-icons/lin/report.svg
deleted file mode 100644
index 06097d2ea5..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/report.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/source-code.svg b/src/browser/themes/shared/zen-icons/lin/source-code.svg
deleted file mode 100644
index 4259dcfed0..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/source-code.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/video-open.svg b/src/browser/themes/shared/zen-icons/lin/video-open.svg
deleted file mode 100644
index d4ffc2e0ca..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/video-open.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/video-save.svg b/src/browser/themes/shared/zen-icons/lin/video-save.svg
deleted file mode 100644
index 636097d3fa..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/video-save.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/browser/themes/shared/zen-icons/lin/zoom-control.svg b/src/browser/themes/shared/zen-icons/lin/zoom-control.svg
deleted file mode 100644
index b5a4b856ec..0000000000
--- a/src/browser/themes/shared/zen-icons/lin/zoom-control.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-#filter dumbComments emptyLines substitution
-# 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/.
-
diff --git a/src/zen/common/ZenHasPolyfill.mjs b/src/zen/common/ZenHasPolyfill.mjs
deleted file mode 100644
index 4513033e95..0000000000
--- a/src/zen/common/ZenHasPolyfill.mjs
+++ /dev/null
@@ -1,77 +0,0 @@
-// 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 nsHasPolyfill {
- constructor() {
- this.observers = [];
- this.idStore = 0;
- }
-
- /**
- * @param {{selector: string, exists: boolean}} descendantSelectors
- */
- observeSelectorExistence(element, descendantSelectors, stateAttribute, attributeFilter = []) {
- const updateState = () => {
- const exists = descendantSelectors.some(({ selector }) => {
- let selected = element.querySelector(selector);
- if (selected?.tagName?.toLowerCase() === 'menu') {
- return null;
- }
- return selected;
- });
- const { exists: shouldExist = true } = descendantSelectors;
- if (exists === shouldExist) {
- if (!element.hasAttribute(stateAttribute)) {
- gZenCompactModeManager._setElementExpandAttribute(element, true, stateAttribute);
- }
- } else {
- if (element.hasAttribute(stateAttribute)) {
- gZenCompactModeManager._setElementExpandAttribute(element, false, stateAttribute);
- }
- }
- };
-
- const observer = new MutationObserver(updateState);
- updateState();
- const observerId = this.idStore++;
- this.observers.push({
- id: observerId,
- observer,
- element,
- attributeFilter,
- });
- return observerId;
- }
-
- disconnectObserver(observerId) {
- const index = this.observers.findIndex((o) => o.id === observerId);
- if (index !== -1) {
- this.observers[index].observer.disconnect();
- }
- }
-
- connectObserver(observerId) {
- const observer = this.observers.find((o) => o.id === observerId);
- if (observer) {
- observer.observer.observe(observer.element, {
- childList: true,
- subtree: true,
- attributes: true,
- attributeFilter: observer.attributeFilter.length ? observer.attributeFilter : undefined,
- });
- }
- }
-
- destroy() {
- this.observers.forEach((observer) => observer.observer.disconnect());
- this.observers = [];
- }
- }
-
- const hasPolyfillInstance = new nsHasPolyfill();
- window.addEventListener('unload', () => hasPolyfillInstance.destroy(), { once: true });
-
- window.ZenHasPolyfill = hasPolyfillInstance;
-}
diff --git a/src/zen/common/ZenPreloadedScripts.js b/src/zen/common/ZenPreloadedScripts.js
index 90529c6de1..6d7dd4a7f1 100644
--- a/src/zen/common/ZenPreloadedScripts.js
+++ b/src/zen/common/ZenPreloadedScripts.js
@@ -5,5 +5,5 @@
// prettier-ignore
{
- Services.scriptloader.loadSubScript("chrome://browser/content/ZenStartup.mjs", this);
+ ChromeUtils.importESModule("chrome://browser/content/ZenStartup.mjs", { global: "current" });
}
diff --git a/src/zen/common/ZenSessionStore.mjs b/src/zen/common/ZenSessionStore.mjs
deleted file mode 100644
index 2b86d886ee..0000000000
--- a/src/zen/common/ZenSessionStore.mjs
+++ /dev/null
@@ -1,46 +0,0 @@
-// 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 ZenSessionStore extends nsZenPreloadedFeature {
- init() {
- this.#waitAndCleanup();
- }
-
- promiseInitialized = new Promise((resolve) => {
- this._resolveInitialized = resolve;
- });
-
- restoreInitialTabData(tab, tabData) {
- if (tabData.zenWorkspace) {
- tab.setAttribute('zen-workspace-id', tabData.zenWorkspace);
- }
- if (tabData.zenPinnedId) {
- tab.setAttribute('zen-pin-id', tabData.zenPinnedId);
- }
- if (tabData.zenHasStaticLabel) {
- tab.setAttribute('zen-has-static-label', 'true');
- }
- if (tabData.zenEssential) {
- tab.setAttribute('zen-essential', 'true');
- }
- if (tabData.zenDefaultUserContextId) {
- tab.setAttribute('zenDefaultUserContextId', 'true');
- }
- if (tabData.zenPinnedEntry) {
- tab.setAttribute('zen-pinned-entry', tabData.zenPinnedEntry);
- }
- }
-
- async #waitAndCleanup() {
- await SessionStore.promiseInitialized;
- this.#cleanup();
- }
-
- #cleanup() {
- this._resolveInitialized();
- }
- }
-
- window.gZenSessionStore = new ZenSessionStore();
-}
diff --git a/src/zen/common/ZenStartup.mjs b/src/zen/common/ZenStartup.mjs
deleted file mode 100644
index f1b6c5a0f2..0000000000
--- a/src/zen/common/ZenStartup.mjs
+++ /dev/null
@@ -1,227 +0,0 @@
-// 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/.
-{
- var gZenStartup = new (class {
- #watermarkIgnoreElements = ['zen-toast-container'];
- #hasInitializedLayout = false;
-
- isReady = false;
-
- async init() {
- // important: We do this to ensure that some firefox components
- // are initialized before we start our own initialization.
- // please, do not remove this line and if you do, make sure to
- // test the startup process.
- await new Promise((resolve) => setTimeout(resolve, 0));
- this.openWatermark();
- this.#initBrowserBackground();
- this.#changeSidebarLocation();
- this.#zenInitBrowserLayout();
- }
-
- #initBrowserBackground() {
- const background = document.createXULElement('box');
- background.id = 'zen-browser-background';
- background.classList.add('zen-browser-generic-background');
- const grain = document.createXULElement('box');
- grain.classList.add('zen-browser-grain');
- background.appendChild(grain);
- document.getElementById('browser').prepend(background);
- const toolbarBackground = background.cloneNode(true);
- toolbarBackground.removeAttribute('id');
- toolbarBackground.classList.add('zen-toolbar-background');
- document.getElementById('titlebar').prepend(toolbarBackground);
- }
-
- #zenInitBrowserLayout() {
- if (this.#hasInitializedLayout) return;
- this.#hasInitializedLayout = true;
- try {
- const kNavbarItems = ['nav-bar', 'PersonalToolbar'];
- const kNewContainerId = 'zen-appcontent-navbar-container';
- let newContainer = document.getElementById(kNewContainerId);
- for (let id of kNavbarItems) {
- const node = document.getElementById(id);
- console.assert(node, 'Could not find node with id: ' + id);
- if (!node) continue;
- newContainer.appendChild(node);
- }
-
- // Fix notification deck
- const deckTemplate = document.getElementById('tab-notification-deck-template');
- if (deckTemplate) {
- document.getElementById('zen-appcontent-wrapper').prepend(deckTemplate);
- }
-
- gZenWorkspaces.init();
- setTimeout(() => {
- gZenUIManager.init();
- this.#checkForWelcomePage();
- }, 0);
- } catch (e) {
- console.error('ZenThemeModifier: Error initializing browser layout', e);
- }
- if (gBrowserInit.delayedStartupFinished) {
- this.delayedStartupFinished();
- } else {
- Services.obs.addObserver(this, 'browser-delayed-startup-finished');
- }
- }
-
- observe(aSubject, aTopic) {
- // This nsIObserver method allows us to defer initialization until after
- // this window has finished painting and starting up.
- if (aTopic == 'browser-delayed-startup-finished' && aSubject == window) {
- Services.obs.removeObserver(this, 'browser-delayed-startup-finished');
- this.delayedStartupFinished();
- }
- }
-
- delayedStartupFinished() {
- gZenWorkspaces.promiseInitialized.then(async () => {
- await delayedStartupPromise;
- await SessionStore.promiseAllWindowsRestored;
- delete gZenUIManager.promiseInitialized;
- this.#initSearchBar();
- gZenCompactModeManager.init();
- // Fix for https://github.com/zen-browser/desktop/issues/7605, specially in compact mode
- if (gURLBar.hasAttribute('breakout-extend')) {
- gURLBar.focus();
- }
- // A bit of a hack to make sure the tabs toolbar is updated.
- // Just in case we didn't get the right size.
- gZenUIManager.updateTabsToolbar();
- this.closeWatermark();
- this.isReady = true;
- });
- }
-
- openWatermark() {
- if (!Services.prefs.getBoolPref('zen.watermark.enabled', false)) {
- document.documentElement.removeAttribute('zen-before-loaded');
- return;
- }
- for (let elem of document.querySelectorAll('#browser > *, #urlbar')) {
- elem.style.opacity = 0;
- }
- }
-
- closeWatermark() {
- document.documentElement.removeAttribute('zen-before-loaded');
- if (Services.prefs.getBoolPref('zen.watermark.enabled', false)) {
- let elementsToIgnore = this.#watermarkIgnoreElements.map((id) => '#' + id).join(', ');
- gZenUIManager.motion
- .animate(
- '#browser > *:not(' + elementsToIgnore + '), #urlbar, #tabbrowser-tabbox > *',
- {
- opacity: [0, 1],
- },
- {
- duration: 0.1,
- }
- )
- .then(() => {
- for (let elem of document.querySelectorAll(
- '#browser > *, #urlbar, #tabbrowser-tabbox > *'
- )) {
- elem.style.removeProperty('opacity');
- }
- });
- }
- window.requestAnimationFrame(() => {
- window.dispatchEvent(new window.Event('resize')); // To recalculate the layout
- });
- }
-
- #changeSidebarLocation() {
- const kElementsToAppend = ['sidebar-splitter', 'sidebar-box'];
-
- const browser = document.getElementById('browser');
- browser.prepend(gNavToolbox);
-
- const sidebarPanelWrapper = document.getElementById('tabbrowser-tabbox');
- for (let id of kElementsToAppend) {
- const elem = document.getElementById(id);
- if (elem) {
- sidebarPanelWrapper.prepend(elem);
- }
- }
- }
-
- #initSearchBar() {
- // Only focus the url bar
- gURLBar.focus();
- }
-
- #checkForWelcomePage() {
- if (!Services.prefs.getBoolPref('zen.welcome-screen.seen', false)) {
- Services.prefs.setBoolPref('zen.welcome-screen.seen', true);
- Services.prefs.setStringPref('zen.updates.last-build-id', Services.appinfo.appBuildID);
- Services.scriptloader.loadSubScript(
- 'chrome://browser/content/zen-components/ZenWelcome.mjs',
- window
- );
- } else {
- this.#createUpdateAnimation();
- }
- }
-
- async #createUpdateAnimation() {
- const appID = Services.appinfo.appBuildID;
- if (
- Services.prefs.getStringPref('zen.updates.last-build-id', '') === appID ||
- gZenUIManager.testingEnabled
- ) {
- return;
- }
- Services.prefs.setStringPref('zen.updates.last-build-id', appID);
- await gZenWorkspaces.promiseInitialized;
- const appWrapper = document.getElementById('zen-main-app-wrapper');
- const element = document.createElement('div');
- element.id = 'zen-update-animation';
- const elementBorder = document.createElement('div');
- elementBorder.id = 'zen-update-animation-border';
- requestIdleCallback(() => {
- if (gReduceMotion) {
- return;
- }
- appWrapper.appendChild(element);
- appWrapper.appendChild(elementBorder);
- Promise.all([
- gZenUIManager.motion.animate(
- '#zen-update-animation',
- {
- top: ['100%', '-50%'],
- opacity: [0.5, 1],
- },
- {
- duration: 0.35,
- }
- ),
- gZenUIManager.motion.animate(
- '#zen-update-animation-border',
- {
- '--background-top': ['150%', '-50%'],
- },
- {
- duration: 0.35,
- delay: 0.08,
- }
- ),
- ]).then(() => {
- element.remove();
- elementBorder.remove();
- });
- });
- }
- })();
-
- window.addEventListener(
- 'MozBeforeInitialXULLayout',
- () => {
- gZenStartup.init();
- },
- { once: true }
- );
-}
diff --git a/src/zen/common/emojis/ZenEmojiPicker.mjs b/src/zen/common/emojis/ZenEmojiPicker.mjs
index 8c31d4e18a..0f36a7cafe 100644
--- a/src/zen/common/emojis/ZenEmojiPicker.mjs
+++ b/src/zen/common/emojis/ZenEmojiPicker.mjs
@@ -1,9 +1,11 @@
// 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/.
-{
- // prettier-ignore
- const SVG_ICONS = [
+
+import { nsZenDOMOperatedFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
+
+// prettier-ignore
+const SVG_ICONS = [
"airplane.svg", "american-football.svg", "baseball.svg", "basket.svg",
"bed.svg", "bell.svg", "bookmark.svg", "book.svg",
"briefcase.svg", "brush.svg", "bug.svg", "build.svg",
@@ -28,204 +30,201 @@
"water.svg", "weight.svg",
];
- class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
- #panel;
+class nsZenEmojiPicker extends nsZenDOMOperatedFeature {
+ #panel;
- #anchor;
+ #anchor;
- #currentPromise = null;
- #currentPromiseResolve = null;
- #currentPromiseReject = null;
+ #currentPromise = null;
+ #currentPromiseResolve = null;
+ #currentPromiseReject = null;
- init() {
- this.#panel = document.getElementById('PanelUI-zen-emojis-picker');
- this.#panel.addEventListener('popupshowing', this);
- this.#panel.addEventListener('popuphidden', this);
- this.#panel.addEventListener('command', this);
- this.searchInput.addEventListener('input', this);
- }
+ init() {
+ this.#panel = document.getElementById('PanelUI-zen-emojis-picker');
+ this.#panel.addEventListener('popupshowing', this);
+ this.#panel.addEventListener('popuphidden', this);
+ this.#panel.addEventListener('command', this);
+ this.searchInput.addEventListener('input', this);
+ }
- handleEvent(event) {
- switch (event.type) {
- case 'popupshowing':
- this.#onPopupShowing(event);
- break;
- case 'popuphidden':
- this.#onPopupHidden(event);
- break;
- case 'command':
- if (event.target.id === 'PanelUI-zen-emojis-picker-none') {
- this.#selectEmoji(null);
- } else if (event.target.id === 'PanelUI-zen-emojis-picker-change-emojis') {
- this.#changePage(false);
- } else if (event.target.id === 'PanelUI-zen-emojis-picker-change-svg') {
- this.#changePage(true);
- }
- break;
- case 'input':
- this.#onSearchInput(event);
- break;
- }
+ handleEvent(event) {
+ switch (event.type) {
+ case 'popupshowing':
+ this.#onPopupShowing(event);
+ break;
+ case 'popuphidden':
+ this.#onPopupHidden(event);
+ break;
+ case 'command':
+ if (event.target.id === 'PanelUI-zen-emojis-picker-none') {
+ this.#selectEmoji(null);
+ } else if (event.target.id === 'PanelUI-zen-emojis-picker-change-emojis') {
+ this.#changePage(false);
+ } else if (event.target.id === 'PanelUI-zen-emojis-picker-change-svg') {
+ this.#changePage(true);
+ }
+ break;
+ case 'input':
+ this.#onSearchInput(event);
+ break;
}
+ }
- get #emojis() {
- if (this._emojis) {
- return this._emojis;
- }
- const lazy = {};
- Services.scriptloader.loadSubScript(
- 'chrome://browser/content/zen-components/ZenEmojisData.min.mjs',
- lazy
- );
- this._emojis = lazy.ZenEmojisData;
+ get #emojis() {
+ if (this._emojis) {
return this._emojis;
}
+ const lazy = {};
+ Services.scriptloader.loadSubScript(
+ 'chrome://browser/content/zen-components/ZenEmojisData.min.mjs',
+ lazy
+ );
+ this._emojis = lazy.ZenEmojisData;
+ return this._emojis;
+ }
- get emojiList() {
- return document.getElementById('PanelUI-zen-emojis-picker-list');
- }
+ get emojiList() {
+ return document.getElementById('PanelUI-zen-emojis-picker-list');
+ }
- get svgList() {
- return document.getElementById('PanelUI-zen-emojis-picker-svgs');
- }
+ get svgList() {
+ return document.getElementById('PanelUI-zen-emojis-picker-svgs');
+ }
- get searchInput() {
- return document.getElementById('PanelUI-zen-emojis-picker-search');
- }
+ get searchInput() {
+ return document.getElementById('PanelUI-zen-emojis-picker-search');
+ }
- #changePage(toSvg = false) {
- const itemToScroll = toSvg
- ? this.svgList
- : document
- .getElementById('PanelUI-zen-emojis-picker-pages')
- .querySelector('[emojis="true"]');
- itemToScroll.scrollIntoView({
- behavior: 'smooth',
- block: 'nearest',
- inline: 'start',
- });
- const button = document.getElementById(
- `PanelUI-zen-emojis-picker-change-${toSvg ? 'svg' : 'emojis'}`
- );
- const otherButton = document.getElementById(
- `PanelUI-zen-emojis-picker-change-${toSvg ? 'emojis' : 'svg'}`
- );
- button.classList.add('selected');
- otherButton.classList.remove('selected');
- }
+ #changePage(toSvg = false) {
+ const itemToScroll = toSvg
+ ? this.svgList
+ : document.getElementById('PanelUI-zen-emojis-picker-pages').querySelector('[emojis="true"]');
+ itemToScroll.scrollIntoView({
+ behavior: 'smooth',
+ block: 'nearest',
+ inline: 'start',
+ });
+ const button = document.getElementById(
+ `PanelUI-zen-emojis-picker-change-${toSvg ? 'svg' : 'emojis'}`
+ );
+ const otherButton = document.getElementById(
+ `PanelUI-zen-emojis-picker-change-${toSvg ? 'emojis' : 'svg'}`
+ );
+ button.classList.add('selected');
+ otherButton.classList.remove('selected');
+ }
- #clearEmojis() {
- delete this._emojis;
- }
+ #clearEmojis() {
+ delete this._emojis;
+ }
- #onSearchInput(event) {
- const input = event.target;
- const value = input.value.trim().toLowerCase();
- // search for emojis.tags and order by emojis.order
- const filteredEmojis = this.#emojis
- .filter((emoji) => {
- return emoji.tags.some((tag) => tag.toLowerCase().includes(value));
- })
- .sort((a, b) => a.order - b.order);
- for (const button of this.emojiList.children) {
- const buttonEmoji = button.getAttribute('label');
- const emojiObject = filteredEmojis.find((emoji) => emoji.emoji === buttonEmoji);
- if (emojiObject) {
- button.hidden = !emojiObject.tags.some((tag) => tag.toLowerCase().includes(value));
- button.style.order = emojiObject.order;
- } else {
- button.hidden = true;
- }
+ #onSearchInput(event) {
+ const input = event.target;
+ const value = input.value.trim().toLowerCase();
+ // search for emojis.tags and order by emojis.order
+ const filteredEmojis = this.#emojis
+ .filter((emoji) => {
+ return emoji.tags.some((tag) => tag.toLowerCase().includes(value));
+ })
+ .sort((a, b) => a.order - b.order);
+ for (const button of this.emojiList.children) {
+ const buttonEmoji = button.getAttribute('label');
+ const emojiObject = filteredEmojis.find((emoji) => emoji.emoji === buttonEmoji);
+ if (emojiObject) {
+ button.hidden = !emojiObject.tags.some((tag) => tag.toLowerCase().includes(value));
+ button.style.order = emojiObject.order;
+ } else {
+ button.hidden = true;
}
}
+ }
- // note: It's async on purpose so we can render the popup before processing the emojis
- async #onPopupShowing(event) {
- if (event.target !== this.#panel) return;
- this.searchInput.value = '';
- const allowEmojis = !this.#panel.hasAttribute('only-svg-icons');
- if (allowEmojis) {
- const emojiList = this.emojiList;
- for (const emoji of this.#emojis) {
- const item = document.createXULElement('toolbarbutton');
- item.className = 'toolbarbutton-1 zen-emojis-picker-emoji';
- item.setAttribute('label', emoji.emoji);
- item.setAttribute('tooltiptext', '');
- item.addEventListener('command', () => {
- this.#selectEmoji(emoji.emoji);
- });
- emojiList.appendChild(item);
- }
- setTimeout(() => {
- this.searchInput.focus();
- }, 500);
- }
- const svgList = this.svgList;
- for (const icon of SVG_ICONS) {
+ // note: It's async on purpose so we can render the popup before processing the emojis
+ async #onPopupShowing(event) {
+ if (event.target !== this.#panel) return;
+ this.searchInput.value = '';
+ const allowEmojis = !this.#panel.hasAttribute('only-svg-icons');
+ if (allowEmojis) {
+ const emojiList = this.emojiList;
+ for (const emoji of this.#emojis) {
const item = document.createXULElement('toolbarbutton');
- item.className = 'toolbarbutton-1 zen-emojis-picker-svg';
- item.setAttribute('label', icon);
+ item.className = 'toolbarbutton-1 zen-emojis-picker-emoji';
+ item.setAttribute('label', emoji.emoji);
item.setAttribute('tooltiptext', '');
- item.style.listStyleImage = `url(${this.getSVGURL(icon)})`;
- item.setAttribute('icon', icon);
item.addEventListener('command', () => {
- this.#selectEmoji(this.getSVGURL(icon));
+ this.#selectEmoji(emoji.emoji);
});
- svgList.appendChild(item);
+ emojiList.appendChild(item);
}
+ setTimeout(() => {
+ this.searchInput.focus();
+ }, 500);
+ }
+ const svgList = this.svgList;
+ for (const icon of SVG_ICONS) {
+ const item = document.createXULElement('toolbarbutton');
+ item.className = 'toolbarbutton-1 zen-emojis-picker-svg';
+ item.setAttribute('label', icon);
+ item.setAttribute('tooltiptext', '');
+ item.style.listStyleImage = `url(${this.getSVGURL(icon)})`;
+ item.setAttribute('icon', icon);
+ item.addEventListener('command', () => {
+ this.#selectEmoji(this.getSVGURL(icon));
+ });
+ svgList.appendChild(item);
}
+ }
- #onPopupHidden(event) {
- if (event.target !== this.#panel) return;
- this.#clearEmojis();
+ #onPopupHidden(event) {
+ if (event.target !== this.#panel) return;
+ this.#clearEmojis();
- this.#changePage(false);
+ this.#changePage(false);
- const emojiList = this.emojiList;
- emojiList.innerHTML = '';
+ const emojiList = this.emojiList;
+ emojiList.innerHTML = '';
- this.svgList.innerHTML = '';
+ this.svgList.innerHTML = '';
- if (this.#currentPromiseReject) {
- this.#currentPromiseReject(new Error('Emoji picker closed without selection'));
- }
+ if (this.#currentPromiseReject) {
+ this.#currentPromiseReject(new Error('Emoji picker closed without selection'));
+ }
- this.#currentPromise = null;
- this.#currentPromiseResolve = null;
- this.#currentPromiseReject = null;
+ this.#currentPromise = null;
+ this.#currentPromiseResolve = null;
+ this.#currentPromiseReject = null;
- this.#anchor.removeAttribute('zen-emoji-open');
- this.#anchor = null;
- }
+ this.#anchor.removeAttribute('zen-emoji-open');
+ this.#anchor = null;
+ }
- #selectEmoji(emoji) {
- this.#currentPromiseResolve?.(emoji);
- this.#panel.hidePopup();
- }
+ #selectEmoji(emoji) {
+ this.#currentPromiseResolve?.(emoji);
+ this.#panel.hidePopup();
+ }
- open(anchor, { onlySvgIcons = false } = {}) {
- if (this.#currentPromise) {
- return null;
- }
- this.#currentPromise = new Promise((resolve, reject) => {
- this.#currentPromiseResolve = resolve;
- this.#currentPromiseReject = reject;
- });
- this.#anchor = anchor;
- this.#anchor.setAttribute('zen-emoji-open', 'true');
- if (onlySvgIcons) {
- this.#panel.setAttribute('only-svg-icons', 'true');
- } else {
- this.#panel.removeAttribute('only-svg-icons');
- }
- this.#panel.openPopup(anchor, 'after_start', 0, 0, false, false);
- return this.#currentPromise;
+ open(anchor, { onlySvgIcons = false } = {}) {
+ if (this.#currentPromise) {
+ return null;
}
-
- getSVGURL(icon) {
- return `chrome://browser/skin/zen-icons/selectable/${icon}`;
+ this.#currentPromise = new Promise((resolve, reject) => {
+ this.#currentPromiseResolve = resolve;
+ this.#currentPromiseReject = reject;
+ });
+ this.#anchor = anchor;
+ this.#anchor.setAttribute('zen-emoji-open', 'true');
+ if (onlySvgIcons) {
+ this.#panel.setAttribute('only-svg-icons', 'true');
+ } else {
+ this.#panel.removeAttribute('only-svg-icons');
}
+ this.#panel.openPopup(anchor, 'after_start', 0, 0, false, false);
+ return this.#currentPromise;
}
- window.gZenEmojiPicker = new nsZenEmojiPicker();
+ getSVGURL(icon) {
+ return `chrome://browser/skin/zen-icons/selectable/${icon}`;
+ }
}
+
+window.gZenEmojiPicker = new nsZenEmojiPicker();
diff --git a/src/zen/common/emojis/fetch_emojis.py b/src/zen/common/emojis/fetch_emojis.py
index 5458de1d44..d057f6531b 100644
--- a/src/zen/common/emojis/fetch_emojis.py
+++ b/src/zen/common/emojis/fetch_emojis.py
@@ -1,4 +1,3 @@
-#
# 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/.
diff --git a/src/zen/common/jar.inc.mn b/src/zen/common/jar.inc.mn
new file mode 100644
index 0000000000..2d4172f8b9
--- /dev/null
+++ b/src/zen/common/jar.inc.mn
@@ -0,0 +1,36 @@
+# 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/.
+
+ content/browser/zenThemeModifier.js (../../zen/common/zenThemeModifier.js)
+ content/browser/ZenPreloadedScripts.js (../../zen/common/ZenPreloadedScripts.js)
+ content/browser/zen-sets.js (../../zen/common/zen-sets.js)
+
+ content/browser/ZenStartup.mjs (../../zen/common/modules/ZenStartup.mjs)
+ content/browser/ZenUpdates.mjs (../../zen/common/modules/ZenUpdates.mjs)
+ content/browser/ZenUIManager.mjs (../../zen/common/modules/ZenUIManager.mjs)
+ content/browser/zen-components/ZenCommonUtils.mjs (../../zen/common/modules/ZenCommonUtils.mjs)
+ content/browser/zen-components/ZenSessionStore.mjs (../../zen/common/modules/ZenSessionStore.mjs)
+ content/browser/zen-components/ZenHasPolyfill.mjs (../../zen/common/modules/ZenHasPolyfill.mjs)
+ content/browser/zen-components/ZenSidebarNotification.mjs (../../zen/common/modules/ZenSidebarNotification.mjs)
+
+ content/browser/zen-components/ZenEmojisData.min.mjs (../../zen/common/emojis/ZenEmojisData.min.mjs)
+ content/browser/zen-components/ZenEmojiPicker.mjs (../../zen/common/emojis/ZenEmojiPicker.mjs)
+
+* content/browser/zen-styles/zen-theme.css (../../zen/common/styles/zen-theme.css)
+ content/browser/zen-styles/zen-buttons.css (../../zen/common/styles/zen-buttons.css)
+ content/browser/zen-styles/zen-browser-ui.css (../../zen/common/styles/zen-browser-ui.css)
+ content/browser/zen-styles/zen-animations.css (../../zen/common/styles/zen-animations.css)
+ content/browser/zen-styles/zen-panel-ui.css (../../zen/common/styles/zen-panel-ui.css)
+ content/browser/zen-styles/zen-single-components.css (../../zen/common/styles/zen-single-components.css)
+ content/browser/zen-styles/zen-sidebar.css (../../zen/common/styles/zen-sidebar.css)
+ content/browser/zen-styles/zen-toolbar.css (../../zen/common/styles/zen-toolbar.css)
+ content/browser/zen-styles/zen-browser-container.css (../../zen/common/styles/zen-browser-container.css)
+ content/browser/zen-styles/zen-omnibox.css (../../zen/common/styles/zen-omnibox.css)
+ content/browser/zen-styles/zen-popup.css (../../zen/common/styles/zen-popup.css)
+ content/browser/zen-styles/zen-branding.css (../../zen/common/styles/zen-branding.css)
+ content/browser/zen-styles/zen-sidebar-notification.css (../../zen/common/styles/zen-sidebar-notification.css)
+
+ content/browser/zen-styles/zen-panels/bookmarks.css (../../zen/common/styles/zen-panels/bookmarks.css)
+ content/browser/zen-styles/zen-panels/print.css (../../zen/common/styles/zen-panels/print.css)
+ content/browser/zen-styles/zen-panels/dialog.css (../../zen/common/styles/zen-panels/dialog.css)
diff --git a/src/zen/common/ZenCommonUtils.mjs b/src/zen/common/modules/ZenCommonUtils.mjs
similarity index 93%
rename from src/zen/common/ZenCommonUtils.mjs
rename to src/zen/common/modules/ZenCommonUtils.mjs
index f6b7cdc034..e1ac284bc9 100644
--- a/src/zen/common/ZenCommonUtils.mjs
+++ b/src/zen/common/modules/ZenCommonUtils.mjs
@@ -15,8 +15,7 @@ window.gZenOperatingSystemCommonUtils = {
},
};
-/* eslint-disable no-unused-vars */
-class nsZenMultiWindowFeature {
+export class nsZenMultiWindowFeature {
constructor() {}
static get browsers() {
@@ -50,23 +49,21 @@ class nsZenMultiWindowFeature {
}
}
-/* eslint-disable no-unused-vars */
-class nsZenDOMOperatedFeature {
+export class nsZenDOMOperatedFeature {
constructor() {
var initBound = this.init.bind(this);
document.addEventListener('DOMContentLoaded', initBound, { once: true });
}
}
-/* eslint-disable no-unused-vars */
-class nsZenPreloadedFeature {
+export class nsZenPreloadedFeature {
constructor() {
var initBound = this.init.bind(this);
document.addEventListener('MozBeforeInitialXULLayout', initBound, { once: true });
}
}
-var gZenCommonActions = {
+window.gZenCommonActions = {
copyCurrentURLToClipboard() {
const [currentUrl, ClipboardHelper] = gURLBar.zenStrippedURI;
const displaySpec = currentUrl.displaySpec;
diff --git a/src/zen/common/modules/ZenHasPolyfill.mjs b/src/zen/common/modules/ZenHasPolyfill.mjs
new file mode 100644
index 0000000000..97035fabc1
--- /dev/null
+++ b/src/zen/common/modules/ZenHasPolyfill.mjs
@@ -0,0 +1,75 @@
+// 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 nsHasPolyfill {
+ constructor() {
+ this.observers = [];
+ this.idStore = 0;
+ }
+
+ /**
+ * @param {{selector: string, exists: boolean}} descendantSelectors
+ */
+ observeSelectorExistence(element, descendantSelectors, stateAttribute, attributeFilter = []) {
+ const updateState = () => {
+ const exists = descendantSelectors.some(({ selector }) => {
+ let selected = element.querySelector(selector);
+ if (selected?.tagName?.toLowerCase() === 'menu') {
+ return null;
+ }
+ return selected;
+ });
+ const { exists: shouldExist = true } = descendantSelectors;
+ if (exists === shouldExist) {
+ if (!element.hasAttribute(stateAttribute)) {
+ gZenCompactModeManager._setElementExpandAttribute(element, true, stateAttribute);
+ }
+ } else {
+ if (element.hasAttribute(stateAttribute)) {
+ gZenCompactModeManager._setElementExpandAttribute(element, false, stateAttribute);
+ }
+ }
+ };
+
+ const observer = new MutationObserver(updateState);
+ updateState();
+ const observerId = this.idStore++;
+ this.observers.push({
+ id: observerId,
+ observer,
+ element,
+ attributeFilter,
+ });
+ return observerId;
+ }
+
+ disconnectObserver(observerId) {
+ const index = this.observers.findIndex((o) => o.id === observerId);
+ if (index !== -1) {
+ this.observers[index].observer.disconnect();
+ }
+ }
+
+ connectObserver(observerId) {
+ const observer = this.observers.find((o) => o.id === observerId);
+ if (observer) {
+ observer.observer.observe(observer.element, {
+ childList: true,
+ subtree: true,
+ attributes: true,
+ attributeFilter: observer.attributeFilter.length ? observer.attributeFilter : undefined,
+ });
+ }
+ }
+
+ destroy() {
+ this.observers.forEach((observer) => observer.observer.disconnect());
+ this.observers = [];
+ }
+}
+
+const hasPolyfillInstance = new nsHasPolyfill();
+window.addEventListener('unload', () => hasPolyfillInstance.destroy(), { once: true });
+
+window.ZenHasPolyfill = hasPolyfillInstance;
diff --git a/src/zen/common/modules/ZenSessionStore.mjs b/src/zen/common/modules/ZenSessionStore.mjs
new file mode 100644
index 0000000000..daa0d0962c
--- /dev/null
+++ b/src/zen/common/modules/ZenSessionStore.mjs
@@ -0,0 +1,47 @@
+// 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/.
+
+import { nsZenPreloadedFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
+
+class ZenSessionStore extends nsZenPreloadedFeature {
+ init() {
+ this.#waitAndCleanup();
+ }
+
+ promiseInitialized = new Promise((resolve) => {
+ this._resolveInitialized = resolve;
+ });
+
+ restoreInitialTabData(tab, tabData) {
+ if (tabData.zenWorkspace) {
+ tab.setAttribute('zen-workspace-id', tabData.zenWorkspace);
+ }
+ if (tabData.zenPinnedId) {
+ tab.setAttribute('zen-pin-id', tabData.zenPinnedId);
+ }
+ if (tabData.zenHasStaticLabel) {
+ tab.setAttribute('zen-has-static-label', 'true');
+ }
+ if (tabData.zenEssential) {
+ tab.setAttribute('zen-essential', 'true');
+ }
+ if (tabData.zenDefaultUserContextId) {
+ tab.setAttribute('zenDefaultUserContextId', 'true');
+ }
+ if (tabData.zenPinnedEntry) {
+ tab.setAttribute('zen-pinned-entry', tabData.zenPinnedEntry);
+ }
+ }
+
+ async #waitAndCleanup() {
+ await SessionStore.promiseInitialized;
+ this.#cleanup();
+ }
+
+ #cleanup() {
+ this._resolveInitialized();
+ }
+}
+
+window.gZenSessionStore = new ZenSessionStore();
diff --git a/src/zen/common/modules/ZenSidebarNotification.mjs b/src/zen/common/modules/ZenSidebarNotification.mjs
new file mode 100644
index 0000000000..84c52864e1
--- /dev/null
+++ b/src/zen/common/modules/ZenSidebarNotification.mjs
@@ -0,0 +1,128 @@
+/* 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/. */
+
+import { html } from 'chrome://global/content/vendor/lit.all.mjs';
+import { MozLitElement } from 'chrome://global/content/lit-utils.mjs';
+
+const lazy = {};
+
+ChromeUtils.defineLazyGetter(lazy, 'siblingElement', () => {
+ // All our notifications should be attached after the media controls toolbar
+ return document.getElementById('zen-media-controls-toolbar');
+});
+
+/**
+ * Zen Sidebar Notification Component
+ *
+ * Displays and takes care of animations for notifications that
+ * appear in the sidebar.
+ *
+ * @properties {headingL10nId} - The L10n ID for the heading text.
+ */
+class ZenSidebarNotification extends MozLitElement {
+ static properties = {
+ headingL10nId: { type: String, fluent: true },
+ links: { type: Array },
+ };
+
+ constructor({ headingL10nId = '', links = [] } = {}) {
+ super();
+ this.headingL10nId = headingL10nId;
+ this.links = links;
+ }
+
+ connectedCallback() {
+ super.connectedCallback();
+ if (this.parentElement) {
+ this.#animateIn();
+ }
+ }
+
+ remove() {
+ this.#animateOut().then(() => {
+ super.remove();
+ });
+ }
+
+ render() {
+ return html`
+
+
+
+ ${this.links.map(
+ (link) => html`
+
+ `
+ )}
+
+ `;
+ }
+
+ #animateIn() {
+ this.style.opacity = '0';
+ return gZenUIManager.motion.animate(
+ this,
+ {
+ opacity: [0, 1],
+ y: [50, 0],
+ },
+ {
+ delay: 1,
+ }
+ );
+ }
+
+ #animateOut() {
+ return gZenUIManager.motion.animate(
+ this,
+ {
+ opacity: [1, 0],
+ y: [0, 10],
+ },
+ {}
+ );
+ }
+}
+
+export default function createSidebarNotification(args) {
+ if (!gZenVerticalTabsManager._prefsSidebarExpanded) {
+ return null;
+ }
+
+ const notification = new ZenSidebarNotification(args);
+
+ lazy.siblingElement.insertAdjacentElement('afterend', notification);
+ return notification;
+}
+
+customElements.define('zen-sidebar-notification', ZenSidebarNotification);
diff --git a/src/zen/common/modules/ZenStartup.mjs b/src/zen/common/modules/ZenStartup.mjs
new file mode 100644
index 0000000000..942dd27eef
--- /dev/null
+++ b/src/zen/common/modules/ZenStartup.mjs
@@ -0,0 +1,189 @@
+// 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/.
+
+import checkForZenUpdates, {
+ createWindowUpdateAnimation,
+} from 'chrome://browser/content/ZenUpdates.mjs';
+
+class ZenStartup {
+ #watermarkIgnoreElements = ['zen-toast-container'];
+ #hasInitializedLayout = false;
+
+ isReady = false;
+
+ async init() {
+ // important: We do this to ensure that some firefox components
+ // are initialized before we start our own initialization.
+ // please, do not remove this line and if you do, make sure to
+ // test the startup process.
+ await new Promise((resolve) => setTimeout(resolve, 0));
+ this.openWatermark();
+ this.#initBrowserBackground();
+ this.#changeSidebarLocation();
+ this.#zenInitBrowserLayout();
+ }
+
+ #initBrowserBackground() {
+ const background = document.createXULElement('box');
+ background.id = 'zen-browser-background';
+ background.classList.add('zen-browser-generic-background');
+ const grain = document.createXULElement('box');
+ grain.classList.add('zen-browser-grain');
+ background.appendChild(grain);
+ document.getElementById('browser').prepend(background);
+ const toolbarBackground = background.cloneNode(true);
+ toolbarBackground.removeAttribute('id');
+ toolbarBackground.classList.add('zen-toolbar-background');
+ document.getElementById('titlebar').prepend(toolbarBackground);
+ }
+
+ #zenInitBrowserLayout() {
+ if (this.#hasInitializedLayout) return;
+ this.#hasInitializedLayout = true;
+ try {
+ const kNavbarItems = ['nav-bar', 'PersonalToolbar'];
+ const kNewContainerId = 'zen-appcontent-navbar-container';
+ let newContainer = document.getElementById(kNewContainerId);
+ for (let id of kNavbarItems) {
+ const node = document.getElementById(id);
+ console.assert(node, 'Could not find node with id: ' + id);
+ if (!node) continue;
+ newContainer.appendChild(node);
+ }
+
+ // Fix notification deck
+ const deckTemplate = document.getElementById('tab-notification-deck-template');
+ if (deckTemplate) {
+ document.getElementById('zen-appcontent-wrapper').prepend(deckTemplate);
+ }
+
+ gZenWorkspaces.init();
+ setTimeout(() => {
+ gZenUIManager.init();
+ this.#checkForWelcomePage();
+ }, 0);
+ } catch (e) {
+ console.error('ZenThemeModifier: Error initializing browser layout', e);
+ }
+ if (gBrowserInit.delayedStartupFinished) {
+ this.delayedStartupFinished();
+ } else {
+ Services.obs.addObserver(this, 'browser-delayed-startup-finished');
+ }
+ }
+
+ observe(aSubject, aTopic) {
+ // This nsIObserver method allows us to defer initialization until after
+ // this window has finished painting and starting up.
+ if (aTopic == 'browser-delayed-startup-finished' && aSubject == window) {
+ Services.obs.removeObserver(this, 'browser-delayed-startup-finished');
+ this.delayedStartupFinished();
+ }
+ }
+
+ delayedStartupFinished() {
+ gZenWorkspaces.promiseInitialized.then(async () => {
+ await delayedStartupPromise;
+ await SessionStore.promiseAllWindowsRestored;
+ delete gZenUIManager.promiseInitialized;
+ this.#initSearchBar();
+ gZenCompactModeManager.init();
+ // Fix for https://github.com/zen-browser/desktop/issues/7605, specially in compact mode
+ if (gURLBar.hasAttribute('breakout-extend')) {
+ gURLBar.focus();
+ }
+ // A bit of a hack to make sure the tabs toolbar is updated.
+ // Just in case we didn't get the right size.
+ gZenUIManager.updateTabsToolbar();
+ this.closeWatermark();
+ this.isReady = true;
+ });
+ }
+
+ openWatermark() {
+ if (!Services.prefs.getBoolPref('zen.watermark.enabled', false)) {
+ document.documentElement.removeAttribute('zen-before-loaded');
+ return;
+ }
+ for (let elem of document.querySelectorAll('#browser > *, #urlbar')) {
+ elem.style.opacity = 0;
+ }
+ }
+
+ closeWatermark() {
+ document.documentElement.removeAttribute('zen-before-loaded');
+ if (Services.prefs.getBoolPref('zen.watermark.enabled', false)) {
+ let elementsToIgnore = this.#watermarkIgnoreElements.map((id) => '#' + id).join(', ');
+ gZenUIManager.motion
+ .animate(
+ '#browser > *:not(' + elementsToIgnore + '), #urlbar, #tabbrowser-tabbox > *',
+ {
+ opacity: [0, 1],
+ },
+ {
+ duration: 0.1,
+ }
+ )
+ .then(() => {
+ for (let elem of document.querySelectorAll(
+ '#browser > *, #urlbar, #tabbrowser-tabbox > *'
+ )) {
+ elem.style.removeProperty('opacity');
+ }
+ });
+ }
+ window.requestAnimationFrame(() => {
+ window.dispatchEvent(new window.Event('resize')); // To recalculate the layout
+ });
+ }
+
+ #changeSidebarLocation() {
+ const kElementsToAppend = ['sidebar-splitter', 'sidebar-box'];
+
+ const browser = document.getElementById('browser');
+ browser.prepend(gNavToolbox);
+
+ const sidebarPanelWrapper = document.getElementById('tabbrowser-tabbox');
+ for (let id of kElementsToAppend) {
+ const elem = document.getElementById(id);
+ if (elem) {
+ sidebarPanelWrapper.prepend(elem);
+ }
+ }
+ }
+
+ #initSearchBar() {
+ // Only focus the url bar
+ gURLBar.focus();
+ }
+
+ #checkForWelcomePage() {
+ if (!Services.prefs.getBoolPref('zen.welcome-screen.seen', false)) {
+ Services.prefs.setBoolPref('zen.welcome-screen.seen', true);
+ Services.prefs.setStringPref('zen.updates.last-build-id', Services.appinfo.appBuildID);
+ Services.prefs.setStringPref('zen.updates.last-version', Services.appinfo.version);
+ Services.scriptloader.loadSubScript(
+ 'chrome://browser/content/zen-components/ZenWelcome.mjs',
+ window
+ );
+ } else {
+ this.#createUpdateAnimation();
+ }
+ }
+
+ async #createUpdateAnimation() {
+ checkForZenUpdates();
+ return await createWindowUpdateAnimation();
+ }
+}
+
+window.gZenStartup = new ZenStartup();
+
+window.addEventListener(
+ 'MozBeforeInitialXULLayout',
+ () => {
+ gZenStartup.init();
+ },
+ { once: true }
+);
diff --git a/src/zen/common/ZenUIManager.mjs b/src/zen/common/modules/ZenUIManager.mjs
similarity index 99%
rename from src/zen/common/ZenUIManager.mjs
rename to src/zen/common/modules/ZenUIManager.mjs
index 238501278c..e16fc982f7 100644
--- a/src/zen/common/ZenUIManager.mjs
+++ b/src/zen/common/modules/ZenUIManager.mjs
@@ -2,7 +2,9 @@
// 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/.
-var gZenUIManager = {
+import { nsZenMultiWindowFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
+
+window.gZenUIManager = {
_popupTrackingElements: [],
_hoverPausedForExpand: false,
_hasLoadedDOM: false,
@@ -720,7 +722,7 @@ XPCOMUtils.defineLazyPreferenceGetter(
true
);
-var gZenVerticalTabsManager = {
+window.gZenVerticalTabsManager = {
init() {
this._multiWindowFeature = new nsZenMultiWindowFeature();
this._initWaitPromise();
diff --git a/src/zen/common/modules/ZenUpdates.mjs b/src/zen/common/modules/ZenUpdates.mjs
new file mode 100644
index 0000000000..82fbb95cac
--- /dev/null
+++ b/src/zen/common/modules/ZenUpdates.mjs
@@ -0,0 +1,89 @@
+// 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/.
+
+import createSidebarNotification from 'chrome://browser/content/zen-components/ZenSidebarNotification.mjs';
+
+const ZEN_UPDATE_PREF = 'zen.updates.last-version';
+const ZEN_BUILD_ID_PREF = 'zen.updates.last-build-id';
+const ZEN_UPDATE_SHOW = 'zen.updates.show-update-notification';
+
+export default function checkForZenUpdates() {
+ const version = Services.appinfo.version;
+ const lastVersion = Services.prefs.getStringPref(ZEN_UPDATE_PREF, '');
+ Services.prefs.setStringPref(ZEN_UPDATE_PREF, version);
+ if (
+ version !== lastVersion &&
+ !gZenUIManager.testingEnabled &&
+ Services.prefs.getBoolPref(ZEN_UPDATE_SHOW, true)
+ ) {
+ const updateUrl = Services.prefs.getStringPref('app.releaseNotesURL.prompt', '');
+ createSidebarNotification({
+ headingL10nId: 'zen-sidebar-notification-updated-heading',
+ links: [
+ {
+ url: Services.urlFormatter.formatURL(updateUrl.replace('%VERSION%', version)),
+ l10nId: 'zen-sidebar-notification-updated',
+ special: true,
+ icon: 'chrome://browser/skin/zen-icons/heart-circle-fill.svg',
+ },
+ {
+ action: () => {
+ Services.obs.notifyObservers(window, 'restart-in-safe-mode');
+ },
+ l10nId: 'zen-sidebar-notification-restart-safe-mode',
+ icon: 'chrome://browser/skin/zen-icons/security-broken.svg',
+ },
+ ],
+ });
+ }
+}
+
+export async function createWindowUpdateAnimation() {
+ const appID = Services.appinfo.appBuildID;
+ if (
+ Services.prefs.getStringPref(ZEN_BUILD_ID_PREF, '') === appID ||
+ gZenUIManager.testingEnabled
+ ) {
+ return;
+ }
+ Services.prefs.setStringPref(ZEN_BUILD_ID_PREF, appID);
+ await gZenWorkspaces.promiseInitialized;
+ const appWrapper = document.getElementById('zen-main-app-wrapper');
+ const element = document.createElement('div');
+ element.id = 'zen-update-animation';
+ const elementBorder = document.createElement('div');
+ elementBorder.id = 'zen-update-animation-border';
+ requestIdleCallback(() => {
+ if (gReduceMotion) {
+ return;
+ }
+ appWrapper.appendChild(element);
+ appWrapper.appendChild(elementBorder);
+ Promise.all([
+ gZenUIManager.motion.animate(
+ '#zen-update-animation',
+ {
+ top: ['100%', '-50%'],
+ opacity: [0.5, 1],
+ },
+ {
+ duration: 0.35,
+ }
+ ),
+ gZenUIManager.motion.animate(
+ '#zen-update-animation-border',
+ {
+ '--background-top': ['150%', '-50%'],
+ },
+ {
+ duration: 0.35,
+ delay: 0.08,
+ }
+ ),
+ ]).then(() => {
+ element.remove();
+ elementBorder.remove();
+ });
+ });
+}
diff --git a/src/zen/common/moz.build b/src/zen/common/moz.build
index fa6ec309c1..8dc973b444 100644
--- a/src/zen/common/moz.build
+++ b/src/zen/common/moz.build
@@ -3,7 +3,7 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
EXTRA_JS_MODULES += [
- "ZenActorsManager.sys.mjs",
- "ZenCustomizableUI.sys.mjs",
- "ZenUIMigration.sys.mjs",
+ "sys/ZenActorsManager.sys.mjs",
+ "sys/ZenCustomizableUI.sys.mjs",
+ "sys/ZenUIMigration.sys.mjs",
]
diff --git a/src/zen/common/styles/zen-browser-ui.css b/src/zen/common/styles/zen-browser-ui.css
index 959c16d4eb..a46036d075 100644
--- a/src/zen/common/styles/zen-browser-ui.css
+++ b/src/zen/common/styles/zen-browser-ui.css
@@ -54,29 +54,27 @@
pointer-events: none;
}
- @media -moz-pref('zen.theme.gradient') {
+ &::after {
+ background: var(--zen-main-browser-background);
+ opacity: var(--zen-background-opacity);
+ transition: 0s;
+ }
+
+ &:is(.zen-toolbar-background) {
&::after {
- background: var(--zen-main-browser-background);
- opacity: var(--zen-background-opacity);
- transition: 0s;
+ background: var(--zen-main-browser-background-toolbar);
}
+ }
- &:is(.zen-toolbar-background) {
- &::after {
- background: var(--zen-main-browser-background-toolbar);
- }
- }
+ &::before {
+ background: var(--zen-main-browser-background-old);
+ opacity: calc(1 - var(--zen-background-opacity));
+ transition: 0s;
+ }
+ &:is(.zen-toolbar-background) {
&::before {
- background: var(--zen-main-browser-background-old);
- opacity: calc(1 - var(--zen-background-opacity));
- transition: 0s;
- }
-
- &:is(.zen-toolbar-background) {
- &::before {
- background: var(--zen-main-browser-background-toolbar-old);
- }
+ background: var(--zen-main-browser-background-toolbar-old);
}
}
diff --git a/src/zen/common/styles/zen-sidebar-notification.css b/src/zen/common/styles/zen-sidebar-notification.css
new file mode 100644
index 0000000000..a54e6b05fe
--- /dev/null
+++ b/src/zen/common/styles/zen-sidebar-notification.css
@@ -0,0 +1,122 @@
+/*
+ * 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/.
+ */
+
+@keyframes zen-text-gradient {
+ 0% {
+ background-position: 0% 50%;
+ }
+ 100% {
+ background-position: 100% 50%;
+ }
+}
+
+:host {
+ background: var(--zen-sidebar-notification-bg);
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+ border-radius: var(--border-radius-medium);
+ box-shadow: var(--zen-sidebar-notification-shadow);
+ font-size: 12px;
+}
+
+.zen-sidebar-notification-header {
+ display: flex;
+ position: relative;
+ flex: 1;
+ padding: 8px;
+ border-bottom: 1px solid color-mix(in srgb, currentColor 10%, transparent);
+}
+
+.zen-sidebar-notification-heading {
+ text-align: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-weight: 500;
+ flex: 1;
+}
+
+.zen-sidebar-notification-close-button {
+ position: absolute;
+ right: 2px;
+ top: 2px;
+ border-radius: 4px;
+ border-top-right-radius: calc(var(--border-radius-medium) - 2px);
+ z-index: 1;
+ padding: 4px;
+ width: 18px;
+ height: 18px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ opacity: 0.4;
+
+ &:hover {
+ background: color-mix(in srgb, currentColor 15%, transparent);
+ }
+
+ & img {
+ -moz-context-properties: fill, fill-opacity;
+ fill-opacity: 1;
+ fill: currentColor;
+ width: 14px;
+ pointer-events: none;
+ }
+}
+
+.zen-sidebar-notification-body {
+ padding: 6px;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+
+ & .zen-sidebar-notification-link-container {
+ cursor: pointer;
+ padding: 4px;
+ display: flex;
+ gap: 6px;
+ align-items: center;
+ transition: opacity 0.2s ease-in-out;
+ opacity: 0.7;
+
+ & .zen-sidebar-notification-link-text {
+ cursor: inherit;
+ }
+
+ &[special] {
+ opacity: 1;
+ --special-color: color-mix(in srgb, var(--zen-primary-color), currentColor 50%);
+ --specia-color-2: color-mix(in srgb, var(--zen-primary-color), currentColor 90%);
+ background: linear-gradient(
+ 135deg,
+ var(--specia-color-2),
+ var(--special-color),
+ var(--specia-color-2),
+ var(--special-color)
+ );
+ background-size: 400%;
+ filter: saturate(3);
+ background-clip: text;
+ /* Still works on firefox */
+ -webkit-text-fill-color: transparent;
+ animation: zen-text-gradient 2s linear infinite;
+ }
+
+ &:hover {
+ opacity: 1;
+ }
+
+ & .zen-sidebar-notification-link-icon {
+ pointer-events: none;
+ width: 16px;
+ height: 16px;
+ -moz-context-properties: fill, fill-opacity;
+ fill: var(--special-color, currentColor);
+ }
+ }
+}
diff --git a/src/zen/common/styles/zen-single-components.css b/src/zen/common/styles/zen-single-components.css
index 309c6a15fc..c6bb366a07 100644
--- a/src/zen/common/styles/zen-single-components.css
+++ b/src/zen/common/styles/zen-single-components.css
@@ -622,3 +622,8 @@ body > #confetti {
display: none;
}
}
+
+/* Sidebar notification */
+:root:not([zen-sidebar-expanded='true']) zen-sidebar-notification {
+ display: none;
+}
diff --git a/src/zen/common/styles/zen-theme.css b/src/zen/common/styles/zen-theme.css
index 8dda423713..93f4f8808d 100644
--- a/src/zen/common/styles/zen-theme.css
+++ b/src/zen/common/styles/zen-theme.css
@@ -242,6 +242,14 @@
/* Define tab hover background color */
--tab-hover-background-color: var(--toolbarbutton-hover-background);
+ /* Sidebar Notifications */
+ --zen-sidebar-notification-bg: color-mix(
+ in srgb,
+ var(--zen-primary-color) 15%,
+ light-dark(white, black)
+ );
+ --zen-sidebar-notification-shadow: 0 0 6px light-dark(rgba(0, 0, 0, 0.1), rgba(0, 0, 0, 0.3));
+
/* Nativity */
--zen-native-content-radius: var(--zen-border-radius);
@media (-moz-platform: linux) {
diff --git a/src/zen/common/ZenActorsManager.sys.mjs b/src/zen/common/sys/ZenActorsManager.sys.mjs
similarity index 94%
rename from src/zen/common/ZenActorsManager.sys.mjs
rename to src/zen/common/sys/ZenActorsManager.sys.mjs
index 2d7b8c10a2..f53f80af20 100644
--- a/src/zen/common/ZenActorsManager.sys.mjs
+++ b/src/zen/common/sys/ZenActorsManager.sys.mjs
@@ -45,9 +45,15 @@ let JSWINDOWACTORS = {
mousedown: {
capture: true,
},
+ mouseup: {
+ capture: true,
+ },
keydown: {
capture: true,
},
+ click: {
+ capture: true,
+ },
},
},
allFrames: true,
diff --git a/src/zen/common/ZenCustomizableUI.sys.mjs b/src/zen/common/sys/ZenCustomizableUI.sys.mjs
similarity index 99%
rename from src/zen/common/ZenCustomizableUI.sys.mjs
rename to src/zen/common/sys/ZenCustomizableUI.sys.mjs
index 4e5646ee87..bd7efc760d 100644
--- a/src/zen/common/ZenCustomizableUI.sys.mjs
+++ b/src/zen/common/sys/ZenCustomizableUI.sys.mjs
@@ -4,7 +4,7 @@
import { AppConstants } from 'resource://gre/modules/AppConstants.sys.mjs';
-export var ZenCustomizableUI = new (class {
+export const ZenCustomizableUI = new (class {
constructor() {}
TYPE_TOOLBAR = 'toolbar';
diff --git a/src/zen/common/ZenUIMigration.sys.mjs b/src/zen/common/sys/ZenUIMigration.sys.mjs
similarity index 100%
rename from src/zen/common/ZenUIMigration.sys.mjs
rename to src/zen/common/sys/ZenUIMigration.sys.mjs
diff --git a/src/zen/common/zenThemeModifier.js b/src/zen/common/zenThemeModifier.js
index fb736d7333..06f4674c4c 100644
--- a/src/zen/common/zenThemeModifier.js
+++ b/src/zen/common/zenThemeModifier.js
@@ -95,7 +95,7 @@ var ZenThemeModifier = {
const kMinElementSeparation = 0.1; // in px
let separation = this.elementSeparation;
if (
- window.fullScreen &&
+ document.documentElement.hasAttribute('inFullscreen') &&
window.gZenCompactModeManager?.preference &&
!document.getElementById('tabbrowser-tabbox')?.hasAttribute('zen-split-view') &&
Services.prefs.getBoolPref('zen.view.borderless-fullscreen', true)
diff --git a/src/zen/compact-mode/ZenCompactMode.mjs b/src/zen/compact-mode/ZenCompactMode.mjs
index baecbb6db2..f5f54fe053 100644
--- a/src/zen/compact-mode/ZenCompactMode.mjs
+++ b/src/zen/compact-mode/ZenCompactMode.mjs
@@ -2,201 +2,200 @@
* 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/. */
-{
- const lazy = {};
-
- XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- 'COMPACT_MODE_FLASH_DURATION',
- 'zen.view.compact.toolbar-flash-popup.duration',
- 800
- );
-
- XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- 'COMPACT_MODE_FLASH_ENABLED',
- 'zen.view.compact.toolbar-flash-popup',
- true
- );
-
- XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- 'COMPACT_MODE_CAN_ANIMATE_SIDEBAR',
- 'zen.view.compact.animate-sidebar',
- true
- );
-
- XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- 'COMPACT_MODE_SHOW_SIDEBAR_AND_TOOLBAR_ON_HOVER',
- 'zen.view.compact.show-sidebar-and-toolbar-on-hover',
- true
- );
-
- ChromeUtils.defineLazyGetter(lazy, 'mainAppWrapper', () =>
- document.getElementById('zen-main-app-wrapper')
- );
-
- window.gZenCompactModeManager = {
- _flashTimeouts: {},
- _eventListeners: [],
- _removeHoverFrames: {},
-
- // Delay to avoid flickering when hovering over the sidebar
- HOVER_HACK_DELAY: Services.prefs.getIntPref('zen.view.compact.hover-hack-delay', 0),
-
- preInit() {
- this._wasInCompactMode = Services.prefs.getBoolPref(
- 'zen.view.compact.enable-at-startup',
- false
- );
- this._canDebugLog = Services.prefs.getBoolPref('zen.view.compact.debug', false);
-
- this.addContextMenu();
- },
-
- init() {
- this.addMouseActions();
-
- const tabIsRightObserver = this._updateSidebarIsOnRight.bind(this);
- Services.prefs.addObserver('zen.tabs.vertical.right-side', tabIsRightObserver);
-
- window.addEventListener(
- 'unload',
- () => {
- Services.prefs.removeObserver('zen.tabs.vertical.right-side', tabIsRightObserver);
- },
- { once: true }
- );
-
- gZenUIManager.addPopupTrackingAttribute(this.sidebar);
- gZenUIManager.addPopupTrackingAttribute(
- document.getElementById('zen-appcontent-navbar-wrapper')
- );
-
- this.addHasPolyfillObserver();
-
- // Clear hover states when window state changes (minimize, maximize, etc.)
- window.addEventListener('sizemodechange', () => this._clearAllHoverStates());
-
- this._canShowBackgroundTabToast = Services.prefs.getBoolPref(
- 'zen.view.compact.show-background-tab-toast',
- true
- );
-
- if (AppConstants.platform == 'macosx') {
- window.addEventListener('mouseover', (event) => {
- const buttons = gZenVerticalTabsManager.actualWindowButtons;
- if (event.target.closest('.titlebar-buttonbox-container') === buttons) return;
- this._setElementExpandAttribute(buttons, false);
- });
- }
-
- SessionStore.promiseAllWindowsRestored.then(() => {
- this.preference = this._wasInCompactMode;
+const lazy = {};
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ 'COMPACT_MODE_FLASH_DURATION',
+ 'zen.view.compact.toolbar-flash-popup.duration',
+ 800
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ 'COMPACT_MODE_FLASH_ENABLED',
+ 'zen.view.compact.toolbar-flash-popup',
+ true
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ 'COMPACT_MODE_CAN_ANIMATE_SIDEBAR',
+ 'zen.view.compact.animate-sidebar',
+ true
+);
+
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ 'COMPACT_MODE_SHOW_SIDEBAR_AND_TOOLBAR_ON_HOVER',
+ 'zen.view.compact.show-sidebar-and-toolbar-on-hover',
+ true
+);
+
+ChromeUtils.defineLazyGetter(lazy, 'mainAppWrapper', () =>
+ document.getElementById('zen-main-app-wrapper')
+);
+
+window.gZenCompactModeManager = {
+ _flashTimeouts: {},
+ _eventListeners: [],
+ _removeHoverFrames: {},
+
+ // Delay to avoid flickering when hovering over the sidebar
+ HOVER_HACK_DELAY: Services.prefs.getIntPref('zen.view.compact.hover-hack-delay', 0),
+
+ preInit() {
+ this._wasInCompactMode = Services.prefs.getBoolPref(
+ 'zen.view.compact.enable-at-startup',
+ false
+ );
+ this._canDebugLog = Services.prefs.getBoolPref('zen.view.compact.debug', false);
+
+ this.addContextMenu();
+ },
+
+ init() {
+ this.addMouseActions();
+
+ const tabIsRightObserver = this._updateSidebarIsOnRight.bind(this);
+ Services.prefs.addObserver('zen.tabs.vertical.right-side', tabIsRightObserver);
+
+ window.addEventListener(
+ 'unload',
+ () => {
+ Services.prefs.removeObserver('zen.tabs.vertical.right-side', tabIsRightObserver);
+ },
+ { once: true }
+ );
+
+ gZenUIManager.addPopupTrackingAttribute(this.sidebar);
+ gZenUIManager.addPopupTrackingAttribute(
+ document.getElementById('zen-appcontent-navbar-wrapper')
+ );
+
+ this.addHasPolyfillObserver();
+
+ // Clear hover states when window state changes (minimize, maximize, etc.)
+ window.addEventListener('sizemodechange', () => this._clearAllHoverStates());
+
+ this._canShowBackgroundTabToast = Services.prefs.getBoolPref(
+ 'zen.view.compact.show-background-tab-toast',
+ true
+ );
+
+ if (AppConstants.platform == 'macosx') {
+ window.addEventListener('mouseover', (event) => {
+ const buttons = gZenVerticalTabsManager.actualWindowButtons;
+ if (event.target.closest('.titlebar-buttonbox-container') === buttons) return;
+ this._setElementExpandAttribute(buttons, false);
});
- },
-
- log(...args) {
- if (this._canDebugLog) {
- console.log('[Zen Compact Mode]', ...args);
+ }
+
+ SessionStore.promiseAllWindowsRestored.then(() => {
+ this.preference = this._wasInCompactMode;
+ });
+ },
+
+ log(...args) {
+ if (this._canDebugLog) {
+ console.log('[Zen Compact Mode]', ...args);
+ }
+ },
+
+ get preference() {
+ return document.documentElement.getAttribute('zen-compact-mode') === 'true';
+ },
+
+ get shouldBeCompact() {
+ return !document.documentElement.getAttribute('chromehidden')?.includes('toolbar');
+ },
+
+ set preference(value) {
+ if (!this.shouldBeCompact) {
+ value = false;
+ }
+ this.log('Setting compact mode preference to', value);
+ if (
+ this.preference === value ||
+ document.documentElement.hasAttribute('zen-compact-animating')
+ ) {
+ if (typeof this._wasInCompactMode !== 'undefined') {
+ // We wont do anything with it anyway, so we remove it
+ delete this._wasInCompactMode;
}
- },
-
- get preference() {
- return document.documentElement.getAttribute('zen-compact-mode') === 'true';
- },
-
- get shouldBeCompact() {
- return !document.documentElement.getAttribute('chromehidden')?.includes('toolbar');
- },
-
- set preference(value) {
- if (!this.shouldBeCompact) {
- value = false;
- }
- this.log('Setting compact mode preference to', value);
- if (
- this.preference === value ||
- document.documentElement.hasAttribute('zen-compact-animating')
- ) {
- if (typeof this._wasInCompactMode !== 'undefined') {
- // We wont do anything with it anyway, so we remove it
- delete this._wasInCompactMode;
- }
- delete this._ignoreNextHover;
- // We dont want the user to be able to spam the button
- return;
- }
- this.sidebar.removeAttribute('zen-user-show');
- // We use this element in order to make it persis across restarts, by using the XULStore.
- // main-window can't store attributes other than window sizes, so we use this instead
- lazy.mainAppWrapper.setAttribute('zen-compact-mode', value);
- document.documentElement.setAttribute('zen-compact-mode', value);
- if (typeof this._wasInCompactMode === 'undefined') {
- Services.prefs.setBoolPref('zen.view.compact.enable-at-startup', value);
- }
- this._updateEvent();
- },
-
- get sidebarIsOnRight() {
- if (typeof this._sidebarIsOnRight !== 'undefined') {
- return this._sidebarIsOnRight;
- }
- this._sidebarIsOnRight = Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
+ delete this._ignoreNextHover;
+ // We dont want the user to be able to spam the button
+ return;
+ }
+ this.sidebar.removeAttribute('zen-user-show');
+ // We use this element in order to make it persis across restarts, by using the XULStore.
+ // main-window can't store attributes other than window sizes, so we use this instead
+ lazy.mainAppWrapper.setAttribute('zen-compact-mode', value);
+ document.documentElement.setAttribute('zen-compact-mode', value);
+ if (typeof this._wasInCompactMode === 'undefined') {
+ Services.prefs.setBoolPref('zen.view.compact.enable-at-startup', value);
+ }
+ this._updateEvent();
+ },
+
+ get sidebarIsOnRight() {
+ if (typeof this._sidebarIsOnRight !== 'undefined') {
return this._sidebarIsOnRight;
- },
-
- get sidebar() {
- return gNavToolbox;
- },
-
- addHasPolyfillObserver() {
- const attributes = ['panelopen', 'open', 'breakout-extend', 'zen-floating-urlbar'];
- this.sidebarObserverId = ZenHasPolyfill.observeSelectorExistence(
- this.sidebar,
- [
- {
- selector:
- ":is([panelopen='true'], [open='true'], [breakout-extend='true']):not(#urlbar[zen-floating-urlbar='true']):not(tab):not(.zen-compact-mode-ignore)",
- },
- ],
- 'zen-compact-mode-active',
- attributes
- );
- this.toolbarObserverId = ZenHasPolyfill.observeSelectorExistence(
- document.getElementById('zen-appcontent-navbar-wrapper'),
- [
- {
- selector:
- ":is([panelopen='true'], [open='true'], #urlbar:focus-within, [breakout-extend='true']):not(.zen-compact-mode-ignore)",
- },
- ],
- 'zen-compact-mode-active',
- attributes
- );
- // Always connect this observer, we need it even if compact mode is disabled
- ZenHasPolyfill.connectObserver(this.toolbarObserverId);
- },
-
- flashSidebarIfNecessary(aInstant = false) {
- // This function is called after exiting DOM fullscreen mode,
- // so we do a bit of a hack to re-calculate the URL height
- if (aInstant) {
- gZenVerticalTabsManager.recalculateURLBarHeight();
- }
- if (
- !aInstant &&
- this.preference &&
- lazy.COMPACT_MODE_FLASH_ENABLED &&
- !gZenGlanceManager._animating
- ) {
- this.flashSidebar();
- }
- },
-
- addContextMenu() {
- const fragment = window.MozXULElement.parseXULToFragment(`
+ }
+ this._sidebarIsOnRight = Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
+ return this._sidebarIsOnRight;
+ },
+
+ get sidebar() {
+ return gNavToolbox;
+ },
+
+ addHasPolyfillObserver() {
+ const attributes = ['panelopen', 'open', 'breakout-extend', 'zen-floating-urlbar'];
+ this.sidebarObserverId = ZenHasPolyfill.observeSelectorExistence(
+ this.sidebar,
+ [
+ {
+ selector:
+ ":is([panelopen='true'], [open='true'], [breakout-extend='true']):not(#urlbar[zen-floating-urlbar='true']):not(tab):not(.zen-compact-mode-ignore)",
+ },
+ ],
+ 'zen-compact-mode-active',
+ attributes
+ );
+ this.toolbarObserverId = ZenHasPolyfill.observeSelectorExistence(
+ document.getElementById('zen-appcontent-navbar-wrapper'),
+ [
+ {
+ selector:
+ ":is([panelopen='true'], [open='true'], #urlbar:focus-within, [breakout-extend='true']):not(.zen-compact-mode-ignore)",
+ },
+ ],
+ 'zen-compact-mode-active',
+ attributes
+ );
+ // Always connect this observer, we need it even if compact mode is disabled
+ ZenHasPolyfill.connectObserver(this.toolbarObserverId);
+ },
+
+ flashSidebarIfNecessary(aInstant = false) {
+ // This function is called after exiting DOM fullscreen mode,
+ // so we do a bit of a hack to re-calculate the URL height
+ if (aInstant) {
+ gZenVerticalTabsManager.recalculateURLBarHeight();
+ }
+ if (
+ !aInstant &&
+ this.preference &&
+ lazy.COMPACT_MODE_FLASH_ENABLED &&
+ !gZenGlanceManager._animating
+ ) {
+ this.flashSidebar();
+ }
+ },
+
+ addContextMenu() {
+ const fragment = window.MozXULElement.parseXULToFragment(`
`);
- const idToAction = {
- 'zen-context-menu-compact-mode-hide-sidebar': this.hideSidebar.bind(this),
- 'zen-context-menu-compact-mode-hide-toolbar': this.hideToolbar.bind(this),
- 'zen-context-menu-compact-mode-hide-both': this.hideBoth.bind(this),
- };
-
- for (let menuitem of fragment.querySelectorAll('menuitem')) {
- if (menuitem.id in idToAction) {
- menuitem.addEventListener('command', idToAction[menuitem.id]);
- }
- }
+ const idToAction = {
+ 'zen-context-menu-compact-mode-hide-sidebar': this.hideSidebar.bind(this),
+ 'zen-context-menu-compact-mode-hide-toolbar': this.hideToolbar.bind(this),
+ 'zen-context-menu-compact-mode-hide-both': this.hideBoth.bind(this),
+ };
- document.getElementById('viewToolbarsMenuSeparator').before(fragment);
- this.updateContextMenu();
- },
-
- updateCompactModeContext(isSingleToolbar) {
- const isIllegalState = this.checkIfIllegalState();
- const menuitem = document.getElementById('zen-context-menu-compact-mode-toggle');
- const menu = document.getElementById('zen-context-menu-compact-mode');
- if (isSingleToolbar) {
- menu.setAttribute('hidden', 'true');
- menu.before(menuitem);
- } else {
- menu.removeAttribute('hidden');
- menu.querySelector('menupopup').prepend(menuitem);
+ for (let menuitem of fragment.querySelectorAll('menuitem')) {
+ if (menuitem.id in idToAction) {
+ menuitem.addEventListener('command', idToAction[menuitem.id]);
}
- const hideToolbarMenuItem = document.getElementById(
- 'zen-context-menu-compact-mode-hide-toolbar'
- );
- if (isIllegalState) {
- hideToolbarMenuItem.setAttribute('disabled', 'true');
- } else {
- hideToolbarMenuItem.removeAttribute('disabled');
- }
- },
-
- hideSidebar() {
+ }
+
+ document.getElementById('viewToolbarsMenuSeparator').before(fragment);
+ this.updateContextMenu();
+ },
+
+ updateCompactModeContext(isSingleToolbar) {
+ const isIllegalState = this.checkIfIllegalState();
+ const menuitem = document.getElementById('zen-context-menu-compact-mode-toggle');
+ const menu = document.getElementById('zen-context-menu-compact-mode');
+ if (isSingleToolbar) {
+ menu.setAttribute('hidden', 'true');
+ menu.before(menuitem);
+ } else {
+ menu.removeAttribute('hidden');
+ menu.querySelector('menupopup').prepend(menuitem);
+ }
+ const hideToolbarMenuItem = document.getElementById(
+ 'zen-context-menu-compact-mode-hide-toolbar'
+ );
+ if (isIllegalState) {
+ hideToolbarMenuItem.setAttribute('disabled', 'true');
+ } else {
+ hideToolbarMenuItem.removeAttribute('disabled');
+ }
+ },
+
+ hideSidebar() {
+ Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', true);
+ Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', false);
+ this.callAllEventListeners();
+ },
+
+ hideToolbar() {
+ Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', true);
+ Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', false);
+ this.callAllEventListeners();
+ },
+
+ hideBoth() {
+ Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', true);
+ Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', true);
+ this.callAllEventListeners();
+ },
+
+ /* Check for illegal states and fix them
+ * @returns {boolean} If the context menu should just show the "toggle" item
+ * instead of a submenu with hide options
+ */
+ checkIfIllegalState() {
+ // Due to how we layout the sidebar and toolbar, there are some states
+ // that are not allowed mainly due to the caption buttons not being accessible
+ // at the top left/right of the window.
+ const isSidebarExpanded = gZenVerticalTabsManager._prefsSidebarExpanded;
+ if (isSidebarExpanded) {
+ // Fast exit if the sidebar is expanded, as we dont have illegal states then
+ return false;
+ }
+ const canHideSidebar = this.canHideSidebar;
+ const canHideToolbar = this.canHideToolbar;
+ const isLeftSideButtons = !gZenVerticalTabsManager.isWindowsStyledButtons;
+ const isRightSidebar = gZenVerticalTabsManager._prefsRightSide;
+ // on macos: collapsed + left side + only toolbar
+ // on windows: collapsed + right side + only toolbar
+ const closelyIllegalState =
+ (isLeftSideButtons && !isRightSidebar) || (!isLeftSideButtons && isRightSidebar);
+ if (closelyIllegalState && canHideToolbar && !canHideSidebar) {
+ // This state is illegal
Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', true);
Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', false);
this.callAllEventListeners();
- },
-
- hideToolbar() {
- Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', true);
- Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', false);
+ return true;
+ }
+ return closelyIllegalState;
+ },
+
+ callAllEventListeners() {
+ this._eventListeners.forEach((callback) => callback());
+ },
+
+ addEventListener(callback) {
+ this._eventListeners.push(callback);
+ },
+
+ removeEventListener(callback) {
+ const index = this._eventListeners.indexOf(callback);
+ if (index !== -1) {
+ this._eventListeners.splice(index, 1);
+ }
+ },
+
+ async _updateEvent() {
+ const isUrlbarFocused = gURLBar.focused;
+ // IF we are animating IN, call the callbacks first so we can calculate the width
+ // once the window buttons are shown
+ this.updateContextMenu();
+ if (!this.preference) {
this.callAllEventListeners();
- },
-
- hideBoth() {
- Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', true);
- Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', true);
+ await this.animateCompactMode();
+ } else {
+ await this.animateCompactMode();
this.callAllEventListeners();
- },
-
- /* Check for illegal states and fix them
- * @returns {boolean} If the context menu should just show the "toggle" item
- * instead of a submenu with hide options
- */
- checkIfIllegalState() {
- // Due to how we layout the sidebar and toolbar, there are some states
- // that are not allowed mainly due to the caption buttons not being accessible
- // at the top left/right of the window.
- const isSidebarExpanded = gZenVerticalTabsManager._prefsSidebarExpanded;
- if (isSidebarExpanded) {
- // Fast exit if the sidebar is expanded, as we dont have illegal states then
- return false;
+ }
+ gZenUIManager.updateTabsToolbar();
+ if (isUrlbarFocused) {
+ gURLBar.focus();
+ }
+ if (this.preference) {
+ ZenHasPolyfill.connectObserver(this.sidebarObserverId);
+ } else {
+ ZenHasPolyfill.disconnectObserver(this.sidebarObserverId);
+ }
+ window.dispatchEvent(new CustomEvent('ZenCompactMode:Toggled', { detail: this.preference }));
+ },
+
+ // NOTE: Dont actually use event, it's just so we make sure
+ // the caller is from the ResizeObserver
+ getAndApplySidebarWidth(event = undefined) {
+ if (this._ignoreNextResize) {
+ delete this._ignoreNextResize;
+ return;
+ }
+ let sidebarWidth = this.sidebar.getBoundingClientRect().width;
+ const shouldRecalculate =
+ this.preference || document.documentElement.hasAttribute('zen-creating-workspace');
+ const sidebarExpanded = document.documentElement.hasAttribute('zen-sidebar-expanded');
+ if (sidebarWidth > 1) {
+ if (shouldRecalculate && sidebarExpanded) {
+ sidebarWidth = Math.max(sidebarWidth, 150);
}
- const canHideSidebar = this.canHideSidebar;
- const canHideToolbar = this.canHideToolbar;
- const isLeftSideButtons = !gZenVerticalTabsManager.isWindowsStyledButtons;
- const isRightSidebar = gZenVerticalTabsManager._prefsRightSide;
- // on macos: collapsed + left side + only toolbar
- // on windows: collapsed + right side + only toolbar
- const closelyIllegalState =
- (isLeftSideButtons && !isRightSidebar) || (!isLeftSideButtons && isRightSidebar);
- if (closelyIllegalState && canHideToolbar && !canHideSidebar) {
- // This state is illegal
- Services.prefs.setBoolPref('zen.view.compact.hide-tabbar', true);
- Services.prefs.setBoolPref('zen.view.compact.hide-toolbar', false);
- this.callAllEventListeners();
- return true;
- }
- return closelyIllegalState;
- },
-
- callAllEventListeners() {
- this._eventListeners.forEach((callback) => callback());
- },
-
- addEventListener(callback) {
- this._eventListeners.push(callback);
- },
-
- removeEventListener(callback) {
- const index = this._eventListeners.indexOf(callback);
- if (index !== -1) {
- this._eventListeners.splice(index, 1);
+ // Second variable to get the genuine width of the sidebar
+ this.sidebar.style.setProperty('--actual-zen-sidebar-width', `${sidebarWidth}px`);
+ window.dispatchEvent(new window.Event('resize')); // To recalculate the layout
+ if (
+ event &&
+ shouldRecalculate &&
+ sidebarExpanded &&
+ !gZenVerticalTabsManager._hadSidebarCollapse
+ ) {
+ return;
}
- },
-
- async _updateEvent() {
- const isUrlbarFocused = gURLBar.focused;
- // IF we are animating IN, call the callbacks first so we can calculate the width
- // once the window buttons are shown
- this.updateContextMenu();
- if (!this.preference) {
- this.callAllEventListeners();
- await this.animateCompactMode();
- } else {
- await this.animateCompactMode();
- this.callAllEventListeners();
+ delete gZenVerticalTabsManager._hadSidebarCollapse;
+ this.sidebar.style.setProperty('--zen-sidebar-width', `${sidebarWidth}px`);
+ }
+ return sidebarWidth;
+ },
+
+ get canHideSidebar() {
+ return (
+ Services.prefs.getBoolPref('zen.view.compact.hide-tabbar') ||
+ gZenVerticalTabsManager._hasSetSingleToolbar
+ );
+ },
+
+ get canHideToolbar() {
+ return (
+ Services.prefs.getBoolPref('zen.view.compact.hide-toolbar') &&
+ !gZenVerticalTabsManager._hasSetSingleToolbar
+ );
+ },
+
+ animateCompactMode() {
+ // Get the splitter width before hiding it (we need to hide it before animating on right)
+ document.documentElement.setAttribute('zen-compact-animating', 'true');
+ return new Promise((resolve) => {
+ // We need to set the splitter width before hiding it
+ let splitterWidth = document
+ .getElementById('zen-sidebar-splitter')
+ .getBoundingClientRect().width;
+ const isCompactMode = this.preference;
+ const canHideSidebar = this.canHideSidebar;
+ let canAnimate = lazy.COMPACT_MODE_CAN_ANIMATE_SIDEBAR && !this.isSidebarPotentiallyOpen();
+ if (typeof this._wasInCompactMode !== 'undefined') {
+ canAnimate = false;
+ delete this._wasInCompactMode;
}
- gZenUIManager.updateTabsToolbar();
- if (isUrlbarFocused) {
- gURLBar.focus();
+ // Do this so we can get the correct width ONCE compact mode styled have been applied
+ if (canAnimate) {
+ this.sidebar.setAttribute('animate', 'true');
}
- if (this.preference) {
- ZenHasPolyfill.connectObserver(this.sidebarObserverId);
- } else {
- ZenHasPolyfill.disconnectObserver(this.sidebarObserverId);
+ if (this._ignoreNextHover) {
+ this._setElementExpandAttribute(this.sidebar, false);
}
- window.dispatchEvent(new CustomEvent('ZenCompactMode:Toggled', { detail: this.preference }));
- },
-
- // NOTE: Dont actually use event, it's just so we make sure
- // the caller is from the ResizeObserver
- getAndApplySidebarWidth(event = undefined) {
- if (this._ignoreNextResize) {
+ this.sidebar.style.removeProperty('margin-right');
+ this.sidebar.style.removeProperty('margin-left');
+ this.sidebar.style.removeProperty('transform');
+ window.requestAnimationFrame(() => {
delete this._ignoreNextResize;
- return;
- }
- let sidebarWidth = this.sidebar.getBoundingClientRect().width;
- const shouldRecalculate =
- this.preference || document.documentElement.hasAttribute('zen-creating-workspace');
- const sidebarExpanded = document.documentElement.hasAttribute('zen-sidebar-expanded');
- if (sidebarWidth > 1) {
- if (shouldRecalculate && sidebarExpanded) {
- sidebarWidth = Math.max(sidebarWidth, 150);
- }
- // Second variable to get the genuine width of the sidebar
- this.sidebar.style.setProperty('--actual-zen-sidebar-width', `${sidebarWidth}px`);
- window.dispatchEvent(new window.Event('resize')); // To recalculate the layout
- if (
- event &&
- shouldRecalculate &&
- sidebarExpanded &&
- !gZenVerticalTabsManager._hadSidebarCollapse
- ) {
- return;
- }
- delete gZenVerticalTabsManager._hadSidebarCollapse;
- this.sidebar.style.setProperty('--zen-sidebar-width', `${sidebarWidth}px`);
- }
- return sidebarWidth;
- },
-
- get canHideSidebar() {
- return (
- Services.prefs.getBoolPref('zen.view.compact.hide-tabbar') ||
- gZenVerticalTabsManager._hasSetSingleToolbar
- );
- },
-
- get canHideToolbar() {
- return (
- Services.prefs.getBoolPref('zen.view.compact.hide-toolbar') &&
- !gZenVerticalTabsManager._hasSetSingleToolbar
- );
- },
-
- animateCompactMode() {
- // Get the splitter width before hiding it (we need to hide it before animating on right)
- document.documentElement.setAttribute('zen-compact-animating', 'true');
- return new Promise((resolve) => {
- // We need to set the splitter width before hiding it
- let splitterWidth = document
- .getElementById('zen-sidebar-splitter')
- .getBoundingClientRect().width;
- const isCompactMode = this.preference;
- const canHideSidebar = this.canHideSidebar;
- let canAnimate = lazy.COMPACT_MODE_CAN_ANIMATE_SIDEBAR && !this.isSidebarPotentiallyOpen();
- if (typeof this._wasInCompactMode !== 'undefined') {
- canAnimate = false;
- delete this._wasInCompactMode;
- }
- // Do this so we can get the correct width ONCE compact mode styled have been applied
- if (canAnimate) {
- this.sidebar.setAttribute('animate', 'true');
- }
- if (this._ignoreNextHover) {
- this._setElementExpandAttribute(this.sidebar, false);
- }
- this.sidebar.style.removeProperty('margin-right');
- this.sidebar.style.removeProperty('margin-left');
- this.sidebar.style.removeProperty('transform');
- window.requestAnimationFrame(() => {
- delete this._ignoreNextResize;
- let sidebarWidth = this.getAndApplySidebarWidth();
- const elementSeparation = ZenThemeModifier.elementSeparation;
- if (!canAnimate) {
- this.sidebar.removeAttribute('animate');
- document.documentElement.removeAttribute('zen-compact-animating');
+ let sidebarWidth = this.getAndApplySidebarWidth();
+ const elementSeparation = ZenThemeModifier.elementSeparation;
+ if (!canAnimate) {
+ this.sidebar.removeAttribute('animate');
+ document.documentElement.removeAttribute('zen-compact-animating');
- this.getAndApplySidebarWidth({});
- this._ignoreNextResize = true;
+ this.getAndApplySidebarWidth({});
+ this._ignoreNextResize = true;
- delete this._ignoreNextHover;
+ delete this._ignoreNextHover;
- resolve();
- return;
- }
- if (document.documentElement.hasAttribute('zen-sidebar-expanded')) {
- sidebarWidth -= 0.5 * splitterWidth;
- if (elementSeparation < splitterWidth) {
- // Subtract from the splitter width to end up with the correct element separation
- sidebarWidth += 1.5 * splitterWidth - elementSeparation;
- }
- } else {
- sidebarWidth -= elementSeparation;
+ resolve();
+ return;
+ }
+ if (document.documentElement.hasAttribute('zen-sidebar-expanded')) {
+ sidebarWidth -= 0.5 * splitterWidth;
+ if (elementSeparation < splitterWidth) {
+ // Subtract from the splitter width to end up with the correct element separation
+ sidebarWidth += 1.5 * splitterWidth - elementSeparation;
}
- if (canHideSidebar && isCompactMode) {
- this._setElementExpandAttribute(this.sidebar, false);
- gZenUIManager.motion
- .animate(
- this.sidebar,
- {
- marginRight: [0, this.sidebarIsOnRight ? `-${sidebarWidth}px` : 0],
- marginLeft: [0, this.sidebarIsOnRight ? 0 : `-${sidebarWidth}px`],
- },
- {
- ease: 'easeIn',
- type: 'spring',
- bounce: 0,
- duration: 0.12,
- }
- )
- .then(() => {
- this.sidebar.style.transition = 'none';
- this.sidebar.style.pointEvents = 'none';
- const titlebar = document.getElementById('titlebar');
- titlebar.style.visibility = 'hidden';
- titlebar.style.transition = 'none';
- this.sidebar.removeAttribute('animate');
- document.documentElement.removeAttribute('zen-compact-animating');
+ } else {
+ sidebarWidth -= elementSeparation;
+ }
+ if (canHideSidebar && isCompactMode) {
+ this._setElementExpandAttribute(this.sidebar, false);
+ gZenUIManager.motion
+ .animate(
+ this.sidebar,
+ {
+ marginRight: [0, this.sidebarIsOnRight ? `-${sidebarWidth}px` : 0],
+ marginLeft: [0, this.sidebarIsOnRight ? 0 : `-${sidebarWidth}px`],
+ },
+ {
+ ease: 'easeIn',
+ type: 'spring',
+ bounce: 0,
+ duration: 0.12,
+ }
+ )
+ .then(() => {
+ this.sidebar.style.transition = 'none';
+ this.sidebar.style.pointEvents = 'none';
+ const titlebar = document.getElementById('titlebar');
+ titlebar.style.visibility = 'hidden';
+ titlebar.style.transition = 'none';
+ this.sidebar.removeAttribute('animate');
+ document.documentElement.removeAttribute('zen-compact-animating');
+
+ setTimeout(() => {
+ this.getAndApplySidebarWidth({});
+ this._ignoreNextResize = true;
setTimeout(() => {
- this.getAndApplySidebarWidth({});
- this._ignoreNextResize = true;
-
- setTimeout(() => {
- if (this._ignoreNextHover) {
- setTimeout(() => {
- delete this._ignoreNextHover;
- });
- }
-
- this.sidebar.style.removeProperty('margin-right');
- this.sidebar.style.removeProperty('margin-left');
- this.sidebar.style.removeProperty('transition');
- this.sidebar.style.removeProperty('transform');
- this.sidebar.style.removeProperty('point-events');
+ if (this._ignoreNextHover) {
+ setTimeout(() => {
+ delete this._ignoreNextHover;
+ });
+ }
+
+ this.sidebar.style.removeProperty('margin-right');
+ this.sidebar.style.removeProperty('margin-left');
+ this.sidebar.style.removeProperty('transition');
+ this.sidebar.style.removeProperty('transform');
+ this.sidebar.style.removeProperty('point-events');
- titlebar.style.removeProperty('visibility');
- titlebar.style.removeProperty('transition');
+ titlebar.style.removeProperty('visibility');
+ titlebar.style.removeProperty('transition');
- gURLBar.textbox.style.removeProperty('visibility');
+ gURLBar.textbox.style.removeProperty('visibility');
- resolve();
- });
- });
- });
- } else if (canHideSidebar && !isCompactMode) {
- // Shouldn't be ever true, but just in case
- delete this._ignoreNextHover;
- document.getElementById('browser').style.overflow = 'clip';
- if (this.sidebarIsOnRight) {
- this.sidebar.style.marginRight = `-${sidebarWidth}px`;
- } else {
- this.sidebar.style.marginLeft = `-${sidebarWidth}px`;
- }
- gZenUIManager.motion
- .animate(
- this.sidebar,
- this.sidebarIsOnRight
- ? {
- marginRight: [`-${sidebarWidth}px`, 0],
- transform: ['translateX(100%)', 'translateX(0)'],
- }
- : { marginLeft: 0 },
- {
- ease: 'easeOut',
- type: 'spring',
- bounce: 0,
- duration: 0.12,
- }
- )
- .then(() => {
- this.sidebar.removeAttribute('animate');
- document.getElementById('browser').style.removeProperty('overflow');
- this.sidebar.style.transition = 'none';
- this.sidebar.style.removeProperty('margin-right');
- this.sidebar.style.removeProperty('margin-left');
- this.sidebar.style.removeProperty('transform');
- document.documentElement.removeAttribute('zen-compact-animating');
- setTimeout(() => {
- this.sidebar.style.removeProperty('transition');
resolve();
});
});
+ });
+ } else if (canHideSidebar && !isCompactMode) {
+ // Shouldn't be ever true, but just in case
+ delete this._ignoreNextHover;
+ document.getElementById('browser').style.overflow = 'clip';
+ if (this.sidebarIsOnRight) {
+ this.sidebar.style.marginRight = `-${sidebarWidth}px`;
} else {
- this.sidebar.removeAttribute('animate'); // remove the attribute if we are not animating
- document.documentElement.removeAttribute('zen-compact-animating');
- delete this._ignoreNextHover;
- resolve();
+ this.sidebar.style.marginLeft = `-${sidebarWidth}px`;
}
- });
+ gZenUIManager.motion
+ .animate(
+ this.sidebar,
+ this.sidebarIsOnRight
+ ? {
+ marginRight: [`-${sidebarWidth}px`, 0],
+ transform: ['translateX(100%)', 'translateX(0)'],
+ }
+ : { marginLeft: 0 },
+ {
+ ease: 'easeOut',
+ type: 'spring',
+ bounce: 0,
+ duration: 0.12,
+ }
+ )
+ .then(() => {
+ this.sidebar.removeAttribute('animate');
+ document.getElementById('browser').style.removeProperty('overflow');
+ this.sidebar.style.transition = 'none';
+ this.sidebar.style.removeProperty('margin-right');
+ this.sidebar.style.removeProperty('margin-left');
+ this.sidebar.style.removeProperty('transform');
+ document.documentElement.removeAttribute('zen-compact-animating');
+ setTimeout(() => {
+ this.sidebar.style.removeProperty('transition');
+ resolve();
+ });
+ });
+ } else {
+ this.sidebar.removeAttribute('animate'); // remove the attribute if we are not animating
+ document.documentElement.removeAttribute('zen-compact-animating');
+ delete this._ignoreNextHover;
+ resolve();
+ }
});
- },
-
- updateContextMenu() {
- document
- .getElementById('zen-context-menu-compact-mode-toggle')
- .setAttribute('checked', this.preference);
-
- const hideTabBar = this.canHideSidebar;
- const hideToolbar = this.canHideToolbar;
- const hideBoth = hideTabBar && hideToolbar;
-
- const idName = 'zen-context-menu-compact-mode-hide-';
- const sidebarItem = document.getElementById(idName + 'sidebar');
- const toolbarItem = document.getElementById(idName + 'toolbar');
- const bothItem = document.getElementById(idName + 'both');
- sidebarItem.setAttribute('checked', !hideBoth && hideTabBar);
- toolbarItem.setAttribute('checked', !hideBoth && hideToolbar);
- bothItem.setAttribute('checked', hideBoth);
- },
-
- _removeOpenStateOnUnifiedExtensions() {
- // Fix for bug https://github.com/zen-browser/desktop/issues/1925
- const buttons = document.querySelectorAll(
- 'toolbarbutton:is(#unified-extensions-button, .webextension-browser-action)'
- );
- for (let button of buttons) {
- button.removeAttribute('open');
+ });
+ },
+
+ updateContextMenu() {
+ document
+ .getElementById('zen-context-menu-compact-mode-toggle')
+ .setAttribute('checked', this.preference);
+
+ const hideTabBar = this.canHideSidebar;
+ const hideToolbar = this.canHideToolbar;
+ const hideBoth = hideTabBar && hideToolbar;
+
+ const idName = 'zen-context-menu-compact-mode-hide-';
+ const sidebarItem = document.getElementById(idName + 'sidebar');
+ const toolbarItem = document.getElementById(idName + 'toolbar');
+ const bothItem = document.getElementById(idName + 'both');
+ sidebarItem.setAttribute('checked', !hideBoth && hideTabBar);
+ toolbarItem.setAttribute('checked', !hideBoth && hideToolbar);
+ bothItem.setAttribute('checked', hideBoth);
+ },
+
+ _removeOpenStateOnUnifiedExtensions() {
+ // Fix for bug https://github.com/zen-browser/desktop/issues/1925
+ const buttons = document.querySelectorAll(
+ 'toolbarbutton:is(#unified-extensions-button, .webextension-browser-action)'
+ );
+ for (let button of buttons) {
+ button.removeAttribute('open');
+ }
+ },
+
+ toggle(ignoreHover = false) {
+ // Only ignore the next hover when we are enabling compact mode
+ this._ignoreNextHover = ignoreHover && !this.preference;
+ return (this.preference = !this.preference);
+ },
+
+ _updateSidebarIsOnRight() {
+ this._sidebarIsOnRight = Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
+ },
+
+ toggleSidebar() {
+ this.sidebar.toggleAttribute('zen-user-show');
+ },
+
+ get hideAfterHoverDuration() {
+ if (this._hideAfterHoverDuration) {
+ return this._hideAfterHoverDuration;
+ }
+ return Services.prefs.getIntPref('zen.view.compact.toolbar-hide-after-hover.duration');
+ },
+
+ get hoverableElements() {
+ return [
+ {
+ element: this.sidebar,
+ screenEdge: this.sidebarIsOnRight ? 'right' : 'left',
+ keepHoverDuration: 100,
+ },
+ {
+ element: document.getElementById('zen-appcontent-navbar-wrapper'),
+ screenEdge: 'top',
+ },
+ {
+ element: gZenVerticalTabsManager.actualWindowButtons,
+ },
+ ];
+ },
+
+ flashSidebar(duration = lazy.COMPACT_MODE_FLASH_DURATION) {
+ let tabPanels = document.getElementById('tabbrowser-tabpanels');
+ if (!tabPanels.matches("[zen-split-view='true']")) {
+ this.flashElement(this.sidebar, duration, this.sidebar.id);
+ }
+ },
+
+ flashElement(element, duration, id, attrName = 'flash-popup') {
+ if (this._flashTimeouts[id]) {
+ clearTimeout(this._flashTimeouts[id]);
+ } else {
+ requestAnimationFrame(() => this._setElementExpandAttribute(element, true, attrName));
+ }
+ this._flashTimeouts[id] = setTimeout(() => {
+ window.requestAnimationFrame(() => {
+ this._setElementExpandAttribute(element, false, attrName);
+ this._flashTimeouts[id] = null;
+ });
+ }, duration);
+ },
+
+ clearFlashTimeout(id) {
+ clearTimeout(this._flashTimeouts[id]);
+ this._flashTimeouts[id] = null;
+ },
+
+ _setElementExpandAttribute(element, value, attr = 'zen-has-hover') {
+ const kVerifiedAttributes = ['zen-has-hover', 'has-popup-menu', 'zen-compact-mode-active'];
+ const isToolbar = element.id === 'zen-appcontent-navbar-wrapper';
+ if (value) {
+ if (attr === 'zen-has-hover' && element !== gZenVerticalTabsManager.actualWindowButtons) {
+ element.setAttribute('zen-has-implicit-hover', 'true');
+ if (!lazy.COMPACT_MODE_SHOW_SIDEBAR_AND_TOOLBAR_ON_HOVER) {
+ return;
+ }
}
- },
-
- toggle(ignoreHover = false) {
- // Only ignore the next hover when we are enabling compact mode
- this._ignoreNextHover = ignoreHover && !this.preference;
- return (this.preference = !this.preference);
- },
-
- _updateSidebarIsOnRight() {
- this._sidebarIsOnRight = Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
- },
-
- toggleSidebar() {
- this.sidebar.toggleAttribute('zen-user-show');
- },
-
- get hideAfterHoverDuration() {
- if (this._hideAfterHoverDuration) {
- return this._hideAfterHoverDuration;
+ element.setAttribute(attr, 'true');
+ if (
+ isToolbar &&
+ ((gZenVerticalTabsManager._hasSetSingleToolbar &&
+ (element.hasAttribute('should-hide') ||
+ document.documentElement.hasAttribute('zen-has-bookmarks'))) ||
+ (this.preference &&
+ Services.prefs.getBoolPref('zen.view.compact.hide-toolbar') &&
+ !gZenVerticalTabsManager._hasSetSingleToolbar))
+ ) {
+ gBrowser.tabpanels.setAttribute('has-toolbar-hovered', 'true');
}
- return Services.prefs.getIntPref('zen.view.compact.toolbar-hide-after-hover.duration');
- },
-
- get hoverableElements() {
- return [
- {
- element: this.sidebar,
- screenEdge: this.sidebarIsOnRight ? 'right' : 'left',
- keepHoverDuration: 100,
- },
- {
- element: document.getElementById('zen-appcontent-navbar-wrapper'),
- screenEdge: 'top',
- },
- {
- element: gZenVerticalTabsManager.actualWindowButtons,
- },
- ];
- },
-
- flashSidebar(duration = lazy.COMPACT_MODE_FLASH_DURATION) {
- let tabPanels = document.getElementById('tabbrowser-tabpanels');
- if (!tabPanels.matches("[zen-split-view='true']")) {
- this.flashElement(this.sidebar, duration, this.sidebar.id);
+ } else {
+ if (attr === 'zen-has-hover') {
+ element.removeAttribute('zen-has-implicit-hover');
}
- },
-
- flashElement(element, duration, id, attrName = 'flash-popup') {
- if (this._flashTimeouts[id]) {
- clearTimeout(this._flashTimeouts[id]);
- } else {
- requestAnimationFrame(() => this._setElementExpandAttribute(element, true, attrName));
+ element.removeAttribute(attr);
+ // Only remove if none of the verified attributes are present
+ if (isToolbar && !kVerifiedAttributes.some((attr) => element.hasAttribute(attr))) {
+ gBrowser.tabpanels.removeAttribute('has-toolbar-hovered');
}
- this._flashTimeouts[id] = setTimeout(() => {
+ }
+ },
+
+ addMouseActions() {
+ gURLBar.textbox.addEventListener('mouseenter', (event) => {
+ if (event.target.closest('#urlbar[zen-floating-urlbar]')) {
window.requestAnimationFrame(() => {
- this._setElementExpandAttribute(element, false, attrName);
- this._flashTimeouts[id] = null;
+ this._setElementExpandAttribute(gZenVerticalTabsManager.actualWindowButtons, false);
});
- }, duration);
- },
-
- clearFlashTimeout(id) {
- clearTimeout(this._flashTimeouts[id]);
- this._flashTimeouts[id] = null;
- },
-
- _setElementExpandAttribute(element, value, attr = 'zen-has-hover') {
- const kVerifiedAttributes = ['zen-has-hover', 'has-popup-menu', 'zen-compact-mode-active'];
- const isToolbar = element.id === 'zen-appcontent-navbar-wrapper';
- if (value) {
- if (attr === 'zen-has-hover' && element !== gZenVerticalTabsManager.actualWindowButtons) {
- element.setAttribute('zen-has-implicit-hover', 'true');
- if (!lazy.COMPACT_MODE_SHOW_SIDEBAR_AND_TOOLBAR_ON_HOVER) {
- return;
- }
- }
- element.setAttribute(attr, 'true');
- if (
- isToolbar &&
- ((gZenVerticalTabsManager._hasSetSingleToolbar &&
- (element.hasAttribute('should-hide') ||
- document.documentElement.hasAttribute('zen-has-bookmarks'))) ||
- (this.preference &&
- Services.prefs.getBoolPref('zen.view.compact.hide-toolbar') &&
- !gZenVerticalTabsManager._hasSetSingleToolbar))
- ) {
- gBrowser.tabpanels.setAttribute('has-toolbar-hovered', 'true');
- }
- } else {
- if (attr === 'zen-has-hover') {
- element.removeAttribute('zen-has-implicit-hover');
- }
- element.removeAttribute(attr);
- // Only remove if none of the verified attributes are present
- if (isToolbar && !kVerifiedAttributes.some((attr) => element.hasAttribute(attr))) {
- gBrowser.tabpanels.removeAttribute('has-toolbar-hovered');
- }
+ this._hasHoveredUrlbar = true;
+ return;
}
- },
-
- addMouseActions() {
- gURLBar.textbox.addEventListener('mouseenter', (event) => {
- if (event.target.closest('#urlbar[zen-floating-urlbar]')) {
- window.requestAnimationFrame(() => {
- this._setElementExpandAttribute(gZenVerticalTabsManager.actualWindowButtons, false);
- });
- this._hasHoveredUrlbar = true;
- return;
- }
- });
+ });
- for (let i = 0; i < this.hoverableElements.length; i++) {
- let target = this.hoverableElements[i].element;
+ for (let i = 0; i < this.hoverableElements.length; i++) {
+ let target = this.hoverableElements[i].element;
- // Add the attribute on startup if the mouse is already over the element
- if (target.matches(':hover')) {
- this._setElementExpandAttribute(target, true);
- }
-
- const onEnter = (event) => {
- setTimeout(() => {
- if (event.type === 'mouseenter' && !event.target.matches(':hover')) return;
- if (event.target.closest('panel')) return;
- // Dont register the hover if the urlbar is floating and we are hovering over it
- this.clearFlashTimeout('has-hover' + target.id);
- window.requestAnimationFrame(() => {
- if (
- document.documentElement.getAttribute('supress-primary-adjustment') === 'true' ||
- this._hasHoveredUrlbar ||
- this._ignoreNextHover ||
- target.hasAttribute('zen-has-hover')
- ) {
- return;
- }
- this._setElementExpandAttribute(target, true);
- });
- }, this.HOVER_HACK_DELAY);
- };
-
- const onLeave = (event) => {
- if (AppConstants.platform == 'macosx') {
- const buttonRect = gZenVerticalTabsManager.actualWindowButtons.getBoundingClientRect();
- const MAC_WINDOW_BUTTONS_X_BORDER = buttonRect.width + buttonRect.x;
- const MAC_WINDOW_BUTTONS_Y_BORDER = buttonRect.height + buttonRect.y;
- if (
- event.clientX < MAC_WINDOW_BUTTONS_X_BORDER &&
- event.clientY < MAC_WINDOW_BUTTONS_Y_BORDER &&
- event.clientX > buttonRect.x &&
- event.clientY > buttonRect.y
- ) {
- return;
- }
- }
-
- // See bug https://bugzilla.mozilla.org/show_bug.cgi?id=1979340 and issue https://github.com/zen-browser/desktop/issues/7746.
- // If we want the toolbars to be draggable, we need to make sure to check the hover state after a short delay.
- // This is because the mouse is left to be handled natively so firefox thinks the mouse left the window for a split second.
- setTimeout(() => {
- // Let's double check if the mouse is still hovering over the element, see the bug above.
- if (event.target.matches(':hover')) {
- return;
- }
+ // Add the attribute on startup if the mouse is already over the element
+ if (target.matches(':hover')) {
+ this._setElementExpandAttribute(target, true);
+ }
+ const onEnter = (event) => {
+ setTimeout(() => {
+ if (event.type === 'mouseenter' && !event.target.matches(':hover')) return;
+ if (event.target.closest('panel')) return;
+ // Dont register the hover if the urlbar is floating and we are hovering over it
+ this.clearFlashTimeout('has-hover' + target.id);
+ window.requestAnimationFrame(() => {
if (
- event.explicitOriginalTarget?.closest?.('#urlbar[zen-floating-urlbar]') ||
- (document.documentElement.getAttribute('supress-primary-adjustment') === 'true' &&
- gZenVerticalTabsManager._hasSetSingleToolbar) ||
+ document.documentElement.getAttribute('supress-primary-adjustment') === 'true' ||
this._hasHoveredUrlbar ||
this._ignoreNextHover ||
- (event.type === 'dragleave' &&
- event.explicitOriginalTarget !== target &&
- target.contains?.(event.explicitOriginalTarget))
+ target.hasAttribute('zen-has-hover')
) {
return;
}
+ this._setElementExpandAttribute(target, true);
+ });
+ }, this.HOVER_HACK_DELAY);
+ };
- if (this.hoverableElements[i].keepHoverDuration) {
- this.flashElement(
- target,
- this.hoverableElements[i].keepHoverDuration,
- 'has-hover' + target.id,
- 'zen-has-hover'
- );
- } else {
- this._removeHoverFrames[target.id] = window.requestAnimationFrame(() =>
- this._setElementExpandAttribute(target, false)
- );
- }
- }, this.HOVER_HACK_DELAY);
- };
-
- target.addEventListener('mouseover', onEnter);
- target.addEventListener('dragover', onEnter);
-
- target.addEventListener('mouseleave', onLeave);
- target.addEventListener('dragleave', onLeave);
- }
+ const onLeave = (event) => {
+ if (AppConstants.platform == 'macosx') {
+ const buttonRect = gZenVerticalTabsManager.actualWindowButtons.getBoundingClientRect();
+ const MAC_WINDOW_BUTTONS_X_BORDER = buttonRect.width + buttonRect.x;
+ const MAC_WINDOW_BUTTONS_Y_BORDER = buttonRect.height + buttonRect.y;
+ if (
+ event.clientX < MAC_WINDOW_BUTTONS_X_BORDER &&
+ event.clientY < MAC_WINDOW_BUTTONS_Y_BORDER &&
+ event.clientX > buttonRect.x &&
+ event.clientY > buttonRect.y
+ ) {
+ return;
+ }
+ }
- document.documentElement.addEventListener('mouseleave', (event) => {
+ // See bug https://bugzilla.mozilla.org/show_bug.cgi?id=1979340 and issue https://github.com/zen-browser/desktop/issues/7746.
+ // If we want the toolbars to be draggable, we need to make sure to check the hover state after a short delay.
+ // This is because the mouse is left to be handled natively so firefox thinks the mouse left the window for a split second.
setTimeout(() => {
- const screenEdgeCrossed = this._getCrossedEdge(event.pageX, event.pageY);
- if (!screenEdgeCrossed) return;
- for (let entry of this.hoverableElements) {
- if (screenEdgeCrossed !== entry.screenEdge) continue;
- const target = entry.element;
- const boundAxis =
- entry.screenEdge === 'right' || entry.screenEdge === 'left' ? 'y' : 'x';
- if (!this._positionInBounds(boundAxis, target, event.pageX, event.pageY, 7)) {
- continue;
- }
- window.cancelAnimationFrame(this._removeHoverFrames[target.id]);
+ // Let's double check if the mouse is still hovering over the element, see the bug above.
+ if (event.target.matches(':hover')) {
+ return;
+ }
+ if (
+ event.explicitOriginalTarget?.closest?.('#urlbar[zen-floating-urlbar]') ||
+ (document.documentElement.getAttribute('supress-primary-adjustment') === 'true' &&
+ gZenVerticalTabsManager._hasSetSingleToolbar) ||
+ this._hasHoveredUrlbar ||
+ this._ignoreNextHover ||
+ (event.type === 'dragleave' &&
+ event.explicitOriginalTarget !== target &&
+ target.contains?.(event.explicitOriginalTarget))
+ ) {
+ return;
+ }
+
+ if (this.hoverableElements[i].keepHoverDuration) {
this.flashElement(
target,
- this.hideAfterHoverDuration,
+ this.hoverableElements[i].keepHoverDuration,
'has-hover' + target.id,
'zen-has-hover'
);
- document.addEventListener(
- 'mousemove',
- () => {
- if (target.matches(':hover')) return;
- this._setElementExpandAttribute(target, false);
- this.clearFlashTimeout('has-hover' + target.id);
- },
- { once: true }
+ } else {
+ this._removeHoverFrames[target.id] = window.requestAnimationFrame(() =>
+ this._setElementExpandAttribute(target, false)
);
}
}, this.HOVER_HACK_DELAY);
- });
+ };
- gURLBar.textbox.addEventListener('mouseleave', () => {
- setTimeout(() => {
- setTimeout(() => {
- requestAnimationFrame(() => {
- delete this._hasHoveredUrlbar;
- });
- }, 10);
- }, 0);
- });
- },
-
- _getCrossedEdge(posX, posY, element = document.documentElement, maxDistance = 10) {
- const targetBox = element.getBoundingClientRect();
- posX = Math.max(targetBox.left, Math.min(posX, targetBox.right));
- posY = Math.max(targetBox.top, Math.min(posY, targetBox.bottom));
- return ['top', 'bottom', 'left', 'right'].find((edge, i) => {
- const distance = Math.abs((i < 2 ? posY : posX) - targetBox[edge]);
- return distance <= maxDistance;
- });
- },
-
- _positionInBounds(axis = 'x', element, x, y, error = 0) {
- const bBox = element.getBoundingClientRect();
- if (axis === 'y') return bBox.top - error < y && y < bBox.bottom + error;
- else return bBox.left - error < x && x < bBox.right + error;
- },
-
- _clearAllHoverStates() {
- // Clear hover attributes from all hoverable elements
- for (let entry of this.hoverableElements) {
- const target = entry.element;
- if (target && !target.matches(':hover') && target.hasAttribute('zen-has-hover')) {
- this._setElementExpandAttribute(target, false);
- this.clearFlashTimeout('has-hover' + target.id);
+ target.addEventListener('mouseover', onEnter);
+ target.addEventListener('dragover', onEnter);
+
+ target.addEventListener('mouseleave', onLeave);
+ target.addEventListener('dragleave', onLeave);
+ }
+
+ document.documentElement.addEventListener('mouseleave', (event) => {
+ setTimeout(() => {
+ const screenEdgeCrossed = this._getCrossedEdge(event.pageX, event.pageY);
+ if (!screenEdgeCrossed) return;
+ for (let entry of this.hoverableElements) {
+ if (screenEdgeCrossed !== entry.screenEdge) continue;
+ const target = entry.element;
+ const boundAxis = entry.screenEdge === 'right' || entry.screenEdge === 'left' ? 'y' : 'x';
+ if (!this._positionInBounds(boundAxis, target, event.pageX, event.pageY, 7)) {
+ continue;
+ }
+ window.cancelAnimationFrame(this._removeHoverFrames[target.id]);
+
+ this.flashElement(
+ target,
+ this.hideAfterHoverDuration,
+ 'has-hover' + target.id,
+ 'zen-has-hover'
+ );
+ document.addEventListener(
+ 'mousemove',
+ () => {
+ if (target.matches(':hover')) return;
+ this._setElementExpandAttribute(target, false);
+ this.clearFlashTimeout('has-hover' + target.id);
+ },
+ { once: true }
+ );
}
- }
- },
+ }, this.HOVER_HACK_DELAY);
+ });
- isSidebarPotentiallyOpen() {
- if (this._ignoreNextHover) {
- this._setElementExpandAttribute(this.sidebar, false);
+ gURLBar.textbox.addEventListener('mouseleave', () => {
+ setTimeout(() => {
+ setTimeout(() => {
+ requestAnimationFrame(() => {
+ delete this._hasHoveredUrlbar;
+ });
+ }, 10);
+ }, 0);
+ });
+ },
+
+ _getCrossedEdge(posX, posY, element = document.documentElement, maxDistance = 10) {
+ const targetBox = element.getBoundingClientRect();
+ posX = Math.max(targetBox.left, Math.min(posX, targetBox.right));
+ posY = Math.max(targetBox.top, Math.min(posY, targetBox.bottom));
+ return ['top', 'bottom', 'left', 'right'].find((edge, i) => {
+ const distance = Math.abs((i < 2 ? posY : posX) - targetBox[edge]);
+ return distance <= maxDistance;
+ });
+ },
+
+ _positionInBounds(axis = 'x', element, x, y, error = 0) {
+ const bBox = element.getBoundingClientRect();
+ if (axis === 'y') return bBox.top - error < y && y < bBox.bottom + error;
+ else return bBox.left - error < x && x < bBox.right + error;
+ },
+
+ _clearAllHoverStates() {
+ // Clear hover attributes from all hoverable elements
+ for (let entry of this.hoverableElements) {
+ const target = entry.element;
+ if (target && !target.matches(':hover') && target.hasAttribute('zen-has-hover')) {
+ this._setElementExpandAttribute(target, false);
+ this.clearFlashTimeout('has-hover' + target.id);
}
- return (
- this.sidebar.hasAttribute('zen-user-show') ||
- this.sidebar.hasAttribute('zen-has-hover') ||
- this.sidebar.hasAttribute('zen-has-empty-tab')
- );
- },
-
- async _onTabOpen(tab, inBackground) {
- if (
- inBackground &&
- this.preference &&
- !this.isSidebarPotentiallyOpen() &&
- this._canShowBackgroundTabToast &&
- !gZenGlanceManager._animating &&
- !this._nextTimeWillBeActive
- ) {
- gZenUIManager.showToast('zen-background-tab-opened-toast', {
- button: {
- id: 'zen-open-background-tab-button',
- command: () => {
- const targetWindow = window.ownerGlobal.parent || window;
- targetWindow.gBrowser.selectedTab = tab;
- },
+ }
+ },
+
+ isSidebarPotentiallyOpen() {
+ if (this._ignoreNextHover) {
+ this._setElementExpandAttribute(this.sidebar, false);
+ }
+ return (
+ this.sidebar.hasAttribute('zen-user-show') ||
+ this.sidebar.hasAttribute('zen-has-hover') ||
+ this.sidebar.hasAttribute('zen-has-empty-tab')
+ );
+ },
+
+ async _onTabOpen(tab, inBackground) {
+ if (
+ inBackground &&
+ this.preference &&
+ !this.isSidebarPotentiallyOpen() &&
+ this._canShowBackgroundTabToast &&
+ !gZenGlanceManager._animating &&
+ !this._nextTimeWillBeActive
+ ) {
+ gZenUIManager.showToast('zen-background-tab-opened-toast', {
+ button: {
+ id: 'zen-open-background-tab-button',
+ command: () => {
+ const targetWindow = window.ownerGlobal.parent || window;
+ targetWindow.gBrowser.selectedTab = tab;
},
- });
- }
- delete this._nextTimeWillBeActive;
- },
- };
-
- document.addEventListener(
- 'MozBeforeInitialXULLayout',
- () => {
- gZenCompactModeManager.preInit();
- },
- { once: true }
- );
-}
+ },
+ });
+ }
+ delete this._nextTimeWillBeActive;
+ },
+};
+
+document.addEventListener(
+ 'MozBeforeInitialXULLayout',
+ () => {
+ gZenCompactModeManager.preInit();
+ },
+ { once: true }
+);
diff --git a/src/zen/compact-mode/jar.inc.mn b/src/zen/compact-mode/jar.inc.mn
new file mode 100644
index 0000000000..93d26d3a36
--- /dev/null
+++ b/src/zen/compact-mode/jar.inc.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+* content/browser/zen-styles/zen-compact-mode.css (../../zen/compact-mode/zen-compact-mode.css)
+ content/browser/zen-components/ZenCompactMode.mjs (../../zen/compact-mode/ZenCompactMode.mjs)
diff --git a/src/zen/downloads/ZenDownloadAnimation.mjs b/src/zen/downloads/ZenDownloadAnimation.mjs
index 133aea0989..f205d80b85 100644
--- a/src/zen/downloads/ZenDownloadAnimation.mjs
+++ b/src/zen/downloads/ZenDownloadAnimation.mjs
@@ -1,165 +1,169 @@
// 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/.
-{
- const { Downloads } = ChromeUtils.importESModule('resource://gre/modules/Downloads.sys.mjs');
-
- const CONFIG = Object.freeze({
- ANIMATION: {
- ARC_STEPS: 60,
- MAX_ARC_HEIGHT: 1200,
- ARC_HEIGHT_RATIO: 0.8, // Arc height = distance * ratio (capped at MAX_ARC_HEIGHT)
- SCALE_END: 0.45, // Final scale at destination
- },
- });
-
- class nsZenDownloadAnimation extends nsZenDOMOperatedFeature {
- async init() {
- await this.#setupDownloadListeners();
- }
-
- async #setupDownloadListeners() {
- try {
- const list = await Downloads.getList(Downloads.ALL);
- list.addView({
- onDownloadAdded: this.#handleNewDownload.bind(this),
- });
- } catch (error) {
- console.error(
- `[${nsZenDownloadAnimation.name}] Failed to set up download animation listeners: ${error}`
- );
- }
- }
-
- #handleNewDownload() {
- if (
- !Services.prefs.getBoolPref('zen.downloads.download-animation') ||
- !nsZenMultiWindowFeature.isActiveWindow
- ) {
- return;
- }
- if (!gZenUIManager._lastClickPosition) {
- console.warn(
- `[${nsZenDownloadAnimation.name}] No recent click position available for animation`
- );
- return;
- }
+import {
+ nsZenDOMOperatedFeature,
+ nsZenMultiWindowFeature,
+} from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
+
+const CONFIG = Object.freeze({
+ ANIMATION: {
+ ARC_STEPS: 60,
+ MAX_ARC_HEIGHT: 1200,
+ ARC_HEIGHT_RATIO: 0.8, // Arc height = distance * ratio (capped at MAX_ARC_HEIGHT)
+ SCALE_END: 0.45, // Final scale at destination
+ },
+});
+
+class nsZenDownloadAnimation extends nsZenDOMOperatedFeature {
+ async init() {
+ await this.#setupDownloadListeners();
+ }
- this.#animateDownload(gZenUIManager._lastClickPosition);
+ async #setupDownloadListeners() {
+ try {
+ const Downloads = window.Downloads;
+ const list = await Downloads.getList(Downloads.ALL);
+ list.addView({
+ onDownloadAdded: this.#handleNewDownload.bind(this),
+ });
+ } catch (error) {
+ console.error(
+ `[${nsZenDownloadAnimation.name}] Failed to set up download animation listeners: ${error}`
+ );
}
+ }
- #animateDownload(startPosition) {
- let animationElement = document.querySelector('zen-download-animation');
-
- if (!animationElement) {
- animationElement = document.createElement('zen-download-animation');
- document.body.appendChild(animationElement);
- }
+ #handleNewDownload() {
+ if (
+ !Services.prefs.getBoolPref('zen.downloads.download-animation') ||
+ !nsZenMultiWindowFeature.isActiveWindow
+ ) {
+ return;
+ }
- animationElement.initializeAnimation(startPosition);
+ if (!gZenUIManager._lastClickPosition) {
+ console.warn(
+ `[${nsZenDownloadAnimation.name}] No recent click position available for animation`
+ );
+ return;
}
- }
- class nsZenDownloadAnimationElement extends HTMLElement {
- #boxAnimationElement = null;
- #boxAnimationTimeoutId = null;
- #isBoxAnimationRunning = false;
+ this.#animateDownload(gZenUIManager._lastClickPosition);
+ }
- constructor() {
- super();
- this.attachShadow({ mode: 'open' });
- this.#loadArcStyles();
- }
+ #animateDownload(startPosition) {
+ let animationElement = document.querySelector('zen-download-animation');
- #loadArcStyles() {
- try {
- const link = document.createElement('link');
- link.setAttribute('rel', 'stylesheet');
- link.setAttribute(
- 'href',
- 'chrome://browser/content/zen-styles/zen-download-arc-animation.css'
- );
- this.shadowRoot.appendChild(link);
- } catch (error) {
- console.error(`[${nsZenDownloadAnimationElement.name}] Error loading arc styles: ${error}`);
- }
+ if (!animationElement) {
+ animationElement = document.createElement('zen-download-animation');
+ document.body.appendChild(animationElement);
}
- async initializeAnimation(startPosition) {
- if (!startPosition) {
- console.warn(
- `[${nsZenDownloadAnimationElement.name}] No start position provided, skipping animation`
- );
- return;
- }
-
- // Determine animation target position
- const { endPosition, isDownloadButtonVisible } = this.#determineEndPosition();
- const areTabsPositionedRight = this.#areTabsOnRightSide();
+ animationElement.initializeAnimation(startPosition);
+ }
+}
- // Create and prepare the arc animation element
- const arcAnimationElement = this.#createArcAnimationElement(startPosition);
+class nsZenDownloadAnimationElement extends HTMLElement {
+ #boxAnimationElement = null;
+ #boxAnimationTimeoutId = null;
+ #isBoxAnimationRunning = false;
- // Calculate optimal arc parameters based on available space
- const distance = this.#calculateDistance(startPosition, endPosition);
- const { arcHeight, shouldArcDownward } = this.#calculateOptimalArc(
- startPosition,
- endPosition,
- distance
- );
- const distanceX = endPosition.clientX - startPosition.clientX;
- const distanceY = endPosition.clientY - startPosition.clientY;
- const arcSequence = this.#createArcAnimationSequence(
- distanceX,
- distanceY,
- arcHeight,
- shouldArcDownward
- );
+ constructor() {
+ super();
+ this.attachShadow({ mode: 'open' });
+ this.#loadArcStyles();
+ }
- // Start the download animation
- await this.#startDownloadAnimation(
- areTabsPositionedRight,
- isDownloadButtonVisible,
- arcAnimationElement,
- arcSequence
+ #loadArcStyles() {
+ try {
+ const link = document.createElement('link');
+ link.setAttribute('rel', 'stylesheet');
+ link.setAttribute(
+ 'href',
+ 'chrome://browser/content/zen-styles/zen-download-arc-animation.css'
);
+ this.shadowRoot.appendChild(link);
+ } catch (error) {
+ console.error(`[${nsZenDownloadAnimationElement.name}] Error loading arc styles: ${error}`);
}
+ }
- #areTabsOnRightSide() {
- return Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
+ async initializeAnimation(startPosition) {
+ if (!startPosition) {
+ console.warn(
+ `[${nsZenDownloadAnimationElement.name}] No start position provided, skipping animation`
+ );
+ return;
}
- #determineEndPosition() {
- const downloadsButton = document.getElementById('downloads-button');
- const isDownloadButtonVisible = downloadsButton && this.#isElementVisible(downloadsButton);
+ // Determine animation target position
+ const { endPosition, isDownloadButtonVisible } = this.#determineEndPosition();
+ const areTabsPositionedRight = this.#areTabsOnRightSide();
+
+ // Create and prepare the arc animation element
+ const arcAnimationElement = this.#createArcAnimationElement(startPosition);
+
+ // Calculate optimal arc parameters based on available space
+ const distance = this.#calculateDistance(startPosition, endPosition);
+ const { arcHeight, shouldArcDownward } = this.#calculateOptimalArc(
+ startPosition,
+ endPosition,
+ distance
+ );
+ const distanceX = endPosition.clientX - startPosition.clientX;
+ const distanceY = endPosition.clientY - startPosition.clientY;
+ const arcSequence = this.#createArcAnimationSequence(
+ distanceX,
+ distanceY,
+ arcHeight,
+ shouldArcDownward
+ );
+
+ // Start the download animation
+ await this.#startDownloadAnimation(
+ areTabsPositionedRight,
+ isDownloadButtonVisible,
+ arcAnimationElement,
+ arcSequence
+ );
+ }
- let endPosition = { clientX: 0, clientY: 0 };
+ #areTabsOnRightSide() {
+ return Services.prefs.getBoolPref('zen.tabs.vertical.right-side');
+ }
- if (isDownloadButtonVisible) {
- // Use download button as target
- const buttonRect = downloadsButton.getBoundingClientRect();
- endPosition = {
- clientX: buttonRect.left + buttonRect.width / 2,
- clientY: buttonRect.top + buttonRect.height / 2,
- };
- } else {
- // Use alternative position at bottom of wrapper
- const areTabsPositionedRight = this.#areTabsOnRightSide();
- const wrapper = document.getElementById('zen-main-app-wrapper');
- const wrapperRect = wrapper.getBoundingClientRect();
-
- endPosition = {
- clientX: areTabsPositionedRight ? wrapperRect.right - 42 : wrapperRect.left + 42,
- clientY: wrapperRect.bottom - 40,
- };
- }
+ #determineEndPosition() {
+ const downloadsButton = document.getElementById('downloads-button');
+ const isDownloadButtonVisible = downloadsButton && this.#isElementVisible(downloadsButton);
+
+ let endPosition = { clientX: 0, clientY: 0 };
+
+ if (isDownloadButtonVisible) {
+ // Use download button as target
+ const buttonRect = downloadsButton.getBoundingClientRect();
+ endPosition = {
+ clientX: buttonRect.left + buttonRect.width / 2,
+ clientY: buttonRect.top + buttonRect.height / 2,
+ };
+ } else {
+ // Use alternative position at bottom of wrapper
+ const areTabsPositionedRight = this.#areTabsOnRightSide();
+ const wrapper = document.getElementById('zen-main-app-wrapper');
+ const wrapperRect = wrapper.getBoundingClientRect();
- return { endPosition, isDownloadButtonVisible };
+ endPosition = {
+ clientX: areTabsPositionedRight ? wrapperRect.right - 42 : wrapperRect.left + 42,
+ clientY: wrapperRect.bottom - 40,
+ };
}
- #createArcAnimationElement(startPosition) {
- const arcAnimationHTML = `
+ return { endPosition, isDownloadButtonVisible };
+ }
+
+ #createArcAnimationElement(startPosition) {
+ const arcAnimationHTML = `
@@ -167,328 +171,327 @@
`;
- const fragment = window.MozXULElement.parseXULToFragment(arcAnimationHTML);
- const animationElement = fragment.querySelector('.zen-download-arc-animation');
+ const fragment = window.MozXULElement.parseXULToFragment(arcAnimationHTML);
+ const animationElement = fragment.querySelector('.zen-download-arc-animation');
- Object.assign(animationElement.style, {
- left: `${startPosition.clientX}px`,
- top: `${startPosition.clientY}px`,
- transform: 'translate(-50%, -50%)',
- });
+ Object.assign(animationElement.style, {
+ left: `${startPosition.clientX}px`,
+ top: `${startPosition.clientY}px`,
+ transform: 'translate(-50%, -50%)',
+ });
- this.shadowRoot.appendChild(animationElement);
+ this.shadowRoot.appendChild(animationElement);
- return animationElement;
- }
+ return animationElement;
+ }
- #calculateOptimalArc(startPosition, endPosition, distance) {
- // Calculate available space for the arc
- const availableTopSpace = Math.min(startPosition.clientY, endPosition.clientY);
- const viewportHeight = window.innerHeight;
- const availableBottomSpace =
- viewportHeight - Math.max(startPosition.clientY, endPosition.clientY);
+ #calculateOptimalArc(startPosition, endPosition, distance) {
+ // Calculate available space for the arc
+ const availableTopSpace = Math.min(startPosition.clientY, endPosition.clientY);
+ const viewportHeight = window.innerHeight;
+ const availableBottomSpace =
+ viewportHeight - Math.max(startPosition.clientY, endPosition.clientY);
- // Determine if we should arc downward or upward based on available space
- const shouldArcDownward = availableBottomSpace > availableTopSpace;
+ // Determine if we should arc downward or upward based on available space
+ const shouldArcDownward = availableBottomSpace > availableTopSpace;
- // Use the space in the direction we're arcing
- const availableSpace = shouldArcDownward ? availableBottomSpace : availableTopSpace;
+ // Use the space in the direction we're arcing
+ const availableSpace = shouldArcDownward ? availableBottomSpace : availableTopSpace;
- // Limit arc height to a percentage of the available space
- const arcHeight = Math.min(
- distance * CONFIG.ANIMATION.ARC_HEIGHT_RATIO,
- CONFIG.ANIMATION.MAX_ARC_HEIGHT,
- availableSpace * 0.8
- );
+ // Limit arc height to a percentage of the available space
+ const arcHeight = Math.min(
+ distance * CONFIG.ANIMATION.ARC_HEIGHT_RATIO,
+ CONFIG.ANIMATION.MAX_ARC_HEIGHT,
+ availableSpace * 0.8
+ );
- return { arcHeight, shouldArcDownward };
- }
+ return { arcHeight, shouldArcDownward };
+ }
- #calculateDistance(start, end) {
- const distanceX = end.clientX - start.clientX;
- const distanceY = end.clientY - start.clientY;
- return Math.sqrt(distanceX * distanceX + distanceY * distanceY);
- }
+ #calculateDistance(start, end) {
+ const distanceX = end.clientX - start.clientX;
+ const distanceY = end.clientY - start.clientY;
+ return Math.sqrt(distanceX * distanceX + distanceY * distanceY);
+ }
- async #startDownloadAnimation(
- areTabsPositionedRight,
- isDownloadButtonVisible,
- arcAnimationElement,
- sequence
- ) {
- try {
- if (!isDownloadButtonVisible) {
- this.#startBoxAnimation(areTabsPositionedRight);
- }
+ async #startDownloadAnimation(
+ areTabsPositionedRight,
+ isDownloadButtonVisible,
+ arcAnimationElement,
+ sequence
+ ) {
+ try {
+ if (!isDownloadButtonVisible) {
+ this.#startBoxAnimation(areTabsPositionedRight);
+ }
- await gZenUIManager.motion.animate(arcAnimationElement, sequence, {
- duration: Services.prefs.getIntPref('zen.downloads.download-animation-duration') / 1000,
- easing: 'cubic-bezier(0.37, 0, 0.63, 1)',
- fill: 'forwards',
- });
+ await gZenUIManager.motion.animate(arcAnimationElement, sequence, {
+ duration: Services.prefs.getIntPref('zen.downloads.download-animation-duration') / 1000,
+ easing: 'cubic-bezier(0.37, 0, 0.63, 1)',
+ fill: 'forwards',
+ });
- this.#cleanArcAnimation(arcAnimationElement);
- } catch (error) {
- console.error('[nsZenDownloadAnimationElement] Error in animation sequence:', error);
- this.#cleanArcAnimation(arcAnimationElement);
- }
+ this.#cleanArcAnimation(arcAnimationElement);
+ } catch (error) {
+ console.error('[nsZenDownloadAnimationElement] Error in animation sequence:', error);
+ this.#cleanArcAnimation(arcAnimationElement);
}
+ }
- #createArcAnimationSequence(distanceX, distanceY, arcHeight, shouldArcDownward) {
- const sequence = { offset: [], opacity: [], transform: [] };
-
- const arcDirection = shouldArcDownward ? 1 : -1;
- const steps = CONFIG.ANIMATION.ARC_STEPS;
- const endScale = CONFIG.ANIMATION.SCALE_END;
+ #createArcAnimationSequence(distanceX, distanceY, arcHeight, shouldArcDownward) {
+ const sequence = { offset: [], opacity: [], transform: [] };
- function easeInOutQuad(t) {
- return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
- }
+ const arcDirection = shouldArcDownward ? 1 : -1;
+ const steps = CONFIG.ANIMATION.ARC_STEPS;
+ const endScale = CONFIG.ANIMATION.SCALE_END;
- let previousRotation = 0;
- for (let i = 0; i <= steps; i++) {
- const progress = i / steps;
- const eased = easeInOutQuad(progress);
-
- // Calculate opacity changes
- let opacity;
- if (progress < 0.3) {
- // Fade in during first 30%
- opacity = 0.3 + (progress / 0.3) * 0.6;
- } else if (progress < 0.98) {
- // Slight increase to full opacity
- opacity = 0.9 + ((progress - 0.3) / 0.6) * 0.1;
- } else {
- // Decrease opacity in the final steps
- opacity = 1 - ((progress - 0.9) / 0.1) * 1;
- }
+ function easeInOutQuad(t) {
+ return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
+ }
- // Calculate scaling changes
- let scale;
- if (progress < 0.5) {
- scale = 0.5 + (progress / 0.5) * 1.3;
- } else {
- scale = 1.8 - ((progress - 0.5) / 0.5) * (1.8 - endScale);
- }
+ let previousRotation = 0;
+ for (let i = 0; i <= steps; i++) {
+ const progress = i / steps;
+ const eased = easeInOutQuad(progress);
+
+ // Calculate opacity changes
+ let opacity;
+ if (progress < 0.3) {
+ // Fade in during first 30%
+ opacity = 0.3 + (progress / 0.3) * 0.6;
+ } else if (progress < 0.98) {
+ // Slight increase to full opacity
+ opacity = 0.9 + ((progress - 0.3) / 0.6) * 0.1;
+ } else {
+ // Decrease opacity in the final steps
+ opacity = 1 - ((progress - 0.9) / 0.1) * 1;
+ }
- // Position on arc
- const x = distanceX * eased;
- const y = distanceY * eased + arcDirection * arcHeight * (1 - (2 * eased - 1) ** 2);
+ // Calculate scaling changes
+ let scale;
+ if (progress < 0.5) {
+ scale = 0.5 + (progress / 0.5) * 1.3;
+ } else {
+ scale = 1.8 - ((progress - 0.5) / 0.5) * (1.8 - endScale);
+ }
- // Calculate rotation to point in the direction of movement
- let rotation = previousRotation;
- if (i > 0) {
- const prevEased = easeInOutQuad((i - 1) / steps);
+ // Position on arc
+ const x = distanceX * eased;
+ const y = distanceY * eased + arcDirection * arcHeight * (1 - (2 * eased - 1) ** 2);
- const prevX = distanceX * prevEased;
- const prevAdjustedProgress = prevEased * 2 - 1;
- const prevVerticalOffset = arcDirection * arcHeight * (1 - prevAdjustedProgress * 2);
- const prevY = distanceY * prevEased + prevVerticalOffset;
+ // Calculate rotation to point in the direction of movement
+ let rotation = previousRotation;
+ if (i > 0) {
+ const prevEased = easeInOutQuad((i - 1) / steps);
- const targetRotation = Math.atan2(y - prevY, x - prevX) * (180 / Math.PI);
+ const prevX = distanceX * prevEased;
+ const prevAdjustedProgress = prevEased * 2 - 1;
+ const prevVerticalOffset = arcDirection * arcHeight * (1 - prevAdjustedProgress * 2);
+ const prevY = distanceY * prevEased + prevVerticalOffset;
- rotation += (targetRotation - previousRotation) * 0.01;
- previousRotation = rotation;
- }
+ const targetRotation = Math.atan2(y - prevY, x - prevX) * (180 / Math.PI);
- sequence.offset.push(progress);
- sequence.opacity.push(opacity);
- sequence.transform.push(
- `translate(calc(${x}px - 50%), calc(${y}px - 50%)) rotate(${rotation}deg) scale(${scale})`
- );
+ rotation += (targetRotation - previousRotation) * 0.01;
+ previousRotation = rotation;
}
- return sequence;
+ sequence.offset.push(progress);
+ sequence.opacity.push(opacity);
+ sequence.transform.push(
+ `translate(calc(${x}px - 50%), calc(${y}px - 50%)) rotate(${rotation}deg) scale(${scale})`
+ );
}
- #cleanArcAnimation(element) {
- element.remove();
- }
+ return sequence;
+ }
- async #startBoxAnimation(areTabsPositionedRight) {
- // If animation is already in progress, don't start a new one
- if (this.#isBoxAnimationRunning) {
- console.warn(
- `[${nsZenDownloadAnimationElement.name}] Box animation already running, skipping new request.`
- );
- return;
- }
+ #cleanArcAnimation(element) {
+ element.remove();
+ }
- if (this.#boxAnimationElement) {
- clearTimeout(this.#boxAnimationTimeoutId);
- this.#boxAnimationTimeoutId = setTimeout(
- () => this.#finishBoxAnimation(areTabsPositionedRight),
- this.#getBoxAnimationDurationMs()
- );
- return;
- }
+ async #startBoxAnimation(areTabsPositionedRight) {
+ // If animation is already in progress, don't start a new one
+ if (this.#isBoxAnimationRunning) {
+ console.warn(
+ `[${nsZenDownloadAnimationElement.name}] Box animation already running, skipping new request.`
+ );
+ return;
+ }
- const wrapper = document.getElementById('zen-main-app-wrapper');
- if (!wrapper) {
- console.warn(
- `[${nsZenDownloadAnimationElement.name}] Cannot start box animation, Wrapper element not found`
- );
- return;
- }
+ if (this.#boxAnimationElement) {
+ clearTimeout(this.#boxAnimationTimeoutId);
+ this.#boxAnimationTimeoutId = setTimeout(
+ () => this.#finishBoxAnimation(areTabsPositionedRight),
+ this.#getBoxAnimationDurationMs()
+ );
+ return;
+ }
- this.#isBoxAnimationRunning = true;
+ const wrapper = document.getElementById('zen-main-app-wrapper');
+ if (!wrapper) {
+ console.warn(
+ `[${nsZenDownloadAnimationElement.name}] Cannot start box animation, Wrapper element not found`
+ );
+ return;
+ }
- try {
- const boxAnimationHTML = `
+ this.#isBoxAnimationRunning = true;
+
+ try {
+ const boxAnimationHTML = `
`;
- const sideProp = areTabsPositionedRight ? 'right' : 'left';
+ const sideProp = areTabsPositionedRight ? 'right' : 'left';
- const fragment = window.MozXULElement.parseXULToFragment(boxAnimationHTML);
- this.#boxAnimationElement = fragment.querySelector('.zen-download-box-animation');
+ const fragment = window.MozXULElement.parseXULToFragment(boxAnimationHTML);
+ this.#boxAnimationElement = fragment.querySelector('.zen-download-box-animation');
- Object.assign(this.#boxAnimationElement.style, {
- bottom: '24px',
- transform: 'scale(0.8)',
- [sideProp]: '-50px',
- });
-
- wrapper.appendChild(this.#boxAnimationElement);
-
- await gZenUIManager.motion.animate(
- this.#boxAnimationElement,
- {
- [sideProp]: '34px',
- opacity: 1,
- transform: 'scale(1.1)',
- },
- {
- duration: 0.35,
- easing: 'ease-out',
- }
- ).finished;
-
- await gZenUIManager.motion.animate(
- this.#boxAnimationElement,
- {
- [sideProp]: '24px',
- transform: 'scale(1)',
- },
- {
- duration: 0.2,
- easing: 'ease-in-out',
- }
- ).finished;
-
- clearTimeout(this.#boxAnimationTimeoutId);
- this.#boxAnimationTimeoutId = setTimeout(
- () => this.#finishBoxAnimation(areTabsPositionedRight),
- this.#getBoxAnimationDurationMs()
- );
- } catch (error) {
- console.error(
- `[${nsZenDownloadAnimationElement.name}] Error during box entry animation: ${error}`
- );
- this.#cleanBoxAnimation();
- } finally {
- this.#isBoxAnimationRunning = false;
- }
+ Object.assign(this.#boxAnimationElement.style, {
+ bottom: '24px',
+ transform: 'scale(0.8)',
+ [sideProp]: '-50px',
+ });
+
+ wrapper.appendChild(this.#boxAnimationElement);
+
+ await gZenUIManager.motion.animate(
+ this.#boxAnimationElement,
+ {
+ [sideProp]: '34px',
+ opacity: 1,
+ transform: 'scale(1.1)',
+ },
+ {
+ duration: 0.35,
+ easing: 'ease-out',
+ }
+ ).finished;
+
+ await gZenUIManager.motion.animate(
+ this.#boxAnimationElement,
+ {
+ [sideProp]: '24px',
+ transform: 'scale(1)',
+ },
+ {
+ duration: 0.2,
+ easing: 'ease-in-out',
+ }
+ ).finished;
+
+ clearTimeout(this.#boxAnimationTimeoutId);
+ this.#boxAnimationTimeoutId = setTimeout(
+ () => this.#finishBoxAnimation(areTabsPositionedRight),
+ this.#getBoxAnimationDurationMs()
+ );
+ } catch (error) {
+ console.error(
+ `[${nsZenDownloadAnimationElement.name}] Error during box entry animation: ${error}`
+ );
+ this.#cleanBoxAnimation();
+ } finally {
+ this.#isBoxAnimationRunning = false;
}
+ }
+
+ #getBoxAnimationDurationMs() {
+ return Services.prefs.getIntPref('zen.downloads.download-animation-duration') + 200;
+ }
+
+ async #finishBoxAnimation(areTabsPositionedRight) {
+ clearTimeout(this.#boxAnimationTimeoutId);
+ this.#boxAnimationTimeoutId = null;
- #getBoxAnimationDurationMs() {
- return Services.prefs.getIntPref('zen.downloads.download-animation-duration') + 200;
+ if (!this.#boxAnimationElement || this.#isBoxAnimationRunning) {
+ if (!this.#boxAnimationElement) this.#cleanBoxAnimationState();
+ return;
}
- async #finishBoxAnimation(areTabsPositionedRight) {
- clearTimeout(this.#boxAnimationTimeoutId);
- this.#boxAnimationTimeoutId = null;
+ this.#isBoxAnimationRunning = true;
- if (!this.#boxAnimationElement || this.#isBoxAnimationRunning) {
- if (!this.#boxAnimationElement) this.#cleanBoxAnimationState();
- return;
- }
+ try {
+ const sideProp = areTabsPositionedRight ? 'right' : 'left';
- this.#isBoxAnimationRunning = true;
+ await gZenUIManager.motion.animate(
+ this.#boxAnimationElement,
+ {
+ transform: 'scale(0.9)',
+ },
+ {
+ duration: 0.15,
+ easing: 'ease-in',
+ }
+ ).finished;
- try {
- const sideProp = areTabsPositionedRight ? 'right' : 'left';
-
- await gZenUIManager.motion.animate(
- this.#boxAnimationElement,
- {
- transform: 'scale(0.9)',
- },
- {
- duration: 0.15,
- easing: 'ease-in',
- }
- ).finished;
-
- await gZenUIManager.motion.animate(
- this.#boxAnimationElement,
- {
- [sideProp]: '-50px',
- opacity: 0,
- transform: 'scale(0.8)',
- },
- {
- duration: 0.3,
- easing: 'cubic-bezier(0.5, 0, 0.75, 0)',
- }
- ).finished;
- } catch (error) {
- console.warn(
- `[${nsZenDownloadAnimationElement.name}] Error during box exit animation: ${error}`
- );
- } finally {
- this.#cleanBoxAnimation();
- }
+ await gZenUIManager.motion.animate(
+ this.#boxAnimationElement,
+ {
+ [sideProp]: '-50px',
+ opacity: 0,
+ transform: 'scale(0.8)',
+ },
+ {
+ duration: 0.3,
+ easing: 'cubic-bezier(0.5, 0, 0.75, 0)',
+ }
+ ).finished;
+ } catch (error) {
+ console.warn(
+ `[${nsZenDownloadAnimationElement.name}] Error during box exit animation: ${error}`
+ );
+ } finally {
+ this.#cleanBoxAnimation();
}
+ }
- #cleanBoxAnimationState() {
- this.#boxAnimationElement = null;
- if (this.#boxAnimationTimeoutId) {
- clearTimeout(this.#boxAnimationTimeoutId);
- this.#boxAnimationTimeoutId = null;
- }
- this.#isBoxAnimationRunning = false;
+ #cleanBoxAnimationState() {
+ this.#boxAnimationElement = null;
+ if (this.#boxAnimationTimeoutId) {
+ clearTimeout(this.#boxAnimationTimeoutId);
+ this.#boxAnimationTimeoutId = null;
}
+ this.#isBoxAnimationRunning = false;
+ }
- #cleanBoxAnimation() {
- if (this.#boxAnimationElement && this.#boxAnimationElement.isConnected) {
- try {
- this.#boxAnimationElement.remove();
- } catch (error) {
- console.error(
- `[${nsZenDownloadAnimationElement.name}] Error removing box animation element: ${error}`,
- error
- );
- }
+ #cleanBoxAnimation() {
+ if (this.#boxAnimationElement && this.#boxAnimationElement.isConnected) {
+ try {
+ this.#boxAnimationElement.remove();
+ } catch (error) {
+ console.error(
+ `[${nsZenDownloadAnimationElement.name}] Error removing box animation element: ${error}`,
+ error
+ );
}
- this.#cleanBoxAnimationState();
}
+ this.#cleanBoxAnimationState();
+ }
- #isElementVisible(element) {
- if (!element) return false;
-
- const rect = element.getBoundingClientRect();
-
- // Element must be in the viewport
- // Is 1 and no 0 because if you pin the download button in the overflow menu
- // the download button is in the viewport but in the position 0,0 so this
- // avoid this case
- if (
- rect.bottom < 1 ||
- rect.right < 1 ||
- rect.top > window.innerHeight ||
- rect.left > window.innerWidth
- ) {
- return false;
- }
+ #isElementVisible(element) {
+ if (!element) return false;
+
+ const rect = element.getBoundingClientRect();
- return true;
+ // Element must be in the viewport
+ // Is 1 and no 0 because if you pin the download button in the overflow menu
+ // the download button is in the viewport but in the position 0,0 so this
+ // avoid this case
+ if (
+ rect.bottom < 1 ||
+ rect.right < 1 ||
+ rect.top > window.innerHeight ||
+ rect.left > window.innerWidth
+ ) {
+ return false;
}
+
+ return true;
}
+}
- customElements.define('zen-download-animation', nsZenDownloadAnimationElement);
+customElements.define('zen-download-animation', nsZenDownloadAnimationElement);
- new nsZenDownloadAnimation();
-}
+new nsZenDownloadAnimation();
diff --git a/src/zen/downloads/jar.inc.mn b/src/zen/downloads/jar.inc.mn
new file mode 100644
index 0000000000..19463fe27b
--- /dev/null
+++ b/src/zen/downloads/jar.inc.mn
@@ -0,0 +1,7 @@
+# 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/.
+
+ content/browser/zen-components/ZenDownloadAnimation.mjs (../../zen/downloads/ZenDownloadAnimation.mjs)
+ content/browser/zen-styles/zen-download-arc-animation.css (../../zen/downloads/zen-download-arc-animation.css)
+ content/browser/zen-styles/zen-download-box-animation.css (../../zen/downloads/zen-download-box-animation.css)
diff --git a/src/zen/folders/ZenFolder.mjs b/src/zen/folders/ZenFolder.mjs
index f6ac00a865..2209e67138 100644
--- a/src/zen/folders/ZenFolder.mjs
+++ b/src/zen/folders/ZenFolder.mjs
@@ -1,11 +1,11 @@
// 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 = `
+class ZenFolder extends MozTabbrowserTabGroup {
+ #initialized = false;
+
+ static markup = `
@@ -19,8 +19,8 @@
`;
- static rawIcon = new DOMParser().parseFromString(
- `
+ static rawIcon = new DOMParser().parseFromString(
+ `
@@ -53,230 +53,227 @@
`,
- 'image/svg+xml'
- ).documentElement;
+ 'image/svg+xml'
+ ).documentElement;
- constructor() {
- super();
- }
+ 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));
+ 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.parentElement.setAttribute('context', 'zenFolderActions');
- this.labelElement.onRenameFinished = (newLabel) => {
- this.name = newLabel.trim() || 'Folder';
- const event = new CustomEvent('ZenFolderRenamed', {
- bubbles: true,
- });
- this.dispatchEvent(event);
- };
+ 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);
- }
+ if (this.collapsed) {
+ this.querySelector('.tab-group-container').setAttribute('hidden', true);
}
+ }
- get icon() {
- return this.querySelector('.tab-group-folder-icon');
- }
+ 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;
+ /**
+ * 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 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);
- }
+ 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;
}
+ return activeGroups;
+ }
- get childActiveGroups() {
- return Array.from(this.querySelectorAll('zen-folder[has-active]'));
- }
+ 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,
- });
+ 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,
- });
- }
+ 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 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);
- }
+ 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 });
}
+ 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);
- }
+ get allItemsRecursive() {
+ const items = [];
+ for (const item of this.allItems) {
+ if (item.isZenFolder) {
+ items.push(item, ...item.allItemsRecursive);
+ } else {
+ items.push(item);
}
- return items;
}
+ return items;
+ }
- get allItems() {
- return [...this.querySelector('.tab-group-container').children].filter(
- (child) => !child.classList.contains('zen-tab-group-start')
- );
- }
+ get allItems() {
+ return [...this.querySelector('.tab-group-container').children].filter(
+ (child) => !child.classList.contains('zen-tab-group-start')
+ );
+ }
- get pinned() {
- return this.isZenFolder;
- }
+ 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) {}
+ /**
+ * 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') || '';
- }
+ 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');
+ 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));
}
- } 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');
- }
+ let activeGroup = folders.get(group?.id);
+ if (!activeGroup) {
+ tab.removeAttribute('folder-active');
+ tab.style.removeProperty('--zen-folder-indent');
}
- this._activeTabs = [];
- folders.clear();
}
+ this._activeTabs = [];
+ folders.clear();
}
+ }
- get activeTabs() {
- return this._activeTabs;
- }
-
- get resetButton() {
- return this.labelElement.parentElement.querySelector('.tab-reset-button');
- }
+ get activeTabs() {
+ return this._activeTabs;
+ }
- unloadAllTabs(event) {
- this.#unloadAllActiveTabs(event, /* noClose */ true);
- }
+ get resetButton() {
+ return this.labelElement.parentElement.querySelector('.tab-reset-button');
+ }
- async #unloadAllActiveTabs(event, noClose = false) {
- await gZenPinnedTabManager.onCloseTabShortcut(event, this.tabs, {
- noClose,
- alwaysUnload: true,
- folderToUnload: this,
- });
- this.activeTabs = [];
- }
+ unloadAllTabs(event) {
+ this.#unloadAllActiveTabs(event, /* noClose */ true);
+ }
- on_click(event) {
- if (event.target === this.resetButton) {
- event.stopPropagation();
- this.unloadAllTabs(event);
- return;
- }
- super.on_click(event);
- }
+ async #unloadAllActiveTabs(event, noClose = false) {
+ await gZenPinnedTabManager.onCloseTabShortcut(event, this.tabs, {
+ noClose,
+ alwaysUnload: true,
+ folderToUnload: this,
+ });
+ this.activeTabs = [];
+ }
- /**
- * 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;
+ on_click(event) {
+ if (event.target === this.resetButton) {
+ event.stopPropagation();
+ this.unloadAllTabs(event);
+ return;
}
+ super.on_click(event);
}
- customElements.define('zen-folder', ZenFolder);
+ /**
+ * 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);
diff --git a/src/zen/folders/ZenFolders.mjs b/src/zen/folders/ZenFolders.mjs
index 010a196a79..8fdf63fec7 100644
--- a/src/zen/folders/ZenFolders.mjs
+++ b/src/zen/folders/ZenFolders.mjs
@@ -1,1733 +1,1722 @@
// 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/.
-{
- function formatRelativeTime(timestamp) {
- const now = Date.now();
- const sec = Math.floor((now - timestamp) / 1000);
- if (sec < 60) {
- return 'Just now';
- }
+import { nsZenDOMOperatedFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
- const min = Math.floor(sec / 60);
- if (min < 60) {
- return `${min} minute${min === 1 ? '' : 's'} ago`;
- }
+function formatRelativeTime(timestamp) {
+ const now = Date.now();
- const hour = Math.floor(min / 60);
- if (hour < 24) {
- return `${hour} hour${hour === 1 ? '' : 's'} ago`;
- }
+ const sec = Math.floor((now - timestamp) / 1000);
+ if (sec < 60) {
+ return 'Just now';
+ }
- const day = Math.floor(hour / 24);
- if (day < 30) {
- return `${day} day${day === 1 ? '' : 's'} ago`;
- }
+ const min = Math.floor(sec / 60);
+ if (min < 60) {
+ return `${min} minute${min === 1 ? '' : 's'} ago`;
+ }
+
+ const hour = Math.floor(min / 60);
+ if (hour < 24) {
+ return `${hour} hour${hour === 1 ? '' : 's'} ago`;
+ }
- const month = Math.floor(day / 30);
- return `${month} month${month === 1 ? '' : 's'} ago`;
+ const day = Math.floor(hour / 24);
+ if (day < 30) {
+ return `${day} day${day === 1 ? '' : 's'} ago`;
}
- class nsZenFolders extends nsZenDOMOperatedFeature {
- #ZEN_MAX_SUBFOLDERS = Services.prefs.getIntPref('zen.folders.max-subfolders', 5);
- #ZEN_EDGE_ZONE_THRESHOLD =
- Services.prefs.getIntPref('zen.view.drag-and-drop.edge-zone-threshold', 25) / 100;
+ const month = Math.floor(day / 30);
+ return `${month} month${month === 1 ? '' : 's'} ago`;
+}
- #popup = null;
- #popupTimer = null;
- #mouseTimer = null;
- #lastHighlightedGroup = null;
+class nsZenFolders extends nsZenDOMOperatedFeature {
+ #ZEN_MAX_SUBFOLDERS = Services.prefs.getIntPref('zen.folders.max-subfolders', 5);
+ #ZEN_EDGE_ZONE_THRESHOLD =
+ Services.prefs.getIntPref('zen.view.drag-and-drop.edge-zone-threshold', 25) / 100;
- #lastFolderContextMenu = null;
+ #popup = null;
+ #popupTimer = null;
+ #mouseTimer = null;
+ #lastHighlightedGroup = null;
- #foldersEnabled = false;
+ #lastFolderContextMenu = null;
- #animationCount = 0;
+ #foldersEnabled = false;
- init() {
- this.#foldersEnabled = !gZenWorkspaces.privateWindowOrDisabled;
+ #animationCount = 0;
- if (!this.#foldersEnabled) {
- return;
- }
+ init() {
+ this.#foldersEnabled = !gZenWorkspaces.privateWindowOrDisabled;
- this.#initContextMenu();
- this.#initTabsPopup();
- this.#initEventListeners();
+ if (!this.#foldersEnabled) {
+ return;
}
- #initContextMenu() {
- const contextMenuItems = window.MozXULElement.parseXULToFragment(
- ``
- );
- document.getElementById('context_moveTabToGroup').before(contextMenuItems);
- const contextMenuItemsToolbar = window.MozXULElement.parseXULToFragment(
- ``
+ this.#initContextMenu();
+ this.#initTabsPopup();
+ this.#initEventListeners();
+ }
+
+ #initContextMenu() {
+ const contextMenuItems = window.MozXULElement.parseXULToFragment(
+ ``
+ );
+ document.getElementById('context_moveTabToGroup').before(contextMenuItems);
+ const contextMenuItemsToolbar = window.MozXULElement.parseXULToFragment(
+ ``
+ );
+ document.getElementById('toolbar-context-openANewTab').after(contextMenuItemsToolbar);
+
+ const folderActionsMenu = document.getElementById('zenFolderActions');
+ folderActionsMenu.addEventListener('popupshowing', (event) => {
+ const target = event.explicitOriginalTarget;
+ let folder;
+ if (gBrowser.isTabGroupLabel(target)) {
+ folder = target.group;
+ } else if (gBrowser.isTabGroupLabel(target.parentElement)) {
+ folder = target.parentElement.group;
+ } else if (
+ target.parentElement?.isZenFolder &&
+ target?.classList.contains('tab-group-label-container')
+ ) {
+ folder = target.parentElement;
+ }
+
+ // We only want to rename zen-folders as firefox groups don't work well with this
+ if (!folder?.isZenFolder) {
+ return;
+ }
+ this.#lastFolderContextMenu = folder;
+
+ const newSubfolderItem = document.getElementById('context_zenFolderNewSubfolder');
+ newSubfolderItem.setAttribute(
+ 'disabled',
+ folder.level >= this.#ZEN_MAX_SUBFOLDERS - 1 ? 'true' : 'false'
);
- document.getElementById('toolbar-context-openANewTab').after(contextMenuItemsToolbar);
-
- const folderActionsMenu = document.getElementById('zenFolderActions');
- folderActionsMenu.addEventListener('popupshowing', (event) => {
- const target = event.explicitOriginalTarget;
- let folder;
- if (gBrowser.isTabGroupLabel(target)) {
- folder = target.group;
- } else if (gBrowser.isTabGroupLabel(target.parentElement)) {
- folder = target.parentElement.group;
- } else if (
- target.parentElement?.isZenFolder &&
- target?.classList.contains('tab-group-label-container')
- ) {
- folder = target.parentElement;
- }
- // We only want to rename zen-folders as firefox groups don't work well with this
- if (!folder?.isZenFolder) {
- return;
+ const changeFolderSpace = document
+ .getElementById('context_zenChangeFolderSpace')
+ .querySelector('menupopup');
+ changeFolderSpace.innerHTML = '';
+ for (const workspace of [...gZenWorkspaces._workspaceCache.workspaces].reverse()) {
+ const item = document.createXULElement('menuitem');
+ item.className = 'zen-workspace-context-menu-item';
+ item.setAttribute('zen-workspace-id', workspace.uuid);
+ item.setAttribute('disabled', workspace.uuid === gZenWorkspaces.activeWorkspace);
+ let name = workspace.name;
+ const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
+ if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
+ name = `${workspace.icon} ${name}`;
}
- this.#lastFolderContextMenu = folder;
-
- const newSubfolderItem = document.getElementById('context_zenFolderNewSubfolder');
- newSubfolderItem.setAttribute(
- 'disabled',
- folder.level >= this.#ZEN_MAX_SUBFOLDERS - 1 ? 'true' : 'false'
- );
-
- const changeFolderSpace = document
- .getElementById('context_zenChangeFolderSpace')
- .querySelector('menupopup');
- changeFolderSpace.innerHTML = '';
- for (const workspace of [...gZenWorkspaces._workspaceCache.workspaces].reverse()) {
- const item = document.createXULElement('menuitem');
- item.className = 'zen-workspace-context-menu-item';
- item.setAttribute('zen-workspace-id', workspace.uuid);
- item.setAttribute('disabled', workspace.uuid === gZenWorkspaces.activeWorkspace);
- let name = workspace.name;
- const iconIsSvg = workspace.icon && workspace.icon.endsWith('.svg');
- if (workspace.icon && workspace.icon !== '' && !iconIsSvg) {
- name = `${workspace.icon} ${name}`;
- }
- item.setAttribute('label', name);
- if (iconIsSvg) {
- item.setAttribute('image', workspace.icon);
- item.classList.add('zen-workspace-context-icon');
- }
- item.addEventListener('command', (event) => {
- if (!this.#lastFolderContextMenu) return;
- this.changeFolderToSpace(
- this.#lastFolderContextMenu,
- event.target.closest('menuitem').getAttribute('zen-workspace-id')
- );
- });
- changeFolderSpace.appendChild(item);
+ item.setAttribute('label', name);
+ if (iconIsSvg) {
+ item.setAttribute('image', workspace.icon);
+ item.classList.add('zen-workspace-context-icon');
}
- });
-
- folderActionsMenu.addEventListener(
- 'popuphidden',
- (event) => {
- if (event.target === folderActionsMenu) {
- this.#lastFolderContextMenu = null;
- }
- },
- { once: true }
- );
+ item.addEventListener('command', (event) => {
+ if (!this.#lastFolderContextMenu) return;
+ this.changeFolderToSpace(
+ this.#lastFolderContextMenu,
+ event.target.closest('menuitem').getAttribute('zen-workspace-id')
+ );
+ });
+ changeFolderSpace.appendChild(item);
+ }
+ });
- folderActionsMenu.addEventListener('command', (event) => {
- if (!this.#lastFolderContextMenu) return;
- switch (event.target.id) {
- case 'context_zenFolderRename':
- this.#lastFolderContextMenu.rename();
- break;
- case 'context_zenFolderUnpack':
- this.#lastFolderContextMenu.unpackTabs();
- break;
- case 'context_zenFolderUnloadAll':
- this.#lastFolderContextMenu.unloadAllTabs(event);
- break;
- case 'context_zenFolderNewSubfolder':
- this.#lastFolderContextMenu.createSubfolder();
- break;
- case 'context_zenFolderDelete':
- this.#lastFolderContextMenu.delete();
- break;
- case 'context_zenFolderToSpace':
- this.#convertFolderToSpace(this.#lastFolderContextMenu);
- break;
- case 'context_zenFolderChangeIcon':
- this.changeFolderUserIcon(this.#lastFolderContextMenu);
- break;
+ folderActionsMenu.addEventListener(
+ 'popuphidden',
+ (event) => {
+ if (event.target === folderActionsMenu) {
+ this.#lastFolderContextMenu = null;
}
- });
- }
+ },
+ { once: true }
+ );
+
+ folderActionsMenu.addEventListener('command', (event) => {
+ if (!this.#lastFolderContextMenu) return;
+ switch (event.target.id) {
+ case 'context_zenFolderRename':
+ this.#lastFolderContextMenu.rename();
+ break;
+ case 'context_zenFolderUnpack':
+ this.#lastFolderContextMenu.unpackTabs();
+ break;
+ case 'context_zenFolderUnloadAll':
+ this.#lastFolderContextMenu.unloadAllTabs(event);
+ break;
+ case 'context_zenFolderNewSubfolder':
+ this.#lastFolderContextMenu.createSubfolder();
+ break;
+ case 'context_zenFolderDelete':
+ this.#lastFolderContextMenu.delete();
+ break;
+ case 'context_zenFolderToSpace':
+ this.#convertFolderToSpace(this.#lastFolderContextMenu);
+ break;
+ case 'context_zenFolderChangeIcon':
+ this.changeFolderUserIcon(this.#lastFolderContextMenu);
+ break;
+ }
+ });
+ }
- #initTabsPopup() {
- this.#popup = document.getElementById('zen-folder-tabs-popup');
+ #initTabsPopup() {
+ this.#popup = document.getElementById('zen-folder-tabs-popup');
- const search = this.#popup.querySelector('#zen-folder-tabs-list-search');
- const tabsList = this.#popup.querySelector('#zen-folder-tabs-list');
+ const search = this.#popup.querySelector('#zen-folder-tabs-list-search');
+ const tabsList = this.#popup.querySelector('#zen-folder-tabs-list');
- search.addEventListener('input', () => {
- const query = search.value.toLowerCase();
- for (const item of tabsList.children) {
- item.hidden = !item.getAttribute('data-label').includes(query);
- }
- });
+ search.addEventListener('input', () => {
+ const query = search.value.toLowerCase();
+ for (const item of tabsList.children) {
+ item.hidden = !item.getAttribute('data-label').includes(query);
+ }
+ });
- this.#popup.addEventListener('mouseover', () => {
- clearTimeout(this.#popupTimer);
- });
+ this.#popup.addEventListener('mouseover', () => {
+ clearTimeout(this.#popupTimer);
+ });
- this.#popup.addEventListener('mouseout', () => {
- this.#popupTimer = setTimeout(() => {
- if (this.#popup.matches(':hover')) return;
- this.#popup.hidePopup();
- }, 200);
- });
- }
+ this.#popup.addEventListener('mouseout', () => {
+ this.#popupTimer = setTimeout(() => {
+ if (this.#popup.matches(':hover')) return;
+ this.#popup.hidePopup();
+ }, 200);
+ });
+ }
- #initEventListeners() {
- window.addEventListener('TabGrouped', this);
- window.addEventListener('TabUngrouped', this);
- window.addEventListener('TabGroupCreate', this);
- window.addEventListener('TabPinned', this);
- window.addEventListener('TabUnpinned', this);
- window.addEventListener('TabGroupExpand', this);
- window.addEventListener('TabGroupCollapse', this);
- window.addEventListener('FolderGrouped', this);
- window.addEventListener('FolderUngrouped', this);
- window.addEventListener('TabSelect', this);
- window.addEventListener('TabOpen', this);
- const onNewFolder = this.#onNewFolder.bind(this);
- document
- .getElementById('zen-context-menu-new-folder')
- .addEventListener('command', onNewFolder);
- document
- .getElementById('zen-context-menu-new-folder-toolbar')
- .addEventListener('command', onNewFolder);
- SessionStore.promiseInitialized.then(() => {
- gBrowser.tabContainer.addEventListener('dragstart', this.cancelPopupTimer.bind(this));
- });
- }
+ #initEventListeners() {
+ window.addEventListener('TabGrouped', this);
+ window.addEventListener('TabUngrouped', this);
+ window.addEventListener('TabGroupCreate', this);
+ window.addEventListener('TabPinned', this);
+ window.addEventListener('TabUnpinned', this);
+ window.addEventListener('TabGroupExpand', this);
+ window.addEventListener('TabGroupCollapse', this);
+ window.addEventListener('FolderGrouped', this);
+ window.addEventListener('FolderUngrouped', this);
+ window.addEventListener('TabSelect', this);
+ window.addEventListener('TabOpen', this);
+ const onNewFolder = this.#onNewFolder.bind(this);
+ document.getElementById('zen-context-menu-new-folder').addEventListener('command', onNewFolder);
+ document
+ .getElementById('zen-context-menu-new-folder-toolbar')
+ .addEventListener('command', onNewFolder);
+ SessionStore.promiseInitialized.then(() => {
+ gBrowser.tabContainer.addEventListener('dragstart', this.cancelPopupTimer.bind(this));
+ });
+ }
- handleEvent(aEvent) {
- let methodName = `on_${aEvent.type}`;
- if (methodName in this) {
- this[methodName](aEvent);
- } else {
- throw new Error(`Unexpected event ${aEvent.type}`);
- }
+ handleEvent(aEvent) {
+ let methodName = `on_${aEvent.type}`;
+ if (methodName in this) {
+ this[methodName](aEvent);
+ } else {
+ throw new Error(`Unexpected event ${aEvent.type}`);
}
+ }
- on_TabGrouped(event) {
- const tab = event.detail;
- const group = tab.group;
- group.pinned = tab.pinned;
- const isActiveFolder = group?.activeGroups?.length > 0;
-
- if (isActiveFolder) {
- group.activeTabs = [...new Set([...group.activeTabs, tab])].sort(
- (a, b) => a._tPos > b._tPos
- );
- }
-
- if (group.hasAttribute('split-view-group') && group.hasAttribute('zen-pinned-changed')) {
- // zen-pinned-changed remove it and set it to had-zen-pinned-changed to keep
- // track of the original pinned state
- group.removeAttribute('zen-pinned-changed');
- group.setAttribute('had-zen-pinned-changed', true);
- }
+ on_TabGrouped(event) {
+ const tab = event.detail;
+ const group = tab.group;
+ group.pinned = tab.pinned;
+ const isActiveFolder = group?.activeGroups?.length > 0;
- if (group.collapsed && !this._sessionRestoring) {
- group.collapsed = group.hasAttribute('has-active');
- }
+ if (isActiveFolder) {
+ group.activeTabs = [...new Set([...group.activeTabs, tab])].sort((a, b) => a._tPos > b._tPos);
}
- on_FolderGrouped(event) {
- if (this._sessionRestoring) return;
- const folder = event.detail;
- const parentFolder = event.target;
- const isActiveFolder = parentFolder?.activeGroups?.length > 0;
- const isSplitView = folder.hasAttribute('split-view-group');
- if (isActiveFolder && isSplitView) {
- parentFolder.activeTabs = [...new Set([...parentFolder.activeTabs, ...folder.tabs])].sort(
- (a, b) => a._tPos > b._tPos
- );
- }
- parentFolder.collapsed = isActiveFolder;
+ if (group.hasAttribute('split-view-group') && group.hasAttribute('zen-pinned-changed')) {
+ // zen-pinned-changed remove it and set it to had-zen-pinned-changed to keep
+ // track of the original pinned state
+ group.removeAttribute('zen-pinned-changed');
+ group.setAttribute('had-zen-pinned-changed', true);
}
- on_FolderUngrouped(event) {
- if (this._sessionRestoring) return;
- const parentFolder = event.target;
- const folder = event.detail;
- for (const tab of folder.tabs) {
- this.animateUnload(parentFolder, tab, true);
- }
+ if (group.collapsed && !this._sessionRestoring) {
+ group.collapsed = group.hasAttribute('has-active');
}
+ }
- async on_TabSelect(event) {
- const tab = gZenGlanceManager.getTabOrGlanceParent(event.target);
- let group = tab?.group;
- if (group?.hasAttribute('split-view-group')) group = group?.group;
- if (!group?.isZenFolder) {
- return;
- }
+ on_FolderGrouped(event) {
+ if (this._sessionRestoring) return;
+ const folder = event.detail;
+ const parentFolder = event.target;
+ const isActiveFolder = parentFolder?.activeGroups?.length > 0;
+ const isSplitView = folder.hasAttribute('split-view-group');
+ if (isActiveFolder && isSplitView) {
+ parentFolder.activeTabs = [...new Set([...parentFolder.activeTabs, ...folder.tabs])].sort(
+ (a, b) => a._tPos > b._tPos
+ );
+ }
+ parentFolder.collapsed = isActiveFolder;
+ }
- const collapsedRoot = group.rootMostCollapsedFolder;
- if (!collapsedRoot) {
- return;
- }
+ on_FolderUngrouped(event) {
+ if (this._sessionRestoring) return;
+ const parentFolder = event.target;
+ const folder = event.detail;
+ for (const tab of folder.tabs) {
+ this.animateUnload(parentFolder, tab, true);
+ }
+ }
- collapsedRoot.setAttribute('has-active', 'true');
- await this.animateSelect(collapsedRoot);
- gBrowser.tabContainer._invalidateCachedTabs();
+ async on_TabSelect(event) {
+ const tab = gZenGlanceManager.getTabOrGlanceParent(event.target);
+ let group = tab?.group;
+ if (group?.hasAttribute('split-view-group')) group = group?.group;
+ if (!group?.isZenFolder) {
+ return;
}
- on_TabOpen(event) {
- const tab = event.target;
- const group = tab.group;
- if (!group?.isZenFolder || tab.pinned) return;
- // Edge case: In occations where we add a tab with an ownerTab
- // inside a folder, the tab gets added into the folder in an
- // unpinned state. We need to pin it and re-add it into the folder.
- if (Services.prefs.getBoolPref('zen.folders.owned-tabs-in-folder')) {
- gBrowser.pinTab(tab);
- group.addTabs([tab]);
- }
+ const collapsedRoot = group.rootMostCollapsedFolder;
+ if (!collapsedRoot) {
+ return;
}
- async on_TabUngrouped(event) {
- const tab = event.detail;
- const group = event.target;
- if (group.hasAttribute('split-view-group') && tab.hasAttribute('had-zen-pinned-changed')) {
- tab.setAttribute('zen-pinned-changed', true);
- tab.removeAttribute('had-zen-pinned-changed');
- }
+ collapsedRoot.setAttribute('has-active', 'true');
+ await this.animateSelect(collapsedRoot);
+ gBrowser.tabContainer._invalidateCachedTabs();
+ }
- await this.animateUnload(group, tab, true);
+ on_TabOpen(event) {
+ const tab = event.target;
+ const group = tab.group;
+ if (!group?.isZenFolder || tab.pinned) return;
+ // Edge case: In occations where we add a tab with an ownerTab
+ // inside a folder, the tab gets added into the folder in an
+ // unpinned state. We need to pin it and re-add it into the folder.
+ if (Services.prefs.getBoolPref('zen.folders.owned-tabs-in-folder')) {
+ gBrowser.pinTab(tab);
+ group.addTabs([tab]);
}
+ }
- on_TabGroupCreate(event) {
- const group = event.target;
- const tabs = group.tabs;
- if (!group.pinned) {
- return;
- }
- for (const tab of tabs) {
- if (tab.hasAttribute('zen-pinned-changed')) {
- tab.removeAttribute('zen-pinned-changed');
- tab.setAttribute('had-zen-pinned-changed', true);
- }
- }
+ async on_TabUngrouped(event) {
+ const tab = event.detail;
+ const group = event.target;
+ if (group.hasAttribute('split-view-group') && tab.hasAttribute('had-zen-pinned-changed')) {
+ tab.setAttribute('zen-pinned-changed', true);
+ tab.removeAttribute('had-zen-pinned-changed');
}
- on_TabPinned(event) {
- const tab = event.target;
- const group = tab.group;
- if (group && group.hasAttribute('split-view-group')) {
- group.pinned = true;
- }
- }
+ await this.animateUnload(group, tab, true);
+ }
- on_TabUnpinned(event) {
- const tab = event.target;
- const group = tab.group;
- if (group && group.hasAttribute('split-view-group')) {
- group.pinned = false;
- }
+ on_TabGroupCreate(event) {
+ const group = event.target;
+ const tabs = group.tabs;
+ if (!group.pinned) {
+ return;
}
-
- cancelPopupTimer() {
- if (this.#mouseTimer) {
- clearTimeout(this.#mouseTimer);
- this.#mouseTimer = null;
- }
- if (this.#popup) {
- this.#popup.hidePopup();
+ for (const tab of tabs) {
+ if (tab.hasAttribute('zen-pinned-changed')) {
+ tab.removeAttribute('zen-pinned-changed');
+ tab.setAttribute('had-zen-pinned-changed', true);
}
}
+ }
- async on_TabGroupCollapse(event) {
- const group = event.target;
- if (!group.isZenFolder) return;
-
- await this.animateCollapse(group);
+ on_TabPinned(event) {
+ const tab = event.target;
+ const group = tab.group;
+ if (group && group.hasAttribute('split-view-group')) {
+ group.pinned = true;
}
+ }
- async on_TabGroupExpand(event) {
- const group = event.target;
- if (!group.isZenFolder) return;
+ on_TabUnpinned(event) {
+ const tab = event.target;
+ const group = tab.group;
+ if (group && group.hasAttribute('split-view-group')) {
+ group.pinned = false;
+ }
+ }
- await this.animateExpand(group);
+ cancelPopupTimer() {
+ if (this.#mouseTimer) {
+ clearTimeout(this.#mouseTimer);
+ this.#mouseTimer = null;
+ }
+ if (this.#popup) {
+ this.#popup.hidePopup();
}
+ }
- #onNewFolder(event) {
- const isFromToolbar = event.target.id === 'zen-context-menu-new-folder-toolbar';
- const contextMenu = event.target.parentElement;
- let tabs = TabContextMenu.contextTab?.multiselected
- ? gBrowser.selectedTabs
- : [TabContextMenu.contextTab];
- let triggerTab =
- contextMenu.triggerNode &&
- (contextMenu.triggerNode.tab || contextMenu.triggerNode.closest('tab'));
+ async on_TabGroupCollapse(event) {
+ const group = event.target;
+ if (!group.isZenFolder) return;
- const selectedTabs = gBrowser.selectedTabs;
- if (selectedTabs.length > 1) {
- tabs.push(triggerTab, ...gBrowser.selectedTabs);
- } else {
- tabs.push(triggerTab);
- }
- if (isFromToolbar) {
- tabs = [];
- }
+ await this.animateCollapse(group);
+ }
- const canInsertBefore =
- !isFromToolbar &&
- !triggerTab.hasAttribute('zen-essential') &&
- !triggerTab?.group?.hasAttribute('split-view-group') &&
- this.canDropElement({ isZenFolder: true }, triggerTab);
+ async on_TabGroupExpand(event) {
+ const group = event.target;
+ if (!group.isZenFolder) return;
- this.createFolder(tabs, {
- insertAfter: !canInsertBefore ? triggerTab?.group : null,
- insertBefore: canInsertBefore ? triggerTab : null,
- renameFolder: true,
- });
+ await this.animateExpand(group);
+ }
+
+ #onNewFolder(event) {
+ const isFromToolbar = event.target.id === 'zen-context-menu-new-folder-toolbar';
+ const contextMenu = event.target.parentElement;
+ let tabs = TabContextMenu.contextTab?.multiselected
+ ? gBrowser.selectedTabs
+ : [TabContextMenu.contextTab];
+ let triggerTab =
+ contextMenu.triggerNode &&
+ (contextMenu.triggerNode.tab || contextMenu.triggerNode.closest('tab'));
+
+ const selectedTabs = gBrowser.selectedTabs;
+ if (selectedTabs.length > 1) {
+ tabs.push(triggerTab, ...gBrowser.selectedTabs);
+ } else {
+ tabs.push(triggerTab);
+ }
+ if (isFromToolbar) {
+ tabs = [];
}
- async #convertFolderToSpace(folder) {
- const currentWorkspace = gZenWorkspaces.getActiveWorkspaceFromCache();
- let selectedTab = folder.tabs.find((tab) => tab.selected);
- const icon = folder.icon?.querySelector('svg .icon image');
+ const canInsertBefore =
+ !isFromToolbar &&
+ !triggerTab.hasAttribute('zen-essential') &&
+ !triggerTab?.group?.hasAttribute('split-view-group') &&
+ this.canDropElement({ isZenFolder: true }, triggerTab);
+
+ this.createFolder(tabs, {
+ insertAfter: !canInsertBefore ? triggerTab?.group : null,
+ insertBefore: canInsertBefore ? triggerTab : null,
+ renameFolder: true,
+ });
+ }
- const newSpace = await gZenWorkspaces.createAndSaveWorkspace(
- folder.label,
- /* icon= */ icon?.getAttribute('href'),
- /* dontChange= */ false,
- currentWorkspace.containerTabId,
- {
- beforeChangeCallback: async (newWorkspace) => {
- await new Promise((resolve) => {
- requestAnimationFrame(async () => {
- const workspacePinnedContainer = gZenWorkspaces.workspaceElement(
- newWorkspace.uuid
- ).pinnedTabsContainer;
- const tabs = folder.allItems.filter((tab) => !tab.hasAttribute('zen-empty-tab'));
- workspacePinnedContainer.append(...tabs);
- await folder.delete();
- gBrowser.tabContainer._invalidateCachedTabs();
- if (selectedTab) {
- selectedTab.setAttribute('zen-workspace-id', newWorkspace.uuid);
- selectedTab.removeAttribute('folder-active');
- gZenWorkspaces._lastSelectedWorkspaceTabs[newWorkspace.uuid] = selectedTab;
- }
- resolve();
- });
+ async #convertFolderToSpace(folder) {
+ const currentWorkspace = gZenWorkspaces.getActiveWorkspaceFromCache();
+ let selectedTab = folder.tabs.find((tab) => tab.selected);
+ const icon = folder.icon?.querySelector('svg .icon image');
+
+ const newSpace = await gZenWorkspaces.createAndSaveWorkspace(
+ folder.label,
+ /* icon= */ icon?.getAttribute('href'),
+ /* dontChange= */ false,
+ currentWorkspace.containerTabId,
+ {
+ beforeChangeCallback: async (newWorkspace) => {
+ await new Promise((resolve) => {
+ requestAnimationFrame(async () => {
+ const workspacePinnedContainer = gZenWorkspaces.workspaceElement(
+ newWorkspace.uuid
+ ).pinnedTabsContainer;
+ const tabs = folder.allItems.filter((tab) => !tab.hasAttribute('zen-empty-tab'));
+ workspacePinnedContainer.append(...tabs);
+ await folder.delete();
+ gBrowser.tabContainer._invalidateCachedTabs();
+ if (selectedTab) {
+ selectedTab.setAttribute('zen-workspace-id', newWorkspace.uuid);
+ selectedTab.removeAttribute('folder-active');
+ gZenWorkspaces._lastSelectedWorkspaceTabs[newWorkspace.uuid] = selectedTab;
+ }
+ resolve();
});
- },
- }
- );
- // Change the ID for all tabs
- for (const tab of gBrowser.tabs) {
- if (!tab.hasAttribute('zen-essential')) {
- tab.setAttribute('zen-workspace-id', newSpace.uuid);
- tab.style.opacity = '';
- tab.style.height = '';
- }
- gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
- if (gZenWorkspaces._lastSelectedWorkspaceTabs[currentWorkspace.uuid] === tab) {
- // This tab is no longer the last selected tab in the previous workspace because it's being moved to
- // the current workspace
- delete gZenWorkspaces._lastSelectedWorkspaceTabs[currentWorkspace.uuid];
- }
+ });
+ },
}
- }
-
- changeFolderToSpace(folder, workspaceId) {
- const currentWorkspace = gZenWorkspaces.getActiveWorkspaceFromCache();
- if (currentWorkspace.uuid === workspaceId) {
- return;
+ );
+ // Change the ID for all tabs
+ for (const tab of gBrowser.tabs) {
+ if (!tab.hasAttribute('zen-essential')) {
+ tab.setAttribute('zen-workspace-id', newSpace.uuid);
+ tab.style.opacity = '';
+ tab.style.height = '';
}
- const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId);
- const pinnedTabsContainer = workspaceElement.pinnedTabsContainer;
- pinnedTabsContainer.insertBefore(folder, pinnedTabsContainer.lastChild);
- for (const tab of folder.tabs) {
- tab.setAttribute('zen-workspace-id', workspaceId);
- // This sets the ID for the current folder and any sub-folder
- // we may encounter
- tab.group.setAttribute('zen-workspace-id', workspaceId);
- gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
- if (gZenWorkspaces._lastSelectedWorkspaceTabs[workspaceId] === tab) {
- // This tab is no longer the last selected tab in the previous workspace because it's being moved to a new workspace
- delete gZenWorkspaces._lastSelectedWorkspaceTabs[workspaceId];
- }
+ gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
+ if (gZenWorkspaces._lastSelectedWorkspaceTabs[currentWorkspace.uuid] === tab) {
+ // This tab is no longer the last selected tab in the previous workspace because it's being moved to
+ // the current workspace
+ delete gZenWorkspaces._lastSelectedWorkspaceTabs[currentWorkspace.uuid];
}
- folder.dispatchEvent(new CustomEvent('ZenFolderChangedWorkspace', { bubbles: true }));
- gZenWorkspaces.changeWorkspaceWithID(workspaceId).then(() => {
- gBrowser.moveTabTo(folder, { elementIndex: 0, forceUngrouped: true });
- });
}
+ }
- canDropElement(element, targetElement) {
- const isZenFolder = element?.isZenFolder;
- const level = targetElement?.group?.level + 1;
- if (isZenFolder && level >= this.#ZEN_MAX_SUBFOLDERS) {
- return false;
+ changeFolderToSpace(folder, workspaceId) {
+ const currentWorkspace = gZenWorkspaces.getActiveWorkspaceFromCache();
+ if (currentWorkspace.uuid === workspaceId) {
+ return;
+ }
+ const workspaceElement = gZenWorkspaces.workspaceElement(workspaceId);
+ const pinnedTabsContainer = workspaceElement.pinnedTabsContainer;
+ pinnedTabsContainer.insertBefore(folder, pinnedTabsContainer.lastChild);
+ for (const tab of folder.tabs) {
+ tab.setAttribute('zen-workspace-id', workspaceId);
+ // This sets the ID for the current folder and any sub-folder
+ // we may encounter
+ tab.group.setAttribute('zen-workspace-id', workspaceId);
+ gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
+ if (gZenWorkspaces._lastSelectedWorkspaceTabs[workspaceId] === tab) {
+ // This tab is no longer the last selected tab in the previous workspace because it's being moved to a new workspace
+ delete gZenWorkspaces._lastSelectedWorkspaceTabs[workspaceId];
}
- return true;
}
+ folder.dispatchEvent(new CustomEvent('ZenFolderChangedWorkspace', { bubbles: true }));
+ gZenWorkspaces.changeWorkspaceWithID(workspaceId).then(() => {
+ gBrowser.moveTabTo(folder, { elementIndex: 0, forceUngrouped: true });
+ });
+ }
- createFolder(tabs = [], options = {}) {
- const filteredTabs = tabs
- .filter((tab) => !tab.hasAttribute('zen-essential'))
- .map((tab) => {
- gBrowser.pinTab(tab);
- if (tab?.group?.hasAttribute('split-view-group')) {
- tab = tab.group;
- }
- return tab;
- });
+ canDropElement(element, targetElement) {
+ const isZenFolder = element?.isZenFolder;
+ const level = targetElement?.group?.level + 1;
+ if (isZenFolder && level >= this.#ZEN_MAX_SUBFOLDERS) {
+ return false;
+ }
+ return true;
+ }
- const workspacePinned = gZenWorkspaces.workspaceElement(
- options.workspaceId
- )?.pinnedTabsContainer;
- const pinnedContainer =
- options.workspaceId && workspacePinned
- ? workspacePinned
- : gZenWorkspaces.pinnedTabsContainer;
- const insertBefore =
- options.insertBefore || pinnedContainer.querySelector('.pinned-tabs-container-separator');
- const emptyTab = gBrowser.addTab('about:blank', {
- skipAnimation: true,
- pinned: true,
- triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
- _forZenEmptyTab: true,
- createLazyBrowser: true,
+ createFolder(tabs = [], options = {}) {
+ const filteredTabs = tabs
+ .filter((tab) => !tab.hasAttribute('zen-essential'))
+ .map((tab) => {
+ gBrowser.pinTab(tab);
+ if (tab?.group?.hasAttribute('split-view-group')) {
+ tab = tab.group;
+ }
+ return tab;
});
- gBrowser.pinTab(emptyTab);
- tabs = [emptyTab, ...filteredTabs];
+ const workspacePinned = gZenWorkspaces.workspaceElement(
+ options.workspaceId
+ )?.pinnedTabsContainer;
+ const pinnedContainer =
+ options.workspaceId && workspacePinned ? workspacePinned : gZenWorkspaces.pinnedTabsContainer;
+ const insertBefore =
+ options.insertBefore || pinnedContainer.querySelector('.pinned-tabs-container-separator');
+ const emptyTab = gBrowser.addTab('about:blank', {
+ skipAnimation: true,
+ pinned: true,
+ triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
+ _forZenEmptyTab: true,
+ createLazyBrowser: true,
+ });
+
+ gBrowser.pinTab(emptyTab);
+ tabs = [emptyTab, ...filteredTabs];
+
+ const folder = this._createFolderNode(options);
+ if (options.initialPinId) {
+ folder.setAttribute('zen-pin-id', options.initialPinId);
+ }
- const folder = this._createFolderNode(options);
- if (options.initialPinId) {
- folder.setAttribute('zen-pin-id', options.initialPinId);
- }
+ if (options.insertAfter) {
+ options.insertAfter.after(folder);
+ } else {
+ insertBefore.before(folder);
+ }
+ gZenVerticalTabsManager.animateItemOpen(folder);
- if (options.insertAfter) {
- options.insertAfter.after(folder);
- } else {
- insertBefore.before(folder);
- }
- gZenVerticalTabsManager.animateItemOpen(folder);
-
- folder.addTabs(tabs);
-
- // Fixes bug1953801 and bug1954689
- // Ensure that the tab state cache is updated immediately after creating
- // a group. This is necessary because we consider group creation a
- // deliberate user action indicating the tab has importance for the user.
- // Without this, it is not possible to save and close a tab group with
- // a short lifetime.
- folder.tabs.forEach((tab) => {
- gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
- });
+ folder.addTabs(tabs);
- this.updateFolderIcon(folder, 'auto');
+ // Fixes bug1953801 and bug1954689
+ // Ensure that the tab state cache is updated immediately after creating
+ // a group. This is necessary because we consider group creation a
+ // deliberate user action indicating the tab has importance for the user.
+ // Without this, it is not possible to save and close a tab group with
+ // a short lifetime.
+ folder.tabs.forEach((tab) => {
+ gBrowser.TabStateFlusher.flush(tab.linkedBrowser);
+ });
- if (options.renameFolder) {
- folder.rename();
- }
+ this.updateFolderIcon(folder, 'auto');
- this.#groupInit(folder);
- return folder;
+ if (options.renameFolder) {
+ folder.rename();
}
- _createFolderNode(options = {}) {
- const folder = document.createXULElement('zen-folder', { is: 'zen-folder' });
- let id = options.id;
- if (!id) {
- // Note: If this changes, make sure to also update the
- // getExtTabGroupIdForInternalTabGroupId implementation in
- // browser/components/extensions/parent/ext-browser.js.
- // See: Bug 1960104 - Improve tab group ID generation in addTabGroup
- id = `${Date.now()}-${Math.round(Math.random() * 100)}`;
- }
- folder.id = id;
- folder.label = options.label || 'New Folder';
- folder.saveOnWindowClose = !!options.saveOnWindowClose;
- folder.color = 'zen-workspace-color';
-
- folder.setAttribute(
- 'zen-workspace-id',
- options.workspaceId || gZenWorkspaces.activeWorkspace
- );
+ this.#groupInit(folder);
+ return folder;
+ }
- // note: We set if the folder is collapsed some time after creation.
- // we do this to ensure marginBottom is set correctly in the case
- // that we want it to initially be collapsed.
- setTimeout(
- (folder) => {
- gZenPinnedTabManager.promiseInitializedPinned.then(() => {
- folder.collapsed = !!options.collapsed;
- });
- },
- 0,
- folder
- );
- return folder;
+ _createFolderNode(options = {}) {
+ const folder = document.createXULElement('zen-folder', { is: 'zen-folder' });
+ let id = options.id;
+ if (!id) {
+ // Note: If this changes, make sure to also update the
+ // getExtTabGroupIdForInternalTabGroupId implementation in
+ // browser/components/extensions/parent/ext-browser.js.
+ // See: Bug 1960104 - Improve tab group ID generation in addTabGroup
+ id = `${Date.now()}-${Math.round(Math.random() * 100)}`;
}
+ folder.id = id;
+ folder.label = options.label || 'New Folder';
+ folder.saveOnWindowClose = !!options.saveOnWindowClose;
+ folder.color = 'zen-workspace-color';
+
+ folder.setAttribute('zen-workspace-id', options.workspaceId || gZenWorkspaces.activeWorkspace);
+
+ // note: We set if the folder is collapsed some time after creation.
+ // we do this to ensure marginBottom is set correctly in the case
+ // that we want it to initially be collapsed.
+ setTimeout(
+ (folder) => {
+ gZenPinnedTabManager.promiseInitializedPinned.then(() => {
+ folder.collapsed = !!options.collapsed;
+ });
+ },
+ 0,
+ folder
+ );
+ return folder;
+ }
- handleTabPin(tab) {
- const group = tab.group;
- if (!group) {
- return false;
- }
- if (group.hasAttribute('split-view-group') && !this._piningFolder) {
- this._piningFolder = true;
- for (const otherTab of group.tabs) {
- gZenPinnedTabManager.resetPinChangedUrl(otherTab);
- if (tab === otherTab) {
- continue;
- }
- gBrowser.pinTab(otherTab);
+ handleTabPin(tab) {
+ const group = tab.group;
+ if (!group) {
+ return false;
+ }
+ if (group.hasAttribute('split-view-group') && !this._piningFolder) {
+ this._piningFolder = true;
+ for (const otherTab of group.tabs) {
+ gZenPinnedTabManager.resetPinChangedUrl(otherTab);
+ if (tab === otherTab) {
+ continue;
}
- this._piningFolder = false;
- gBrowser.pinnedTabsContainer.insertBefore(group, gBrowser.pinnedTabsContainer.lastChild);
- gBrowser.tabContainer._invalidateCachedTabs();
- return true;
+ gBrowser.pinTab(otherTab);
}
- return this._piningFolder;
+ this._piningFolder = false;
+ gBrowser.pinnedTabsContainer.insertBefore(group, gBrowser.pinnedTabsContainer.lastChild);
+ gBrowser.tabContainer._invalidateCachedTabs();
+ return true;
}
+ return this._piningFolder;
+ }
- handleTabUnpin(tab) {
- tab.style.removeProperty('--zen-folder-indent');
- const group = tab.group;
- if (!group) {
- return false;
- }
- if (group.hasAttribute('split-view-group') && !this._piningFolder) {
- this._piningFolder = true;
- for (const otherTab of group.tabs) {
- if (tab === otherTab) {
- continue;
- }
- gBrowser.unpinTab(otherTab);
+ handleTabUnpin(tab) {
+ tab.style.removeProperty('--zen-folder-indent');
+ const group = tab.group;
+ if (!group) {
+ return false;
+ }
+ if (group.hasAttribute('split-view-group') && !this._piningFolder) {
+ this._piningFolder = true;
+ for (const otherTab of group.tabs) {
+ if (tab === otherTab) {
+ continue;
}
- this._piningFolder = false;
- gZenWorkspaces.activeWorkspaceStrip.prepend(group);
- gBrowser.tabContainer._invalidateCachedTabs();
- return true;
+ gBrowser.unpinTab(otherTab);
}
- return this._piningFolder;
+ this._piningFolder = false;
+ gZenWorkspaces.activeWorkspaceStrip.prepend(group);
+ gBrowser.tabContainer._invalidateCachedTabs();
+ return true;
}
+ return this._piningFolder;
+ }
- openTabsPopup(event) {
- event.stopPropagation();
- if (document.documentElement.getAttribute('zen-renaming-tab') || gURLBar.focused) {
- return;
- }
+ openTabsPopup(event) {
+ event.stopPropagation();
+ if (document.documentElement.getAttribute('zen-renaming-tab') || gURLBar.focused) {
+ return;
+ }
- const activeGroup = event.target.parentElement;
- if (
- activeGroup.tabs.filter((tab) => this.#shouldAppearOnTabSearch(tab, activeGroup)).length ===
- 0
- ) {
- // If the group has no tabs, we don't show the popup
- return;
+ const activeGroup = event.target.parentElement;
+ if (
+ activeGroup.tabs.filter((tab) => this.#shouldAppearOnTabSearch(tab, activeGroup)).length === 0
+ ) {
+ // If the group has no tabs, we don't show the popup
+ return;
+ }
+ document.getElementById('zen-folder-tabs-search-no-results').hidden = true;
+ this.#populateTabsList(activeGroup);
+
+ const search = this.#popup.querySelector('#zen-folder-tabs-list-search');
+ document.l10n.setArgs(search, {
+ 'folder-name': activeGroup.name,
+ });
+ const tabsList = this.#popup.querySelector('#zen-folder-tabs-list');
+
+ const onSearchInput = () => {
+ const query = search.value.toLowerCase();
+ let foundTabs = 0;
+ for (const item of tabsList.children) {
+ const found = item.getAttribute('data-label').includes(query);
+ item.hidden = !found;
+ if (found) {
+ foundTabs++;
+ }
}
- document.getElementById('zen-folder-tabs-search-no-results').hidden = true;
- this.#populateTabsList(activeGroup);
-
- const search = this.#popup.querySelector('#zen-folder-tabs-list-search');
- document.l10n.setArgs(search, {
- 'folder-name': activeGroup.name,
- });
- const tabsList = this.#popup.querySelector('#zen-folder-tabs-list');
-
- const onSearchInput = () => {
- const query = search.value.toLowerCase();
- let foundTabs = 0;
- for (const item of tabsList.children) {
- const found = item.getAttribute('data-label').includes(query);
- item.hidden = !found;
- if (found) {
- foundTabs++;
- }
+ document.getElementById('zen-folder-tabs-search-no-results').hidden = foundTabs > 0;
+ };
+ search.addEventListener('input', onSearchInput);
+
+ const onKeyDown = (event) => {
+ // Arrow down and up to navigate through the list
+ if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
+ event.preventDefault();
+ const items = Array.from(tabsList.children).filter((item) => !item.hidden);
+ if (items.length === 0) return;
+ let index = items.indexOf(tabsList.querySelector('.folders-tabs-list-item[selected]'));
+ if (event.key === 'ArrowDown') {
+ index = (index + 1) % items.length;
+ } else if (event.key === 'ArrowUp') {
+ index = (index - 1 + items.length) % items.length;
}
- document.getElementById('zen-folder-tabs-search-no-results').hidden = foundTabs > 0;
- };
- search.addEventListener('input', onSearchInput);
-
- const onKeyDown = (event) => {
- // Arrow down and up to navigate through the list
- if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
- event.preventDefault();
- const items = Array.from(tabsList.children).filter((item) => !item.hidden);
- if (items.length === 0) return;
- let index = items.indexOf(tabsList.querySelector('.folders-tabs-list-item[selected]'));
- if (event.key === 'ArrowDown') {
- index = (index + 1) % items.length;
- } else if (event.key === 'ArrowUp') {
- index = (index - 1 + items.length) % items.length;
- }
- items.forEach((item) => item.removeAttribute('selected'));
- const targetItem = items[index];
- targetItem.setAttribute('selected', 'true');
- targetItem.scrollIntoView({ block: 'start', behavior: 'smooth' });
- } else if (event.key === 'Enter') {
- // Enter to select the currently highlighted item
- const highlightedItem = tabsList.querySelector('.folders-tabs-list-item[selected]');
- if (highlightedItem) {
- highlightedItem.click();
- }
+ items.forEach((item) => item.removeAttribute('selected'));
+ const targetItem = items[index];
+ targetItem.setAttribute('selected', 'true');
+ targetItem.scrollIntoView({ block: 'start', behavior: 'smooth' });
+ } else if (event.key === 'Enter') {
+ // Enter to select the currently highlighted item
+ const highlightedItem = tabsList.querySelector('.folders-tabs-list-item[selected]');
+ if (highlightedItem) {
+ highlightedItem.click();
}
- };
- document.addEventListener('keydown', onKeyDown);
-
- const target = event.target;
- target.setAttribute('open', true);
+ }
+ };
+ document.addEventListener('keydown', onKeyDown);
+
+ const target = event.target;
+ target.setAttribute('open', true);
+
+ const handlePopupHidden = (event) => {
+ if (event.target !== this.#popup) return;
+ search.value = '';
+ target.removeAttribute('open');
+ search.removeEventListener('input', onSearchInput);
+ document.removeEventListener('keydown', onKeyDown);
+ };
+
+ this.#popup.addEventListener(
+ 'popupshown',
+ () => {
+ search.focus();
+ search.select();
+ },
+ { once: true }
+ );
+
+ this.#popup.addEventListener('popuphidden', handlePopupHidden, { once: true });
+ this.#popup.openPopup(target, this.#searchPopupOptions);
+ }
- const handlePopupHidden = (event) => {
- if (event.target !== this.#popup) return;
- search.value = '';
- target.removeAttribute('open');
- search.removeEventListener('input', onSearchInput);
- document.removeEventListener('keydown', onKeyDown);
- };
+ get #searchPopupOptions() {
+ const isRightSide = gZenVerticalTabsManager._prefsRightSide;
+ const position = isRightSide ? 'topleft topright' : 'topright topleft';
+ return {
+ position: position,
+ x: 5,
+ y: -25,
+ };
+ }
- this.#popup.addEventListener(
- 'popupshown',
- () => {
- search.focus();
- search.select();
- },
- { once: true }
- );
+ #shouldAppearOnTabSearch(tab, group) {
+ // Note that tab.visible and tab.hidden act in different ways.
+ // We don't want to show already visible tabs in the search results.
+ // That's why we need to do the active tab search, tab.hidden doesn't
+ // account for the visibility of the tab itself, it's just a literal
+ // representation of the `hidden` attribute.
+ const tabIsInActiveGroup = group.activeTabs.includes(tab);
+ return !tabIsInActiveGroup && !(tab.hidden || tab.hasAttribute('zen-empty-tab'));
+ }
- this.#popup.addEventListener('popuphidden', handlePopupHidden, { once: true });
- this.#popup.openPopup(target, this.#searchPopupOptions);
- }
+ #populateTabsList(group) {
+ const tabsList = this.#popup.querySelector('#zen-folder-tabs-list');
+ tabsList.replaceChildren();
- get #searchPopupOptions() {
- const isRightSide = gZenVerticalTabsManager._prefsRightSide;
- const position = isRightSide ? 'topleft topright' : 'topright topleft';
- return {
- position: position,
- x: 5,
- y: -25,
- };
- }
+ for (const tab of group.tabs) {
+ if (!this.#shouldAppearOnTabSearch(tab, group)) continue;
- #shouldAppearOnTabSearch(tab, group) {
- // Note that tab.visible and tab.hidden act in different ways.
- // We don't want to show already visible tabs in the search results.
- // That's why we need to do the active tab search, tab.hidden doesn't
- // account for the visibility of the tab itself, it's just a literal
- // representation of the `hidden` attribute.
- const tabIsInActiveGroup = group.activeTabs.includes(tab);
- return !tabIsInActiveGroup && !(tab.hidden || tab.hasAttribute('zen-empty-tab'));
- }
+ const item = document.createElement('div');
+ item.className = 'folders-tabs-list-item';
- #populateTabsList(group) {
- const tabsList = this.#popup.querySelector('#zen-folder-tabs-list');
- tabsList.replaceChildren();
+ const content = document.createElement('div');
+ content.className = 'folders-tabs-list-item-content';
- for (const tab of group.tabs) {
- if (!this.#shouldAppearOnTabSearch(tab, group)) continue;
+ const icon = document.createElement('img');
+ icon.className = 'folders-tabs-list-item-icon';
- const item = document.createElement('div');
- item.className = 'folders-tabs-list-item';
+ let tabURL = tab.linkedBrowser?.currentURI?.spec || '';
+ try {
+ // Get the hostname from the URL
+ const url = new URL(tabURL);
+ tabURL = url.hostname || tabURL;
+ } catch {
+ // We don't need to do anything if the URL is invalid. e.g. about:blank
+ }
+ let tabLabel = tab.label || '';
+ let iconURL = gBrowser.getIcon(tab) || PlacesUtils.favicons.defaultFavicon.spec;
- const content = document.createElement('div');
- content.className = 'folders-tabs-list-item-content';
+ icon.src = iconURL;
- const icon = document.createElement('img');
- icon.className = 'folders-tabs-list-item-icon';
+ const labelsContainer = document.createElement('div');
+ labelsContainer.className = 'folders-tabs-list-item-labels';
- let tabURL = tab.linkedBrowser?.currentURI?.spec || '';
- try {
- // Get the hostname from the URL
- const url = new URL(tabURL);
- tabURL = url.hostname || tabURL;
- } catch {
- // We don't need to do anything if the URL is invalid. e.g. about:blank
- }
- let tabLabel = tab.label || '';
- let iconURL = gBrowser.getIcon(tab) || PlacesUtils.favicons.defaultFavicon.spec;
+ const mainLabel = document.createElement('div');
+ mainLabel.className = 'folders-tabs-list-item-label';
+ mainLabel.textContent = tabLabel;
- icon.src = iconURL;
+ const secondaryLabel = document.createElement('div');
+ secondaryLabel.className = 'tab-list-item-secondary-label';
+ secondaryLabel.textContent = `${formatRelativeTime(tab.lastAccessed)} • ${tab.group.label}`;
- const labelsContainer = document.createElement('div');
- labelsContainer.className = 'folders-tabs-list-item-labels';
+ labelsContainer.append(mainLabel, secondaryLabel);
+ content.append(icon, labelsContainer);
+ item.append(content);
- const mainLabel = document.createElement('div');
- mainLabel.className = 'folders-tabs-list-item-label';
- mainLabel.textContent = tabLabel;
+ if (tab.selected) {
+ item.setAttribute('selected', 'true');
+ }
- const secondaryLabel = document.createElement('div');
- secondaryLabel.className = 'tab-list-item-secondary-label';
- secondaryLabel.textContent = `${formatRelativeTime(tab.lastAccessed)} • ${tab.group.label}`;
+ item.setAttribute('data-label', `${tabLabel.toLowerCase()} ${tabURL.toLowerCase()}`);
- labelsContainer.append(mainLabel, secondaryLabel);
- content.append(icon, labelsContainer);
- item.append(content);
+ item.addEventListener('click', () => {
+ gBrowser.selectedTab = tab;
+ });
- if (tab.selected) {
- item.setAttribute('selected', 'true');
+ item.addEventListener('mouseenter', () => {
+ for (const sibling of tabsList.children) {
+ sibling.removeAttribute('selected');
}
+ item.setAttribute('selected', 'true');
+ });
- item.setAttribute('data-label', `${tabLabel.toLowerCase()} ${tabURL.toLowerCase()}`);
-
- item.addEventListener('click', () => {
- gBrowser.selectedTab = tab;
- });
-
- item.addEventListener('mouseenter', () => {
- for (const sibling of tabsList.children) {
- sibling.removeAttribute('selected');
- }
- item.setAttribute('selected', 'true');
- });
-
- tabsList.appendChild(item);
- }
+ tabsList.appendChild(item);
}
+ }
- updateFolderIcon(group, state = 'auto') {
- const svg = group.querySelector('svg');
- if (!svg) return [];
+ updateFolderIcon(group, state = 'auto') {
+ const svg = group.querySelector('svg');
+ if (!svg) return [];
- const isCollapsed = group.collapsed;
- svg.setAttribute('state', state === 'auto' ? (isCollapsed ? 'close' : 'open') : state);
- const hasActive = group.hasAttribute('has-active');
- svg.setAttribute('active', hasActive && isCollapsed ? 'true' : 'false');
+ const isCollapsed = group.collapsed;
+ svg.setAttribute('state', state === 'auto' ? (isCollapsed ? 'close' : 'open') : state);
+ const hasActive = group.hasAttribute('has-active');
+ svg.setAttribute('active', hasActive && isCollapsed ? 'true' : 'false');
- return [];
- }
+ return [];
+ }
- setFolderIndentation(tabs, groupElem = undefined, forCollapse = true, animate = true) {
- if (!gZenPinnedTabManager.expandedSidebarMode) {
- return;
- }
- 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
- }
- if (
- gBrowser.isTab(groupElem) &&
- (!(groupElem.hasAttribute('zen-empty-tab') && groupElem.group === tab.group) ||
- groupElem?.hasAttribute('zen-empty-tab'))
- ) {
- groupElem = groupElem.group;
- isTab = true;
- }
- if (!isTab && !groupElem?.hasAttribute('selected') && !forCollapse) {
- groupElem = null; // Don't indent if the group is not selected
- }
- let level = groupElem?.level + 1 || 0;
- if (gBrowser.isTabGroupLabel(groupElem)) {
- // If it is a group label, we should not increase its level by one.
- level = groupElem.group.level;
- }
- const baseSpacing = 14; // Base spacing for each level
- let tabToAnimate = tab;
- if (gBrowser.isTabGroupLabel(tab)) {
- tabToAnimate = tab.group;
+ setFolderIndentation(tabs, groupElem = undefined, forCollapse = true, animate = true) {
+ if (!gZenPinnedTabManager.expandedSidebarMode) {
+ return;
+ }
+ 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
+ }
+ if (
+ gBrowser.isTab(groupElem) &&
+ (!(groupElem.hasAttribute('zen-empty-tab') && groupElem.group === tab.group) ||
+ groupElem?.hasAttribute('zen-empty-tab'))
+ ) {
+ groupElem = groupElem.group;
+ isTab = true;
+ }
+ if (!isTab && !groupElem?.hasAttribute('selected') && !forCollapse) {
+ groupElem = null; // Don't indent if the group is not selected
+ }
+ let level = groupElem?.level + 1 || 0;
+ if (gBrowser.isTabGroupLabel(groupElem)) {
+ // If it is a group label, we should not increase its level by one.
+ level = groupElem.group.level;
+ }
+ const baseSpacing = 14; // Base spacing for each level
+ let tabToAnimate = tab;
+ if (gBrowser.isTabGroupLabel(tab)) {
+ tabToAnimate = tab.group;
+ }
+ const tabLevel = tabToAnimate?.group?.level || 0;
+ const spacing = (level - tabLevel) * baseSpacing;
+ if (!animate) {
+ for (const tab of tabs) {
+ tab.style.setProperty('transition', 'none', 'important');
}
- const tabLevel = tabToAnimate?.group?.level || 0;
- const spacing = (level - tabLevel) * baseSpacing;
- if (!animate) {
- for (const tab of tabs) {
- tab.style.setProperty('transition', 'none', 'important');
- }
+ }
+ for (const tab of tabs) {
+ if (gBrowser.isTabGroupLabel(tab) || tab.group?.hasAttribute('split-view-group')) {
+ tab.group.style.setProperty('--zen-folder-indent', `${spacing}px`);
+ continue;
}
+ tab.style.setProperty('--zen-folder-indent', `${spacing}px`);
+ }
+ if (!animate) {
for (const tab of tabs) {
- if (gBrowser.isTabGroupLabel(tab) || tab.group?.hasAttribute('split-view-group')) {
- tab.group.style.setProperty('--zen-folder-indent', `${spacing}px`);
- continue;
- }
- tab.style.setProperty('--zen-folder-indent', `${spacing}px`);
- }
- if (!animate) {
- for (const tab of tabs) {
- tab.style.removeProperty('transition');
- }
+ tab.style.removeProperty('transition');
}
}
+ }
- changeFolderUserIcon(group) {
- if (!group) return;
+ changeFolderUserIcon(group) {
+ if (!group) return;
+
+ gZenEmojiPicker
+ .open(group.icon, { onlySvgIcons: true })
+ .then((icon) => {
+ this.setFolderUserIcon(group, icon);
+ group.dispatchEvent(new CustomEvent('ZenFolderIconChanged', { bubbles: true }));
+ })
+ .catch((err) => {
+ console.error(err);
+ return;
+ });
+ }
- gZenEmojiPicker
- .open(group.icon, { onlySvgIcons: true })
- .then((icon) => {
- this.setFolderUserIcon(group, icon);
- group.dispatchEvent(new CustomEvent('ZenFolderIconChanged', { bubbles: true }));
- })
- .catch((err) => {
- console.error(err);
- return;
- });
+ setFolderUserIcon(group, icon) {
+ const svgIcon = group.icon.querySelector('svg .icon image');
+ if (!svgIcon) return;
+ svgIcon.setAttribute('href', icon ?? '');
+ if (svgIcon.getAttribute('href') !== icon) {
+ svgIcon.style.opacity = '0';
+ } else {
+ svgIcon.style.opacity = '1';
}
+ }
- setFolderUserIcon(group, icon) {
- const svgIcon = group.icon.querySelector('svg .icon image');
- if (!svgIcon) return;
- svgIcon.setAttribute('href', icon ?? '');
- if (svgIcon.getAttribute('href') !== icon) {
- svgIcon.style.opacity = '0';
- } else {
- svgIcon.style.opacity = '1';
- }
+ #groupInit(group, stateData) {
+ // Setup zen-folder icon to the correct position
+ this.updateFolderIcon(group, 'auto');
+ if (stateData?.userIcon) {
+ this.setFolderUserIcon(group, stateData.userIcon);
}
- #groupInit(group, stateData) {
- // Setup zen-folder icon to the correct position
- this.updateFolderIcon(group, 'auto');
- if (stateData?.userIcon) {
- this.setFolderUserIcon(group, stateData.userIcon);
- }
+ if (group.collapsed) {
+ this.on_TabGroupCollapse({ target: group });
+ }
- if (group.collapsed) {
- this.on_TabGroupCollapse({ target: group });
+ const labelContainer = group.querySelector('.tab-group-label-container');
+ // Setup mouseenter/mouseleave events for the folder
+ labelContainer.addEventListener('mouseenter', (event) => {
+ if (
+ !group.collapsed ||
+ !Services.prefs.getBoolPref('zen.folders.search.enabled') ||
+ gBrowser.tabContainer.hasAttribute('movingtab')
+ ) {
+ return;
}
+ this.#mouseTimer = setTimeout(() => {
+ this.openTabsPopup(event);
+ }, Services.prefs.getIntPref('zen.folders.search.hover-delay'));
+ });
+ labelContainer.addEventListener('mouseleave', () => {
+ clearTimeout(this.#mouseTimer);
+ if (!group.collapsed) return;
+ this.#mouseTimer = setTimeout(() => {
+ // If popup is focused don't hide it
+ if (this.#popup.matches(':hover')) return;
+ this.#popup.hidePopup();
+ }, 200);
+ });
+ }
- const labelContainer = group.querySelector('.tab-group-label-container');
- // Setup mouseenter/mouseleave events for the folder
- labelContainer.addEventListener('mouseenter', (event) => {
- if (
- !group.collapsed ||
- !Services.prefs.getBoolPref('zen.folders.search.enabled') ||
- gBrowser.tabContainer.hasAttribute('movingtab')
- ) {
- return;
+ storeDataForSessionStore() {
+ const folders = Array.from(gBrowser.tabContainer.querySelectorAll('zen-folder'));
+ const splitGroups = Array.from(
+ gBrowser.tabContainer.querySelectorAll('tab-group[split-view-group]')
+ );
+ const allData = [...folders, ...splitGroups];
+
+ // Sort elements in the order in which they appear in the DOM
+ allData.sort((a, b) => {
+ const position = a.compareDocumentPosition(b);
+ if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
+ if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
+ return 0;
+ });
+
+ const storedData = [];
+
+ for (const folder of allData) {
+ const parentFolder = folder.parentElement.closest('zen-folder');
+ // Skip split-view-group if it's not a zen-folder child
+ if (!parentFolder && folder.hasAttribute('split-view-group')) continue;
+ const emptyFolderTabs = folder.tabs
+ .filter((tab) => tab.hasAttribute('zen-empty-tab'))
+ .map((tab) => tab.getAttribute('zen-pin-id'));
+
+ let prevSiblingInfo = null;
+ const prevSibling = folder.previousElementSibling;
+ const userIcon = folder?.icon?.querySelector('svg .icon image');
+
+ if (prevSibling) {
+ if (gBrowser.isTabGroup(prevSibling)) {
+ prevSiblingInfo = { type: 'group', id: prevSibling.id };
+ } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-pin-id')) {
+ const zenPinId = prevSibling.getAttribute('zen-pin-id');
+ prevSiblingInfo = { type: 'tab', id: zenPinId };
+ } else {
+ prevSiblingInfo = { type: 'start', id: null };
}
- this.#mouseTimer = setTimeout(() => {
- this.openTabsPopup(event);
- }, Services.prefs.getIntPref('zen.folders.search.hover-delay'));
- });
- labelContainer.addEventListener('mouseleave', () => {
- clearTimeout(this.#mouseTimer);
- if (!group.collapsed) return;
- this.#mouseTimer = setTimeout(() => {
- // If popup is focused don't hide it
- if (this.#popup.matches(':hover')) return;
- this.#popup.hidePopup();
- }, 200);
- });
- }
+ }
- storeDataForSessionStore() {
- const folders = Array.from(gBrowser.tabContainer.querySelectorAll('zen-folder'));
- const splitGroups = Array.from(
- gBrowser.tabContainer.querySelectorAll('tab-group[split-view-group]')
- );
- const allData = [...folders, ...splitGroups];
-
- // Sort elements in the order in which they appear in the DOM
- allData.sort((a, b) => {
- const position = a.compareDocumentPosition(b);
- if (position & Node.DOCUMENT_POSITION_FOLLOWING) return -1;
- if (position & Node.DOCUMENT_POSITION_PRECEDING) return 1;
- return 0;
+ storedData.push({
+ pinned: folder.pinned,
+ essential: folder.essential,
+ splitViewGroup: folder.hasAttribute('split-view-group'),
+ id: folder.id,
+ name: folder.label,
+ collapsed: folder.collapsed,
+ saveOnWindowClose: folder.saveOnWindowClose,
+ parentId: parentFolder ? parentFolder.id : null,
+ prevSiblingInfo: prevSiblingInfo,
+ emptyTabIds: emptyFolderTabs,
+ userIcon: userIcon?.getAttribute('href'),
+ pinId: folder.getAttribute('zen-pin-id'),
+ // note: We shouldn't be using the workspace-id anywhere, we are just
+ // remembering it for the pinned tabs manager to use it later.
+ workspaceId: folder.getAttribute('zen-workspace-id'),
});
-
- const storedData = [];
-
- for (const folder of allData) {
- const parentFolder = folder.parentElement.closest('zen-folder');
- // Skip split-view-group if it's not a zen-folder child
- if (!parentFolder && folder.hasAttribute('split-view-group')) continue;
- const emptyFolderTabs = folder.tabs
- .filter((tab) => tab.hasAttribute('zen-empty-tab'))
- .map((tab) => tab.getAttribute('zen-pin-id'));
-
- let prevSiblingInfo = null;
- const prevSibling = folder.previousElementSibling;
- const userIcon = folder?.icon?.querySelector('svg .icon image');
-
- if (prevSibling) {
- if (gBrowser.isTabGroup(prevSibling)) {
- prevSiblingInfo = { type: 'group', id: prevSibling.id };
- } else if (gBrowser.isTab(prevSibling) && prevSibling.hasAttribute('zen-pin-id')) {
- const zenPinId = prevSibling.getAttribute('zen-pin-id');
- prevSiblingInfo = { type: 'tab', id: zenPinId };
- } else {
- prevSiblingInfo = { type: 'start', id: null };
- }
- }
-
- storedData.push({
- pinned: folder.pinned,
- essential: folder.essential,
- splitViewGroup: folder.hasAttribute('split-view-group'),
- id: folder.id,
- name: folder.label,
- collapsed: folder.collapsed,
- saveOnWindowClose: folder.saveOnWindowClose,
- parentId: parentFolder ? parentFolder.id : null,
- prevSiblingInfo: prevSiblingInfo,
- emptyTabIds: emptyFolderTabs,
- userIcon: userIcon?.getAttribute('href'),
- pinId: folder.getAttribute('zen-pin-id'),
- // note: We shouldn't be using the workspace-id anywhere, we are just
- // remembering it for the pinned tabs manager to use it later.
- workspaceId: folder.getAttribute('zen-workspace-id'),
- });
- }
- return storedData;
}
+ return storedData;
+ }
- restoreDataFromSessionStore(data) {
- if (!data || this._sessionRestoring) {
- return;
- }
+ restoreDataFromSessionStore(data) {
+ if (!data || this._sessionRestoring) {
+ return;
+ }
- this._sessionRestoring = true;
+ this._sessionRestoring = true;
- const tabFolderWorkingData = new Map();
+ const tabFolderWorkingData = new Map();
- for (const folderData of data) {
- const workingData = {
- stateData: folderData,
- node: null,
- containingTabsFragment: document.createDocumentFragment(),
- };
- tabFolderWorkingData.set(folderData.id, workingData);
+ for (const folderData of data) {
+ const workingData = {
+ stateData: folderData,
+ node: null,
+ containingTabsFragment: document.createDocumentFragment(),
+ };
+ tabFolderWorkingData.set(folderData.id, workingData);
- const oldGroup = document.getElementById(folderData.id);
- folderData.emptyTabIds.forEach((zenPinId) => {
- oldGroup
- ?.querySelector(`tab[zen-pin-id="${zenPinId}"]`)
- ?.setAttribute('zen-empty-tab', true);
- });
- if (oldGroup) {
- if (!folderData.splitViewGroup) {
- const folder = this._createFolderNode({
- id: folderData.id,
- label: folderData.name,
- collapsed: folderData.collapsed,
- pinned: folderData.pinned,
- saveOnWindowClose: folderData.saveOnWindowClose,
- workspaceId: folderData.workspaceId,
- });
- folder.setAttribute('zen-pin-id', folderData.pinId);
- workingData.node = folder;
- oldGroup.before(folder);
- } else {
- workingData.node = oldGroup;
- }
- while (oldGroup.tabs.length > 0) {
- const tab = oldGroup.tabs[0];
- if (folderData.workspaceId) {
- tab.setAttribute('zen-workspace-id', folderData.workspaceId);
- }
- workingData.containingTabsFragment.appendChild(tab);
- }
- if (!folderData.splitViewGroup) {
- oldGroup.remove();
+ const oldGroup = document.getElementById(folderData.id);
+ folderData.emptyTabIds.forEach((zenPinId) => {
+ oldGroup
+ ?.querySelector(`tab[zen-pin-id="${zenPinId}"]`)
+ ?.setAttribute('zen-empty-tab', true);
+ });
+ if (oldGroup) {
+ if (!folderData.splitViewGroup) {
+ const folder = this._createFolderNode({
+ id: folderData.id,
+ label: folderData.name,
+ collapsed: folderData.collapsed,
+ pinned: folderData.pinned,
+ saveOnWindowClose: folderData.saveOnWindowClose,
+ workspaceId: folderData.workspaceId,
+ });
+ folder.setAttribute('zen-pin-id', folderData.pinId);
+ workingData.node = folder;
+ oldGroup.before(folder);
+ } else {
+ workingData.node = oldGroup;
+ }
+ while (oldGroup.tabs.length > 0) {
+ const tab = oldGroup.tabs[0];
+ if (folderData.workspaceId) {
+ tab.setAttribute('zen-workspace-id', folderData.workspaceId);
}
+ workingData.containingTabsFragment.appendChild(tab);
+ }
+ if (!folderData.splitViewGroup) {
+ oldGroup.remove();
}
}
+ }
- for (const { node, containingTabsFragment } of tabFolderWorkingData.values()) {
- if (node) {
- node.appendChild(containingTabsFragment);
- }
+ for (const { node, containingTabsFragment } of tabFolderWorkingData.values()) {
+ if (node) {
+ node.appendChild(containingTabsFragment);
}
+ }
- // Nesting folders into each other according to parentId.
- for (const { stateData, node } of tabFolderWorkingData.values()) {
- if (node && stateData.parentId) {
- const parentWorkingData = tabFolderWorkingData.get(stateData.parentId);
- if (parentWorkingData && parentWorkingData.node) {
- switch (stateData?.prevSiblingInfo?.type) {
- case 'tab': {
- const tab = parentWorkingData.node.querySelector(
- `[zen-pin-id="${stateData.prevSiblingInfo.id}"]`
- );
- tab.after(node);
+ // Nesting folders into each other according to parentId.
+ for (const { stateData, node } of tabFolderWorkingData.values()) {
+ if (node && stateData.parentId) {
+ const parentWorkingData = tabFolderWorkingData.get(stateData.parentId);
+ if (parentWorkingData && parentWorkingData.node) {
+ switch (stateData?.prevSiblingInfo?.type) {
+ case 'tab': {
+ const tab = parentWorkingData.node.querySelector(
+ `[zen-pin-id="${stateData.prevSiblingInfo.id}"]`
+ );
+ tab.after(node);
+ break;
+ }
+ case 'group': {
+ const folder = document.getElementById(stateData.prevSiblingInfo.id);
+ if (folder) {
+ folder.after(node);
break;
}
- case 'group': {
- const folder = document.getElementById(stateData.prevSiblingInfo.id);
- if (folder) {
- folder.after(node);
- break;
- }
- // If we didn't find the group, we should debug it and continue to default case.
- console.warn(
- `Zen Folders: Could not find previous sibling group with id ${stateData.prevSiblingInfo.id} while restoring session.`
- );
- // @eslint-disable-next-line no-fallthrough
- }
- default: {
- // Should insert after zen-empty-tab
- const start =
- parentWorkingData.node.querySelector('.zen-tab-group-start').nextElementSibling;
- start.after(node);
- }
+ // If we didn't find the group, we should debug it and continue to default case.
+ console.warn(
+ `Zen Folders: Could not find previous sibling group with id ${stateData.prevSiblingInfo.id} while restoring session.`
+ );
+ // @eslint-disable-next-line no-fallthrough
+ }
+ default: {
+ // Should insert after zen-empty-tab
+ const start =
+ parentWorkingData.node.querySelector('.zen-tab-group-start').nextElementSibling;
+ start.after(node);
}
}
}
}
+ }
- // Initialize UI state for all folders.
- for (const { stateData, node } of tabFolderWorkingData.values()) {
- if (node && !stateData.splitViewGroup) {
- this.#groupInit(node, stateData);
- }
- }
-
- gBrowser.tabContainer._invalidateCachedTabs();
- this._sessionRestoring = false;
- }
-
- /**
- * Highlights the given tab group and removes highlight from any previously highlighted group.
- * @param {MozTabbrowserTabGroup|undefined|null} folder The folder to highlight, or null to clear highlight.
- * @param {Array|null} movingTabs The tabs being moved.
- */
- highlightGroupOnDragOver(folder, movingTabs) {
- if (folder === this.#lastHighlightedGroup) return;
- const tab = movingTabs ? movingTabs[0] : null;
- if (this.#lastHighlightedGroup && this.#lastHighlightedGroup !== folder) {
- this.#lastHighlightedGroup.removeAttribute('selected');
- if (this.#lastHighlightedGroup.collapsed) {
- this.updateFolderIcon(this.#lastHighlightedGroup, 'close');
- }
- this.#lastHighlightedGroup = null;
- }
-
- if (
- folder &&
- (!folder.hasAttribute('split-view-group') || !folder.hasAttribute('selected')) &&
- folder !== tab?.group &&
- !(
- folder.level >= this.#ZEN_MAX_SUBFOLDERS &&
- movingTabs?.some((t) => gBrowser.isTabGroupLabel(t))
- )
- ) {
- folder.setAttribute('selected', 'true');
- folder.style.transform = '';
- if (folder.collapsed) {
- this.updateFolderIcon(folder, 'open');
- }
- this.#lastHighlightedGroup = folder;
+ // Initialize UI state for all folders.
+ for (const { stateData, node } of tabFolderWorkingData.values()) {
+ if (node && !stateData.splitViewGroup) {
+ this.#groupInit(node, stateData);
}
}
- /**
- * Ungroup a tab from all the active groups it belongs to.
- * @param {MozTabbrowserTab[]} tabs The tab to ungroup.
- */
- ungroupTabsFromActiveGroups(tabs) {
- for (const tab of tabs) {
- gBrowser.ungroupTabsUntilNoActive(tab);
- }
+ gBrowser.tabContainer._invalidateCachedTabs();
+ this._sessionRestoring = false;
+ }
+
+ /**
+ * Highlights the given tab group and removes highlight from any previously highlighted group.
+ * @param {MozTabbrowserTabGroup|undefined|null} folder The folder to highlight, or null to clear highlight.
+ * @param {Array|null} movingTabs The tabs being moved.
+ */
+ highlightGroupOnDragOver(folder, movingTabs) {
+ if (folder === this.#lastHighlightedGroup) return;
+ const tab = movingTabs ? movingTabs[0] : null;
+ if (this.#lastHighlightedGroup && this.#lastHighlightedGroup !== folder) {
+ this.#lastHighlightedGroup.removeAttribute('selected');
+ if (this.#lastHighlightedGroup.collapsed) {
+ this.updateFolderIcon(this.#lastHighlightedGroup, 'close');
+ }
+ this.#lastHighlightedGroup = null;
}
- /**
- * Handles the dragover logic when dragging a tab or tab group label over another tab group label.
- * This function determines where the dragged item should be visually dropped (before/after the group, or inside it)
- * and updates related styling and highlighting.
- *
- * @param {MozTabbrowserTabGroupLabel} currentDropElement The tab group label currently being dragged over.
- * @param {MozTabbrowserTab|MozTabbrowserTabGroupLabel} draggedTab The tab or tab group label being dragged.
- * @param {number} overlapPercent The percentage of overlap between the dragged item and the drop target.
- * @param {Array} movingTabs An array of tabs that are currently being dragged together.
- * @param {boolean} currentDropBefore Indicates if the current drop position is before the middle of the drop element.
- * @param {string|undefined} currentColorCode The current color code for dragover highlighting.
- * @returns {{dropElement: MozTabbrowserTabGroup|MozTabbrowserTab|MozTabbrowserTabGroupLabel, colorCode: string|undefined, dropBefore: boolean}}
- * An object containing the updated drop element, color code for highlighting, and drop position.
- */
- handleDragOverTabGroupLabel(
- currentDropElement,
- draggedTab,
- overlapPercent,
- movingTabs,
- currentDropBefore,
- currentColorCode
+ if (
+ folder &&
+ (!folder.hasAttribute('split-view-group') || !folder.hasAttribute('selected')) &&
+ folder !== tab?.group &&
+ !(
+ folder.level >= this.#ZEN_MAX_SUBFOLDERS &&
+ movingTabs?.some((t) => gBrowser.isTabGroupLabel(t))
+ )
) {
- let dropElement = currentDropElement;
- let dropBefore = currentDropBefore;
- let colorCode = currentColorCode;
-
- const dropElementGroup = dropElement?.isZenFolder ? dropElement : dropElement?.group;
- const isSplitGroup = dropElement?.group?.hasAttribute('split-view-group');
- let firstGroupElem =
- dropElementGroup.querySelector('.zen-tab-group-start').nextElementSibling;
- if (gBrowser.isTabGroup(firstGroupElem)) firstGroupElem = firstGroupElem.labelElement;
-
- const isInMiddleZone =
- overlapPercent >= this.#ZEN_EDGE_ZONE_THRESHOLD &&
- overlapPercent <= 1 - this.#ZEN_EDGE_ZONE_THRESHOLD;
- const shouldDropInside = isInMiddleZone && !isSplitGroup;
-
- if (shouldDropInside) {
- dropElement = firstGroupElem;
- dropBefore = true;
- this.highlightGroupOnDragOver(dropElementGroup, movingTabs);
- } else {
- colorCode = undefined;
- this.highlightGroupOnDragOver(null);
+ folder.setAttribute('selected', 'true');
+ folder.style.transform = '';
+ if (folder.collapsed) {
+ this.updateFolderIcon(folder, 'open');
}
+ this.#lastHighlightedGroup = folder;
+ }
+ }
- return { dropElement, colorCode, dropBefore };
+ /**
+ * Ungroup a tab from all the active groups it belongs to.
+ * @param {MozTabbrowserTab[]} tabs The tab to ungroup.
+ */
+ ungroupTabsFromActiveGroups(tabs) {
+ for (const tab of tabs) {
+ gBrowser.ungroupTabsUntilNoActive(tab);
}
+ }
- #normalizeGroupItems(items) {
- return items
- .filter((item) => !item.hasAttribute('zen-empty-tab'))
- .map((item) => {
- if (gBrowser.isTabGroup(item)) {
- item = item.firstChild;
- } else if (gBrowser.isTabGroupLabel(item)) {
- if (item?.group?.hasAttribute('split-view-group')) {
- item = item.group;
- } else {
- item = item.parentElement;
- }
- }
- return item;
- });
+ /**
+ * Handles the dragover logic when dragging a tab or tab group label over another tab group label.
+ * This function determines where the dragged item should be visually dropped (before/after the group, or inside it)
+ * and updates related styling and highlighting.
+ *
+ * @param {MozTabbrowserTabGroupLabel} currentDropElement The tab group label currently being dragged over.
+ * @param {MozTabbrowserTab|MozTabbrowserTabGroupLabel} draggedTab The tab or tab group label being dragged.
+ * @param {number} overlapPercent The percentage of overlap between the dragged item and the drop target.
+ * @param {Array} movingTabs An array of tabs that are currently being dragged together.
+ * @param {boolean} currentDropBefore Indicates if the current drop position is before the middle of the drop element.
+ * @param {string|undefined} currentColorCode The current color code for dragover highlighting.
+ * @returns {{dropElement: MozTabbrowserTabGroup|MozTabbrowserTab|MozTabbrowserTabGroupLabel, colorCode: string|undefined, dropBefore: boolean}}
+ * An object containing the updated drop element, color code for highlighting, and drop position.
+ */
+ handleDragOverTabGroupLabel(
+ currentDropElement,
+ draggedTab,
+ overlapPercent,
+ movingTabs,
+ currentDropBefore,
+ currentColorCode
+ ) {
+ let dropElement = currentDropElement;
+ let dropBefore = currentDropBefore;
+ let colorCode = currentColorCode;
+
+ const dropElementGroup = dropElement?.isZenFolder ? dropElement : dropElement?.group;
+ const isSplitGroup = dropElement?.group?.hasAttribute('split-view-group');
+ let firstGroupElem = dropElementGroup.querySelector('.zen-tab-group-start').nextElementSibling;
+ if (gBrowser.isTabGroup(firstGroupElem)) firstGroupElem = firstGroupElem.labelElement;
+
+ const isInMiddleZone =
+ overlapPercent >= this.#ZEN_EDGE_ZONE_THRESHOLD &&
+ overlapPercent <= 1 - this.#ZEN_EDGE_ZONE_THRESHOLD;
+ const shouldDropInside = isInMiddleZone && !isSplitGroup;
+
+ if (shouldDropInside) {
+ dropElement = firstGroupElem;
+ dropBefore = true;
+ this.highlightGroupOnDragOver(dropElementGroup, movingTabs);
+ } else {
+ colorCode = undefined;
+ this.highlightGroupOnDragOver(null);
}
- #collectGroupItems(group, opts = {}) {
- const { selectedTabs = [], splitViewIds = new Set(), activeFoldersIds = new Set() } = opts;
- const folders = new Map();
- return group.childGroupsAndTabs
- .filter((item) => !item.hasAttribute('zen-empty-tab'))
- .map((item) => {
- const isSplitView = item.group?.hasAttribute?.('split-view-group');
- const group = isSplitView ? item.group.group : item.group;
- if (!folders.has(group?.id)) {
- folders.set(group?.id, group?.activeGroups[0]);
- }
- const lastActiveFolder = folders.get(group?.id);
- const activeFolderId = lastActiveFolder?.id;
- const splitViewId = isSplitView ? item?.group?.id : null;
-
- if (item.multiselected || item.selected || item.hasAttribute('folder-active')) {
- selectedTabs.push(item);
- if (splitViewId) splitViewIds.add(splitViewId);
- if (activeFolderId) activeFoldersIds.add(activeFolderId);
+ return { dropElement, colorCode, dropBefore };
+ }
+
+ #normalizeGroupItems(items) {
+ return items
+ .filter((item) => !item.hasAttribute('zen-empty-tab'))
+ .map((item) => {
+ if (gBrowser.isTabGroup(item)) {
+ item = item.firstChild;
+ } else if (gBrowser.isTabGroupLabel(item)) {
+ if (item?.group?.hasAttribute('split-view-group')) {
+ item = item.group;
+ } else {
+ item = item.parentElement;
}
+ }
+ return item;
+ });
+ }
- if (gBrowser.isTabGroupLabel(item)) {
- if (isSplitView) {
- item = item.group;
- } else {
- item = item.parentElement;
- }
+ #collectGroupItems(group, opts = {}) {
+ const { selectedTabs = [], splitViewIds = new Set(), activeFoldersIds = new Set() } = opts;
+ const folders = new Map();
+ return group.childGroupsAndTabs
+ .filter((item) => !item.hasAttribute('zen-empty-tab'))
+ .map((item) => {
+ const isSplitView = item.group?.hasAttribute?.('split-view-group');
+ const group = isSplitView ? item.group.group : item.group;
+ if (!folders.has(group?.id)) {
+ folders.set(group?.id, group?.activeGroups[0]);
+ }
+ const lastActiveFolder = folders.get(group?.id);
+ const activeFolderId = lastActiveFolder?.id;
+ const splitViewId = isSplitView ? item?.group?.id : null;
+
+ if (item.multiselected || item.selected || item.hasAttribute('folder-active')) {
+ selectedTabs.push(item);
+ if (splitViewId) splitViewIds.add(splitViewId);
+ if (activeFolderId) activeFoldersIds.add(activeFolderId);
+ }
+
+ if (gBrowser.isTabGroupLabel(item)) {
+ if (isSplitView) {
+ item = item.group;
+ } else {
+ item = item.parentElement;
}
+ }
- return { item, splitViewId, activeFolderId };
- });
- }
+ return { item, splitViewId, activeFolderId };
+ });
+ }
- #createAnimation(items, targetState, opts, callback = () => {}) {
- items = Array.isArray(items) ? items : [items];
- return items.map((item) =>
- gZenUIManager.motion.animate(item, targetState, opts).then(callback)
- );
- }
+ #createAnimation(items, targetState, opts, callback = () => {}) {
+ items = Array.isArray(items) ? items : [items];
+ return items.map((item) =>
+ gZenUIManager.motion.animate(item, targetState, opts).then(callback)
+ );
+ }
- #calculateHeightShift(tabsContainer, selectedTabs) {
- let heightShift = 0;
- if (selectedTabs.length) {
- return heightShift;
- } else {
- heightShift += window.windowUtils.getBoundsWithoutFlushing(tabsContainer).height;
- }
+ #calculateHeightShift(tabsContainer, selectedTabs) {
+ let heightShift = 0;
+ if (selectedTabs.length) {
return heightShift;
+ } else {
+ heightShift += window.windowUtils.getBoundsWithoutFlushing(tabsContainer).height;
}
+ return heightShift;
+ }
- async animateCollapse(group) {
- this.cancelPopupTimer();
+ async animateCollapse(group) {
+ this.cancelPopupTimer();
- const animations = [];
- const selectedTabs = [];
- const splitViewIds = new Set();
- const activeFoldersIds = new Set();
- const itemsToHide = [];
+ const animations = [];
+ const selectedTabs = [];
+ const splitViewIds = new Set();
+ const activeFoldersIds = new Set();
+ const itemsToHide = [];
- const tabsContainer = group.querySelector('.tab-group-container');
- const groupStart = group.querySelector('.zen-tab-group-start');
+ const tabsContainer = group.querySelector('.tab-group-container');
+ const groupStart = group.querySelector('.zen-tab-group-start');
- const groupItems = this.#collectGroupItems(group, {
- selectedTabs,
- splitViewIds,
- activeFoldersIds,
- });
- const collapsedHeight = this.#calculateHeightShift(tabsContainer, selectedTabs);
+ const groupItems = this.#collectGroupItems(group, {
+ selectedTabs,
+ splitViewIds,
+ activeFoldersIds,
+ });
+ const collapsedHeight = this.#calculateHeightShift(tabsContainer, selectedTabs);
- if (selectedTabs.length) {
- for (let i = 0; i < groupItems.length; i++) {
- const { item, splitViewId, activeFolderId } = groupItems[i];
+ if (selectedTabs.length) {
+ for (let i = 0; i < groupItems.length; i++) {
+ const { item, splitViewId, activeFolderId } = groupItems[i];
- // Skip selected items
- if (selectedTabs.includes(item)) continue;
+ // Skip selected items
+ if (selectedTabs.includes(item)) continue;
- // Skip items from selected split-view groups
- if (splitViewId && splitViewIds.has(splitViewId)) continue;
+ // Skip items from selected split-view groups
+ if (splitViewId && splitViewIds.has(splitViewId)) continue;
- // Skip items from selected active groups
- if (activeFolderId && activeFoldersIds.has(activeFolderId)) {
- // If item is tab-group-label-container we should hide it.
- // Other items between tab-group-labe-container and folder-active tab should be visible cuz they are hidden by margin-top
- if (item.parentElement.id !== activeFolderId && !item.hasAttribute('folder-active')) {
- continue;
- }
- }
-
- if (!itemsToHide.includes(item)) {
- itemsToHide.push(item);
+ // Skip items from selected active groups
+ if (activeFolderId && activeFoldersIds.has(activeFolderId)) {
+ // If item is tab-group-label-container we should hide it.
+ // Other items between tab-group-labe-container and folder-active tab should be visible cuz they are hidden by margin-top
+ if (item.parentElement.id !== activeFolderId && !item.hasAttribute('folder-active')) {
+ continue;
}
}
- group.setAttribute('has-active', 'true');
- group.activeTabs = selectedTabs;
-
- selectedTabs.forEach((tab) => {
- this.setFolderIndentation([tab], group, /* for collapse = */ true);
- });
+ if (!itemsToHide.includes(item)) {
+ itemsToHide.push(item);
+ }
}
- animations.push(
- ...this.#createAnimation(
- itemsToHide,
- { opacity: [1, 0], height: ['auto', 0] },
- { duration: 0.12, ease: 'easeInOut' }
- ),
- ...this.updateFolderIcon(group),
- ...this.#createAnimation(
- groupStart,
- {
- marginTop: -(collapsedHeight + 4 * (selectedTabs.length === 0 ? 1 : 0)),
- },
- { duration: 0.12, ease: 'easeInOut' }
- )
- );
+ group.setAttribute('has-active', 'true');
+ group.activeTabs = selectedTabs;
- gBrowser.tabContainer._invalidateCachedVisibleTabs();
- this.#animationCount += 1;
- await Promise.all(animations);
- if (this.#animationCount) {
- this.#animationCount -= 1;
- return;
- }
- // Prevent hiding if we spam the group animations
- if (!selectedTabs.length && !this.#animationCount) {
- tabsContainer.setAttribute('hidden', true);
- }
+ selectedTabs.forEach((tab) => {
+ this.setFolderIndentation([tab], group, /* for collapse = */ true);
+ });
+ }
+
+ animations.push(
+ ...this.#createAnimation(
+ itemsToHide,
+ { opacity: [1, 0], height: ['auto', 0] },
+ { duration: 0.12, ease: 'easeInOut' }
+ ),
+ ...this.updateFolderIcon(group),
+ ...this.#createAnimation(
+ groupStart,
+ {
+ marginTop: -(collapsedHeight + 4 * (selectedTabs.length === 0 ? 1 : 0)),
+ },
+ { duration: 0.12, ease: 'easeInOut' }
+ )
+ );
- this.styleCleanup(itemsToHide);
+ gBrowser.tabContainer._invalidateCachedVisibleTabs();
+ this.#animationCount += 1;
+ await Promise.all(animations);
+ if (this.#animationCount) {
+ this.#animationCount -= 1;
+ return;
+ }
+ // Prevent hiding if we spam the group animations
+ if (!selectedTabs.length && !this.#animationCount) {
+ tabsContainer.setAttribute('hidden', true);
}
- async animateExpand(group) {
- this.cancelPopupTimer();
+ this.styleCleanup(itemsToHide);
+ }
- const animations = [];
- const itemsToHide = [];
+ async animateExpand(group) {
+ this.cancelPopupTimer();
- const tabsContainer = group.querySelector('.tab-group-container');
- tabsContainer.removeAttribute('hidden');
- tabsContainer.style.overflow = 'hidden';
+ const animations = [];
+ const itemsToHide = [];
- const groupStart = group.querySelector('.zen-tab-group-start');
- const itemsToShow = this.#normalizeGroupItems(group.childGroupsAndTabs);
- const activeFolders = group.childActiveGroups;
+ const tabsContainer = group.querySelector('.tab-group-container');
+ tabsContainer.removeAttribute('hidden');
+ tabsContainer.style.overflow = 'hidden';
- for (const folder of activeFolders) {
- const splitViewIds = new Set();
- const selectedTabs = folder.activeTabs;
+ const groupStart = group.querySelector('.zen-tab-group-start');
+ const itemsToShow = this.#normalizeGroupItems(group.childGroupsAndTabs);
+ const activeFolders = group.childActiveGroups;
- const activeFoldersIds = new Set();
- const activeFolderItems = this.#collectGroupItems(folder, {
- splitViewIds,
- activeFoldersIds,
- });
+ for (const folder of activeFolders) {
+ const splitViewIds = new Set();
+ const selectedTabs = folder.activeTabs;
- if (selectedTabs.length) {
- for (let i = 0; i < activeFolderItems.length; i++) {
- const { item, splitViewId, activeFolderId } = activeFolderItems[i];
+ const activeFoldersIds = new Set();
+ const activeFolderItems = this.#collectGroupItems(folder, {
+ splitViewIds,
+ activeFoldersIds,
+ });
- // Skip selected items
- if (selectedTabs.includes(item)) continue;
+ if (selectedTabs.length) {
+ for (let i = 0; i < activeFolderItems.length; i++) {
+ const { item, splitViewId, activeFolderId } = activeFolderItems[i];
- // Skip items from selected split-view groups
- if (splitViewId && splitViewIds.has(splitViewId)) continue;
+ // Skip selected items
+ if (selectedTabs.includes(item)) continue;
- if (activeFolderId && activeFoldersIds.has(activeFolderId)) {
- const folder = item.parentElement;
- if (
- gBrowser.isTabGroup(folder) &&
- folder.id !== activeFolderId &&
- item.hasAttribute('folder-active')
- ) {
- continue;
- }
- }
+ // Skip items from selected split-view groups
+ if (splitViewId && splitViewIds.has(splitViewId)) continue;
- if (!itemsToHide.includes(item)) {
- itemsToHide.push(item);
+ if (activeFolderId && activeFoldersIds.has(activeFolderId)) {
+ const folder = item.parentElement;
+ if (
+ gBrowser.isTabGroup(folder) &&
+ folder.id !== activeFolderId &&
+ item.hasAttribute('folder-active')
+ ) {
+ continue;
}
}
+
+ if (!itemsToHide.includes(item)) {
+ itemsToHide.push(item);
+ }
}
}
+ }
- const afterMarginTop = () => {
- tabsContainer.style.overflow = '';
- if (group.hasAttribute('has-active')) {
- const activeTabs = group.activeTabs;
- const folders = new Map();
- group.removeAttribute('has-active');
- for (let tab of activeTabs) {
- const group = tab?.group?.hasAttribute('split-view-group')
- ? tab?.group?.group
- : tab?.group;
- if (!folders.has(group?.id)) {
- folders.set(group?.id, group?.activeGroups?.at(-1));
- }
- let activeGroup = folders.get(group?.id);
- if (activeGroup) {
- this.setFolderIndentation([tab], activeGroup, /* for collapse = */ true);
+ const afterMarginTop = () => {
+ tabsContainer.style.overflow = '';
+ if (group.hasAttribute('has-active')) {
+ const activeTabs = group.activeTabs;
+ const folders = new Map();
+ group.removeAttribute('has-active');
+ for (let tab of activeTabs) {
+ const group = tab?.group?.hasAttribute('split-view-group')
+ ? tab?.group?.group
+ : tab?.group;
+ if (!folders.has(group?.id)) {
+ folders.set(group?.id, group?.activeGroups?.at(-1));
+ }
+ let activeGroup = folders.get(group?.id);
+ if (activeGroup) {
+ this.setFolderIndentation([tab], activeGroup, /* for collapse = */ true);
+ } else {
+ // Since the folder is now expanded, we should remove active attribute
+ // to the tab that was previously visible
+ tab.removeAttribute('folder-active');
+ if (tab.group?.hasAttribute('split-view-group')) {
+ tab.group.style.removeProperty('--zen-folder-indent');
} else {
- // 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');
- }
+ tab.style.removeProperty('--zen-folder-indent');
}
}
- folders.clear();
}
- // Folder has been expanded and has no active tabs
- group.activeTabs = [];
+ folders.clear();
+ }
+ // Folder has been expanded and has no active tabs
+ group.activeTabs = [];
+ };
+
+ animations.push(
+ ...this.#createAnimation(
+ itemsToShow,
+ { opacity: '', height: '' },
+ { duration: 0.12, ease: 'easeInOut' }
+ ),
+ ...this.#createAnimation(
+ itemsToHide,
+ { opacity: 0, height: 0 },
+ { duration: 0.12, ease: 'easeInOut' }
+ ),
+ ...this.updateFolderIcon(group),
+ ...this.#createAnimation(
+ groupStart,
+ {
+ marginTop: 0,
+ },
+ { duration: 0.12, ease: 'easeInOut' },
+ afterMarginTop
+ )
+ );
+
+ this.#animationCount += 1;
+ await Promise.all(animations);
+ this.#animationCount -= 1;
+
+ // Cleanup
+ this.styleCleanup(itemsToShow);
+ this.styleCleanup(itemsToHide);
+ }
+
+ async animateUnloadAll(group) {
+ const animations = [];
+
+ const activeGroups = [group, ...group.childActiveGroups];
+ for (const folder of activeGroups) {
+ folder.removeAttribute('has-active');
+ folder.activeTabs = [];
+ const groupItems = this.#normalizeGroupItems(folder.allItems);
+ const tabsContainer = folder.querySelector('.tab-group-container');
+
+ // Set correct margin-top after animation
+ const afterAnimate = () => {
+ groupStart.style.removeProperty('margin-top');
+ this.styleCleanup(groupItems);
+ // Trigger the recalculation so that zen returns
+ // the correct container size in the DOM
+ tabsContainer.offsetHeight;
+ tabsContainer.setAttribute('hidden', true);
+ const collapsedHeight = this.#calculateHeightShift(tabsContainer, []);
+ groupStart.style.marginTop = `${-(collapsedHeight + 4)}px`;
};
+ const groupStart = folder.querySelector('.zen-tab-group-start');
+ const collapsedHeight = this.#calculateHeightShift(tabsContainer, []);
+
+ // Collect animations for this specific folder becoming inactive
animations.push(
- ...this.#createAnimation(
- itemsToShow,
- { opacity: '', height: '' },
- { duration: 0.12, ease: 'easeInOut' }
- ),
- ...this.#createAnimation(
- itemsToHide,
- { opacity: 0, height: 0 },
- { duration: 0.12, ease: 'easeInOut' }
- ),
- ...this.updateFolderIcon(group),
+ ...this.updateFolderIcon(folder, 'close', false),
...this.#createAnimation(
groupStart,
{
- marginTop: 0,
+ marginTop: -(collapsedHeight + 4),
},
{ duration: 0.12, ease: 'easeInOut' },
- afterMarginTop
+ afterAnimate
)
);
-
- this.#animationCount += 1;
- await Promise.all(animations);
- this.#animationCount -= 1;
-
- // Cleanup
- this.styleCleanup(itemsToShow);
- this.styleCleanup(itemsToHide);
- }
-
- async animateUnloadAll(group) {
- const animations = [];
-
- const activeGroups = [group, ...group.childActiveGroups];
- for (const folder of activeGroups) {
- folder.removeAttribute('has-active');
- folder.activeTabs = [];
- const groupItems = this.#normalizeGroupItems(folder.allItems);
- const tabsContainer = folder.querySelector('.tab-group-container');
-
- // Set correct margin-top after animation
- const afterAnimate = () => {
- groupStart.style.removeProperty('margin-top');
- this.styleCleanup(groupItems);
- // Trigger the recalculation so that zen returns
- // the correct container size in the DOM
- tabsContainer.offsetHeight;
- tabsContainer.setAttribute('hidden', true);
- const collapsedHeight = this.#calculateHeightShift(tabsContainer, []);
- groupStart.style.marginTop = `${-(collapsedHeight + 4)}px`;
- };
-
- const groupStart = folder.querySelector('.zen-tab-group-start');
- const collapsedHeight = this.#calculateHeightShift(tabsContainer, []);
-
- // Collect animations for this specific folder becoming inactive
- animations.push(
- ...this.updateFolderIcon(folder, 'close', false),
- ...this.#createAnimation(
- groupStart,
- {
- marginTop: -(collapsedHeight + 4),
- },
- { duration: 0.12, ease: 'easeInOut' },
- afterAnimate
- )
- );
- }
-
- this.#animationCount += 1;
- await Promise.all(animations);
- this.#animationCount -= 1;
- gBrowser.tabContainer._invalidateCachedTabs();
}
- async animateUnload(group, tabToUnload, ungroup = false) {
- const isSplitView = tabToUnload.group?.hasAttribute('split-view-group');
- if ((!group?.isZenFolder || !isSplitView) && !tabToUnload.hasAttribute('folder-active'))
- return;
- const animations = [];
- let lastTab = false;
-
- const activeGroups = group.activeGroups;
- for (const folder of activeGroups) {
- folder.activeTabs = folder.activeTabs.filter((tab) => tab !== tabToUnload);
-
- if (folder.activeTabs.length === 0) {
- lastTab = true;
- animations.push(async () => {
- folder.removeAttribute('has-active');
- const groupItems = this.#normalizeGroupItems(folder.allItems);
- const tabsContainer = folder.querySelector('.tab-group-container');
-
- // Set correct margin-top after animation
- const afterAnimate = () => {
- groupStart.style.removeProperty('margin-top');
- this.styleCleanup(groupItems);
- // Trigger the recalculation so that zen returns
- // the correct container size in the DOM
- tabsContainer.offsetHeight;
- tabsContainer.setAttribute('hidden', true);
- const collapsedHeight = this.#calculateHeightShift(tabsContainer, []);
- groupStart.style.marginTop = `${-(collapsedHeight + 4)}px`;
- };
+ this.#animationCount += 1;
+ await Promise.all(animations);
+ this.#animationCount -= 1;
+ gBrowser.tabContainer._invalidateCachedTabs();
+ }
- const groupStart = folder.querySelector('.zen-tab-group-start');
+ async animateUnload(group, tabToUnload, ungroup = false) {
+ const isSplitView = tabToUnload.group?.hasAttribute('split-view-group');
+ if ((!group?.isZenFolder || !isSplitView) && !tabToUnload.hasAttribute('folder-active')) return;
+ const animations = [];
+ let lastTab = false;
+
+ const activeGroups = group.activeGroups;
+ for (const folder of activeGroups) {
+ folder.activeTabs = folder.activeTabs.filter((tab) => tab !== tabToUnload);
+
+ if (folder.activeTabs.length === 0) {
+ lastTab = true;
+ animations.push(async () => {
+ folder.removeAttribute('has-active');
+ const groupItems = this.#normalizeGroupItems(folder.allItems);
+ const tabsContainer = folder.querySelector('.tab-group-container');
+
+ // Set correct margin-top after animation
+ const afterAnimate = () => {
+ groupStart.style.removeProperty('margin-top');
+ this.styleCleanup(groupItems);
+ // Trigger the recalculation so that zen returns
+ // the correct container size in the DOM
+ tabsContainer.offsetHeight;
+ tabsContainer.setAttribute('hidden', true);
const collapsedHeight = this.#calculateHeightShift(tabsContainer, []);
+ groupStart.style.marginTop = `${-(collapsedHeight + 4)}px`;
+ };
- // Collect animations for this specific folder becoming inactive
- const folderAnimation = [
- ...this.updateFolderIcon(folder, 'close', false),
- ...this.#createAnimation(
- groupStart,
- {
- marginTop: -(collapsedHeight + 4),
- },
- { duration: 0.12, ease: 'easeInOut' },
- afterAnimate
- ),
- ];
- await Promise.all(folderAnimation);
- });
- }
- }
+ const groupStart = folder.querySelector('.zen-tab-group-start');
+ const collapsedHeight = this.#calculateHeightShift(tabsContainer, []);
- tabToUnload.removeAttribute('folder-active');
- if (isSplitView) {
- tabToUnload = tabToUnload.group;
+ // Collect animations for this specific folder becoming inactive
+ const folderAnimation = [
+ ...this.updateFolderIcon(folder, 'close', false),
+ ...this.#createAnimation(
+ groupStart,
+ {
+ marginTop: -(collapsedHeight + 4),
+ },
+ { duration: 0.12, ease: 'easeInOut' },
+ afterAnimate
+ ),
+ ];
+ await Promise.all(folderAnimation);
+ });
}
+ }
- tabToUnload.style.removeProperty('--zen-folder-indent');
-
- let tabUnloadAnimations = [];
- if (!ungroup && !lastTab) {
- tabUnloadAnimations = this.#createAnimation(
- tabToUnload,
- {
- opacity: 0,
- height: 0,
- },
- {
- duration: 0.12,
- ease: 'easeInOut',
- }
- );
- }
+ tabToUnload.removeAttribute('folder-active');
+ if (isSplitView) {
+ tabToUnload = tabToUnload.group;
+ }
- // Manage global animation count
- this.#animationCount += 1;
+ tabToUnload.style.removeProperty('--zen-folder-indent');
- // Await the tab unload animation first
- await Promise.all(tabUnloadAnimations);
- await Promise.all(animations.map((item) => (typeof item === 'function' ? item() : item)));
- this.#animationCount -= 1;
- gBrowser.tabContainer._invalidateCachedTabs();
+ let tabUnloadAnimations = [];
+ if (!ungroup && !lastTab) {
+ tabUnloadAnimations = this.#createAnimation(
+ tabToUnload,
+ {
+ opacity: 0,
+ height: 0,
+ },
+ {
+ duration: 0.12,
+ ease: 'easeInOut',
+ }
+ );
}
- async animateSelect(group) {
- if (!group?.isZenFolder) return;
+ // Manage global animation count
+ this.#animationCount += 1;
- this.cancelPopupTimer();
+ // Await the tab unload animation first
+ await Promise.all(tabUnloadAnimations);
+ await Promise.all(animations.map((item) => (typeof item === 'function' ? item() : item)));
+ this.#animationCount -= 1;
+ gBrowser.tabContainer._invalidateCachedTabs();
+ }
- const animations = [];
- const selectedTabs = [];
- const splitViewIds = new Set();
- const itemsToHide = [];
+ async animateSelect(group) {
+ if (!group?.isZenFolder) return;
+
+ this.cancelPopupTimer();
+
+ const animations = [];
+ const selectedTabs = [];
+ const splitViewIds = new Set();
+ const itemsToHide = [];
+
+ const groupItems = this.#collectGroupItems(group, {
+ selectedTabs,
+ splitViewIds,
+ });
+
+ for (const tab of selectedTabs) {
+ let currentGroup = tab?.group?.hasAttribute('split-view-group')
+ ? tab.group.group
+ : tab?.group;
+ while (currentGroup) {
+ const activeTabs = selectedTabs.filter((t) => currentGroup.tabs.includes(t));
+ if (activeTabs.length) {
+ if (currentGroup.collapsed) {
+ if (currentGroup.hasAttribute('has-active')) {
+ // It is important to keep the sequence of elements as in the DOM
+ currentGroup.activeTabs = [
+ ...new Set([...currentGroup.activeTabs, ...activeTabs]),
+ ].sort((a, b) => a._tPos > b._tPos);
+ } else {
+ currentGroup.setAttribute('has-active', 'true');
+ currentGroup.activeTabs = activeTabs;
+ }
- const groupItems = this.#collectGroupItems(group, {
- selectedTabs,
- splitViewIds,
- });
+ const tabsContainer = currentGroup.querySelector('.tab-group-container');
+ const groupStart = currentGroup.querySelector('.zen-tab-group-start');
+ tabsContainer.style.overflow = 'clip';
- for (const tab of selectedTabs) {
- let currentGroup = tab?.group?.hasAttribute('split-view-group')
- ? tab.group.group
- : tab?.group;
- while (currentGroup) {
- const activeTabs = selectedTabs.filter((t) => currentGroup.tabs.includes(t));
- if (activeTabs.length) {
- if (currentGroup.collapsed) {
- if (currentGroup.hasAttribute('has-active')) {
- // It is important to keep the sequence of elements as in the DOM
- currentGroup.activeTabs = [
- ...new Set([...currentGroup.activeTabs, ...activeTabs]),
- ].sort((a, b) => a._tPos > b._tPos);
- } else {
- currentGroup.setAttribute('has-active', 'true');
- currentGroup.activeTabs = activeTabs;
- }
+ if (tabsContainer.hasAttribute('hidden')) tabsContainer.removeAttribute('hidden');
+
+ const afterMarginTop = () => {
+ tabsContainer.style.overflow = '';
+ };
- const tabsContainer = currentGroup.querySelector('.tab-group-container');
- const groupStart = currentGroup.querySelector('.zen-tab-group-start');
- tabsContainer.style.overflow = 'clip';
-
- if (tabsContainer.hasAttribute('hidden')) tabsContainer.removeAttribute('hidden');
-
- const afterMarginTop = () => {
- tabsContainer.style.overflow = '';
- };
-
- animations.push(
- ...this.updateFolderIcon(currentGroup, 'close', false),
- ...this.#createAnimation(
- groupStart,
- {
- marginTop: 0,
- },
- { duration: 0.12, ease: 'easeInOut' },
- afterMarginTop
- )
+ animations.push(
+ ...this.updateFolderIcon(currentGroup, 'close', false),
+ ...this.#createAnimation(
+ groupStart,
+ {
+ marginTop: 0,
+ },
+ { duration: 0.12, ease: 'easeInOut' },
+ afterMarginTop
+ )
+ );
+ for (const tab of activeTabs) {
+ this.setFolderIndentation(
+ [tab],
+ currentGroup,
+ /* for collapse = */ true,
+ /* animate = */ false
);
- for (const tab of activeTabs) {
- this.setFolderIndentation(
- [tab],
- currentGroup,
- /* for collapse = */ true,
- /* animate = */ false
- );
- }
}
}
- currentGroup = currentGroup.group;
}
+ currentGroup = currentGroup.group;
}
+ }
- const itemsToShow = [];
- if (selectedTabs.length) {
- for (let i = 0; i < groupItems.length; i++) {
- const { item, splitViewId } = groupItems[i];
+ const itemsToShow = [];
+ if (selectedTabs.length) {
+ for (let i = 0; i < groupItems.length; i++) {
+ const { item, splitViewId } = groupItems[i];
- itemsToShow.push(item);
+ itemsToShow.push(item);
- // Skip selected items
- if (selectedTabs.includes(item)) continue;
+ // Skip selected items
+ if (selectedTabs.includes(item)) continue;
- // Skip items from selected split-view groups
- if (splitViewId && splitViewIds.has(splitViewId)) continue;
+ // Skip items from selected split-view groups
+ if (splitViewId && splitViewIds.has(splitViewId)) continue;
- if (!item.hasAttribute?.('folder-active')) {
- if (!itemsToHide.includes(item)) itemsToHide.push(item);
- }
+ if (!item.hasAttribute?.('folder-active')) {
+ if (!itemsToHide.includes(item)) itemsToHide.push(item);
}
}
-
- // FIXME: This is a hack to fix the animations not working properly
- this.styleCleanup(itemsToShow);
- itemsToHide.forEach((item) => {
- item.style.opacity = 0;
- item.style.height = 0;
- });
-
- animations.push(
- ...this.#createAnimation(
- itemsToShow,
- {
- opacity: '',
- height: '',
- },
- {
- duration: 0.12,
- ease: 'easeInOut',
- }
- ),
- ...this.#createAnimation(
- itemsToHide,
- {
- opacity: 0,
- height: 0,
- },
- {
- duration: 0.12,
- ease: 'easeInOut',
- }
- )
- );
-
- this.#animationCount += 1;
- await Promise.all(animations);
- this.#animationCount -= 1;
- if (this.#animationCount) {
- return;
- }
-
- // Cleanup
- this.styleCleanup(itemsToHide);
- this.styleCleanup(selectedTabs);
}
- animateGroupMove(group, expand = false) {
- if (!group?.isZenFolder) return;
- const groupStart = group.querySelector('.zen-tab-group-start');
- const tabsContainer = group.querySelector('.tab-group-container');
- const heightContainer = expand ? 0 : this.#calculateHeightShift(tabsContainer, []);
- tabsContainer.style.overflow = 'clip';
+ // FIXME: This is a hack to fix the animations not working properly
+ this.styleCleanup(itemsToShow);
+ itemsToHide.forEach((item) => {
+ item.style.opacity = 0;
+ item.style.height = 0;
+ });
- this.#createAnimation(
- groupStart,
+ animations.push(
+ ...this.#createAnimation(
+ itemsToShow,
{
- marginTop: expand ? 0 : -(heightContainer + 4),
+ opacity: '',
+ height: '',
},
- { duration: 0.12, ease: 'easeInOut' }
- );
+ {
+ duration: 0.12,
+ ease: 'easeInOut',
+ }
+ ),
+ ...this.#createAnimation(
+ itemsToHide,
+ {
+ opacity: 0,
+ height: 0,
+ },
+ {
+ duration: 0.12,
+ ease: 'easeInOut',
+ }
+ )
+ );
+
+ this.#animationCount += 1;
+ await Promise.all(animations);
+ this.#animationCount -= 1;
+ if (this.#animationCount) {
+ return;
}
- styleCleanup(items) {
- items.forEach((item) => {
- item.style.removeProperty('opacity');
- item.style.removeProperty('height');
- });
- }
+ // Cleanup
+ this.styleCleanup(itemsToHide);
+ this.styleCleanup(selectedTabs);
}
- window.gZenFolders = new nsZenFolders();
+ animateGroupMove(group, expand = false) {
+ if (!group?.isZenFolder) return;
+ const groupStart = group.querySelector('.zen-tab-group-start');
+ const tabsContainer = group.querySelector('.tab-group-container');
+ const heightContainer = expand ? 0 : this.#calculateHeightShift(tabsContainer, []);
+ tabsContainer.style.overflow = 'clip';
+
+ this.#createAnimation(
+ groupStart,
+ {
+ marginTop: expand ? 0 : -(heightContainer + 4),
+ },
+ { duration: 0.12, ease: 'easeInOut' }
+ );
+ }
+
+ styleCleanup(items) {
+ items.forEach((item) => {
+ item.style.removeProperty('opacity');
+ item.style.removeProperty('height');
+ });
+ }
}
+
+window.gZenFolders = new nsZenFolders();
diff --git a/src/zen/folders/jar.inc.mn b/src/zen/folders/jar.inc.mn
new file mode 100644
index 0000000000..971b59dfcf
--- /dev/null
+++ b/src/zen/folders/jar.inc.mn
@@ -0,0 +1,7 @@
+# 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/.
+
+ content/browser/zen-components/ZenFolder.mjs (../../zen/folders/ZenFolder.mjs)
+ content/browser/zen-components/ZenFolders.mjs (../../zen/folders/ZenFolders.mjs)
+ content/browser/zen-styles/zen-folders.css (../../zen/folders/zen-folders.css)
\ No newline at end of file
diff --git a/src/zen/fonts/jar.inc.mn b/src/zen/fonts/jar.inc.mn
new file mode 100644
index 0000000000..d6650ad61a
--- /dev/null
+++ b/src/zen/fonts/jar.inc.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+ content/browser/zen-fonts/JunicodeVF-Italic.woff2 (../../zen/fonts/JunicodeVF-Italic.woff2)
+ content/browser/zen-fonts/JunicodeVF-Roman.woff2 (../../zen/fonts/JunicodeVF-Roman.woff2)
diff --git a/src/zen/glance/ZenGlanceManager.mjs b/src/zen/glance/ZenGlanceManager.mjs
index 3302a3f4fe..1f38cb56a5 100644
--- a/src/zen/glance/ZenGlanceManager.mjs
+++ b/src/zen/glance/ZenGlanceManager.mjs
@@ -2,1735 +2,1723 @@
// 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/.
-{
- /**
- * Manages the Zen Glance feature - a preview overlay system for tabs
- * Allows users to preview content without fully opening new tabs
- */
- class nsZenGlanceManager extends nsZenDOMOperatedFeature {
- // Animation state
- _animating = false;
- _lazyPref = {};
-
- // Glance management
- #glances = new Map();
- #currentGlanceID = null;
- #confirmationTimeout = null;
-
- // Animation flags
- animatingOpen = false;
- animatingFullOpen = false;
- closingGlance = false;
- #duringOpening = false;
- #ignoreClose = false;
-
- // Click handling
- #lastLinkClickData = { clientX: 0, clientY: 0, height: 0, width: 0 };
-
- // Arc animation configuration
- #ARC_CONFIG = Object.freeze({
- ARC_STEPS: 70, // Increased for smoother bounce
- MAX_ARC_HEIGHT: 25,
- ARC_HEIGHT_RATIO: 0.2, // Arc height = distance * ratio (capped at MAX_ARC_HEIGHT)
- });
+import { nsZenDOMOperatedFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
+
+/**
+ * Manages the Zen Glance feature - a preview overlay system for tabs
+ * Allows users to preview content without fully opening new tabs
+ */
+class nsZenGlanceManager extends nsZenDOMOperatedFeature {
+ // Animation state
+ _animating = false;
+ _lazyPref = {};
+
+ // Glance management
+ #glances = new Map();
+ #currentGlanceID = null;
+ #confirmationTimeout = null;
+
+ // Animation flags
+ animatingOpen = false;
+ animatingFullOpen = false;
+ closingGlance = false;
+ #duringOpening = false;
+ #ignoreClose = false;
+
+ // Click handling
+ #lastLinkClickData = { clientX: 0, clientY: 0, height: 0, width: 0 };
+
+ // Arc animation configuration
+ #ARC_CONFIG = Object.freeze({
+ ARC_STEPS: 70, // Increased for smoother bounce
+ MAX_ARC_HEIGHT: 25,
+ ARC_HEIGHT_RATIO: 0.2, // Arc height = distance * ratio (capped at MAX_ARC_HEIGHT)
+ });
+
+ #GLANCE_ANIMATION_DURATION = Services.prefs.getIntPref('zen.glance.animation-duration') / 1000;
+
+ init() {
+ this.#setupEventListeners();
+ this.#setupPreferences();
+ this.#setupObservers();
+ this.#insertIntoContextMenu();
+ }
- #GLANCE_ANIMATION_DURATION = Services.prefs.getIntPref('zen.glance.animation-duration') / 1000;
+ #setupEventListeners() {
+ window.addEventListener('TabClose', this.onTabClose.bind(this));
+ window.addEventListener('TabSelect', this.onLocationChange.bind(this));
- init() {
- this.#setupEventListeners();
- this.#setupPreferences();
- this.#setupObservers();
- this.#insertIntoContextMenu();
- }
+ document
+ .getElementById('tabbrowser-tabpanels')
+ .addEventListener('click', this.onOverlayClick.bind(this));
+ }
- #setupEventListeners() {
- window.addEventListener('TabClose', this.onTabClose.bind(this));
- window.addEventListener('TabSelect', this.onLocationChange.bind(this));
+ #setupPreferences() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this._lazyPref,
+ 'SHOULD_OPEN_EXTERNAL_TABS_IN_GLANCE',
+ 'zen.glance.open-essential-external-links',
+ false
+ );
+ }
- document
- .getElementById('tabbrowser-tabpanels')
- .addEventListener('click', this.onOverlayClick.bind(this));
- }
+ #setupObservers() {
+ Services.obs.addObserver(this, 'quit-application-requested');
+ }
- #setupPreferences() {
- XPCOMUtils.defineLazyPreferenceGetter(
- this._lazyPref,
- 'SHOULD_OPEN_EXTERNAL_TABS_IN_GLANCE',
- 'zen.glance.open-essential-external-links',
- false
- );
- }
+ #insertIntoContextMenu() {
+ const menuitem = document.createXULElement('menuitem');
+ menuitem.setAttribute('id', 'context-zenOpenLinkInGlance');
+ menuitem.setAttribute('hidden', 'true');
+ menuitem.setAttribute('data-l10n-id', 'zen-open-link-in-glance');
- #setupObservers() {
- Services.obs.addObserver(this, 'quit-application-requested');
+ menuitem.addEventListener('command', () => this.openGlance({ url: gContextMenu.linkURL }));
+
+ document.getElementById('context-sep-open').insertAdjacentElement('beforebegin', menuitem);
+ }
+
+ /**
+ * Handle main command set events for glance operations
+ * @param {Event} event - The command event
+ */
+ handleMainCommandSet(event) {
+ const command = event.target;
+ const commandHandlers = {
+ cmd_zenGlanceClose: () => this.closeGlance({ onTabClose: true }),
+ cmd_zenGlanceExpand: () => this.fullyOpenGlance(),
+ cmd_zenGlanceSplit: () => this.splitGlance(),
+ };
+
+ const handler = commandHandlers[command.id];
+ if (handler) {
+ handler();
}
+ }
- #insertIntoContextMenu() {
- const menuitem = document.createXULElement('menuitem');
- menuitem.setAttribute('id', 'context-zenOpenLinkInGlance');
- menuitem.setAttribute('hidden', 'true');
- menuitem.setAttribute('data-l10n-id', 'zen-open-link-in-glance');
+ /**
+ * Get the current glance browser element
+ * @returns {Browser} The current browser or null
+ */
+ get #currentBrowser() {
+ return this.#glances.get(this.#currentGlanceID)?.browser;
+ }
- menuitem.addEventListener('command', () => this.openGlance({ url: gContextMenu.linkURL }));
+ /**
+ * Get the current glance tab element
+ * @returns {Tab} The current tab or null
+ */
+ get #currentTab() {
+ return this.#glances.get(this.#currentGlanceID)?.tab;
+ }
- document.getElementById('context-sep-open').insertAdjacentElement('beforebegin', menuitem);
- }
+ /**
+ * Get the current glance parent tab element
+ * @returns {Tab} The parent tab or null
+ */
+ get #currentParentTab() {
+ return this.#glances.get(this.#currentGlanceID)?.parentTab;
+ }
- /**
- * Handle main command set events for glance operations
- * @param {Event} event - The command event
- */
- handleMainCommandSet(event) {
- const command = event.target;
- const commandHandlers = {
- cmd_zenGlanceClose: () => this.closeGlance({ onTabClose: true }),
- cmd_zenGlanceExpand: () => this.fullyOpenGlance(),
- cmd_zenGlanceSplit: () => this.splitGlance(),
- };
+ /**
+ * Handle clicks on the glance overlay
+ * @param {Event} event - The click event
+ */
+ onOverlayClick(event) {
+ const isOverlayClick = event.target === this.overlay;
+ const isNotContentClick = event.originalTarget !== this.contentWrapper;
- const handler = commandHandlers[command.id];
- if (handler) {
- handler();
- }
+ if (isOverlayClick && isNotContentClick) {
+ this.closeGlance({ onTabClose: true });
}
+ }
- /**
- * Get the current glance browser element
- * @returns {Browser} The current browser or null
- */
- get #currentBrowser() {
- return this.#glances.get(this.#currentGlanceID)?.browser;
+ /**
+ * Handle application observer notifications
+ * @param {Object} subject - The subject of the notification
+ * @param {string} topic - The topic of the notification
+ */
+ observe(subject, topic) {
+ if (topic === 'quit-application-requested') {
+ this.onUnload();
}
+ }
- /**
- * Get the current glance tab element
- * @returns {Tab} The current tab or null
- */
- get #currentTab() {
- return this.#glances.get(this.#currentGlanceID)?.tab;
+ /**
+ * Clean up all glances when the application is unloading
+ */
+ onUnload() {
+ for (const [, glance] of this.#glances) {
+ gBrowser.removeTab(glance.tab, { animate: false });
}
+ this.#glances.clear();
+ }
- /**
- * Get the current glance parent tab element
- * @returns {Tab} The parent tab or null
- */
- get #currentParentTab() {
- return this.#glances.get(this.#currentGlanceID)?.parentTab;
- }
+ /**
+ * Create a new browser element for a glance
+ * @param {string} url - The URL to load
+ * @param {Tab} currentTab - The current tab
+ * @param {Tab} existingTab - Optional existing tab to reuse
+ * @returns {Browser} The created browser element
+ */
+ #createBrowserElement(url, currentTab, existingTab = null) {
+ const newTabOptions = this.#createTabOptions(currentTab);
+ const newUUID = gZenUIManager.generateUuidv4();
- /**
- * Handle clicks on the glance overlay
- * @param {Event} event - The click event
- */
- onOverlayClick(event) {
- const isOverlayClick = event.target === this.overlay;
- const isNotContentClick = event.originalTarget !== this.contentWrapper;
+ currentTab._selected = true;
+ const newTab =
+ existingTab ?? gBrowser.addTrustedTab(Services.io.newURI(url).spec, newTabOptions);
- if (isOverlayClick && isNotContentClick) {
- this.closeGlance({ onTabClose: true });
- }
- }
+ this.#configureNewTab(newTab, currentTab, newUUID);
+ this.#registerGlance(newTab, currentTab, newUUID);
- /**
- * Handle application observer notifications
- * @param {Object} subject - The subject of the notification
- * @param {string} topic - The topic of the notification
- */
- observe(subject, topic) {
- if (topic === 'quit-application-requested') {
- this.onUnload();
- }
- }
+ gBrowser.selectedTab = newTab;
+ return this.#currentBrowser;
+ }
- /**
- * Clean up all glances when the application is unloading
- */
- onUnload() {
- for (const [, glance] of this.#glances) {
- gBrowser.removeTab(glance.tab, { animate: false });
- }
- this.#glances.clear();
- }
-
- /**
- * Create a new browser element for a glance
- * @param {string} url - The URL to load
- * @param {Tab} currentTab - The current tab
- * @param {Tab} existingTab - Optional existing tab to reuse
- * @returns {Browser} The created browser element
- */
- #createBrowserElement(url, currentTab, existingTab = null) {
- const newTabOptions = this.#createTabOptions(currentTab);
- const newUUID = gZenUIManager.generateUuidv4();
-
- currentTab._selected = true;
- const newTab =
- existingTab ?? gBrowser.addTrustedTab(Services.io.newURI(url).spec, newTabOptions);
-
- this.#configureNewTab(newTab, currentTab, newUUID);
- this.#registerGlance(newTab, currentTab, newUUID);
-
- gBrowser.selectedTab = newTab;
- return this.#currentBrowser;
- }
-
- /**
- * Create tab options for a new glance tab
- * @param {Tab} currentTab - The current tab
- * @returns {Object} Tab options
- */
- #createTabOptions(currentTab) {
- return {
- userContextId: currentTab.getAttribute('usercontextid') || '',
- skipBackgroundNotify: true,
- insertTab: true,
- skipLoad: false,
- };
+ /**
+ * Create tab options for a new glance tab
+ * @param {Tab} currentTab - The current tab
+ * @returns {Object} Tab options
+ */
+ #createTabOptions(currentTab) {
+ return {
+ userContextId: currentTab.getAttribute('usercontextid') || '',
+ skipBackgroundNotify: true,
+ insertTab: true,
+ skipLoad: false,
+ };
+ }
+
+ /**
+ * Configure a new tab for glance usage
+ * @param {Tab} newTab - The new tab to configure
+ * @param {Tab} currentTab - The current tab
+ * @param {string} glanceId - The glance ID
+ */
+ #configureNewTab(newTab, currentTab, glanceId) {
+ if (currentTab.hasAttribute('zenDefaultUserContextId')) {
+ newTab.setAttribute('zenDefaultUserContextId', true);
}
- /**
- * Configure a new tab for glance usage
- * @param {Tab} newTab - The new tab to configure
- * @param {Tab} currentTab - The current tab
- * @param {string} glanceId - The glance ID
- */
- #configureNewTab(newTab, currentTab, glanceId) {
- if (currentTab.hasAttribute('zenDefaultUserContextId')) {
- newTab.setAttribute('zenDefaultUserContextId', true);
- }
+ currentTab.querySelector('.tab-content').appendChild(newTab);
+ newTab.setAttribute('zen-glance-tab', true);
+ newTab.setAttribute('glance-id', glanceId);
+ currentTab.setAttribute('glance-id', glanceId);
+ }
- currentTab.querySelector('.tab-content').appendChild(newTab);
- newTab.setAttribute('zen-glance-tab', true);
- newTab.setAttribute('glance-id', glanceId);
- currentTab.setAttribute('glance-id', glanceId);
- }
-
- /**
- * Register a new glance in the glances map
- * @param {Tab} newTab - The new tab
- * @param {Tab} currentTab - The current tab
- * @param {string} glanceId - The glance ID
- */
- #registerGlance(newTab, currentTab, glanceId) {
- this.#glances.set(glanceId, {
- tab: newTab,
- parentTab: currentTab,
- browser: newTab.linkedBrowser,
- });
- this.#currentGlanceID = glanceId;
- }
+ /**
+ * Register a new glance in the glances map
+ * @param {Tab} newTab - The new tab
+ * @param {Tab} currentTab - The current tab
+ * @param {string} glanceId - The glance ID
+ */
+ #registerGlance(newTab, currentTab, glanceId) {
+ this.#glances.set(glanceId, {
+ tab: newTab,
+ parentTab: currentTab,
+ browser: newTab.linkedBrowser,
+ });
+ this.#currentGlanceID = glanceId;
+ }
- /**
- * Fill overlay references from a browser element
- * @param {Browser} browser - The browser element
- */
- fillOverlay(browser) {
- this.overlay = browser.closest('.browserSidebarContainer');
- this.browserWrapper = browser.closest('.browserContainer');
- this.contentWrapper = browser.closest('.browserStack');
- }
+ /**
+ * Fill overlay references from a browser element
+ * @param {Browser} browser - The browser element
+ */
+ fillOverlay(browser) {
+ this.overlay = browser.closest('.browserSidebarContainer');
+ this.browserWrapper = browser.closest('.browserContainer');
+ this.contentWrapper = browser.closest('.browserStack');
+ }
- /**
- * Create new overlay buttons with animation
- * @returns {DocumentFragment} The cloned button template
- */
- #createNewOverlayButtons() {
- const template = document.getElementById('zen-glance-sidebar-template');
- const newButtons = template.content.cloneNode(true);
- const container = newButtons.querySelector('.zen-glance-sidebar-container');
+ /**
+ * Create new overlay buttons with animation
+ * @returns {DocumentFragment} The cloned button template
+ */
+ #createNewOverlayButtons() {
+ const template = document.getElementById('zen-glance-sidebar-template');
+ const newButtons = template.content.cloneNode(true);
+ const container = newButtons.querySelector('.zen-glance-sidebar-container');
- this.#animateOverlayButtons(container);
- return newButtons;
- }
+ this.#animateOverlayButtons(container);
+ return newButtons;
+ }
- /**
- * Animate the overlay buttons entrance
- * @param {Element} container - The button container
- */
- #animateOverlayButtons(container) {
- container.style.opacity = 0;
+ /**
+ * Animate the overlay buttons entrance
+ * @param {Element} container - The button container
+ */
+ #animateOverlayButtons(container) {
+ container.style.opacity = 0;
+
+ const xOffset = gZenVerticalTabsManager._prefsRightSide ? 20 : -20;
+
+ gZenUIManager.motion.animate(
+ container,
+ {
+ opacity: [0, 1],
+ x: [xOffset, 0],
+ },
+ {
+ duration: 0.2,
+ type: 'spring',
+ delay: this.#GLANCE_ANIMATION_DURATION - 0.2,
+ bounce: 0,
+ }
+ );
+ }
- const xOffset = gZenVerticalTabsManager._prefsRightSide ? 20 : -20;
+ /**
+ * Get element preview data as a data URL
+ * @param {Object} data - Glance data
+ * @returns {Promise} Promise resolving to data URL or null
+ * if not available
+ */
+ async #getElementPreviewData(data) {
+ // Make the rect relative to the tabpanels. We dont do it directly on the
+ // content process since it does not take into account scroll. This way, we can
+ // be sure that the coordinates are correct.
+ const tabPanelsRect = gBrowser.tabpanels.getBoundingClientRect();
+ const rect = new DOMRect(
+ data.clientX + tabPanelsRect.left,
+ data.clientY + tabPanelsRect.top,
+ data.width,
+ data.height
+ );
+ return await this.#imageBitmapToBase64(
+ await window.browsingContext.currentWindowGlobal.drawSnapshot(
+ rect,
+ 1,
+ 'transparent',
+ undefined
+ )
+ );
+ }
- gZenUIManager.motion.animate(
- container,
- {
- opacity: [0, 1],
- x: [xOffset, 0],
- },
- {
- duration: 0.2,
- type: 'spring',
- delay: this.#GLANCE_ANIMATION_DURATION - 0.2,
- bounce: 0,
- }
- );
+ /**
+ * Set the last link click data
+ * @param {Object} data - The link click data
+ */
+ set lastLinkClickData(data) {
+ this.#lastLinkClickData = data;
+ }
+
+ /**
+ * Get the last link click data
+ * @returns {Object} The last link click data
+ */
+ get lastLinkClickData() {
+ return this.#lastLinkClickData;
+ }
+
+ /**
+ * Open a glance overlay with the specified data
+ * @param {Object} data - Glance data including URL, position, and dimensions
+ * @param {Tab} existingTab - Optional existing tab to reuse
+ * @param {Tab} ownerTab - The tab that owns this glance
+ * @returns {Promise} Promise that resolves to the glance tab
+ */
+ openGlance(data, existingTab = null, ownerTab = null) {
+ if (this.#currentBrowser) {
+ return;
}
- /**
- * Get element preview data as a data URL
- * @param {Object} data - Glance data
- * @returns {Promise} Promise resolving to data URL or null
- * if not available
- */
- async #getElementPreviewData(data) {
- // Make the rect relative to the tabpanels. We dont do it directly on the
- // content process since it does not take into account scroll. This way, we can
- // be sure that the coordinates are correct.
- const tabPanelsRect = gBrowser.tabpanels.getBoundingClientRect();
- const rect = new DOMRect(
- data.clientX + tabPanelsRect.left,
- data.clientY + tabPanelsRect.top,
- data.width,
- data.height
- );
- return await this.#imageBitmapToBase64(
- await window.browsingContext.currentWindowGlobal.drawSnapshot(
- rect,
- 1,
- 'transparent',
- undefined
- )
- );
+ if (gBrowser.selectedTab === this.#currentParentTab) {
+ gBrowser.selectedTab = this.#currentTab;
+ return;
}
- /**
- * Set the last link click data
- * @param {Object} data - The link click data
- */
- set lastLinkClickData(data) {
- this.#lastLinkClickData = data;
- }
-
- /**
- * Get the last link click data
- * @returns {Object} The last link click data
- */
- get lastLinkClickData() {
- return this.#lastLinkClickData;
- }
-
- /**
- * Open a glance overlay with the specified data
- * @param {Object} data - Glance data including URL, position, and dimensions
- * @param {Tab} existingTab - Optional existing tab to reuse
- * @param {Tab} ownerTab - The tab that owns this glance
- * @returns {Promise} Promise that resolves to the glance tab
- */
- openGlance(data, existingTab = null, ownerTab = null) {
- if (this.#currentBrowser) {
- return;
- }
+ if (!data.height || !data.width) {
+ data = {
+ ...data,
+ ...this.lastLinkClickData,
+ };
+ }
- if (gBrowser.selectedTab === this.#currentParentTab) {
- gBrowser.selectedTab = this.#currentTab;
- return;
- }
+ this.#setAnimationState(true);
+ const currentTab = ownerTab ?? gBrowser.selectedTab;
+ const browserElement = this.#createBrowserElement(data.url, currentTab, existingTab);
- if (!data.height || !data.width) {
- data = {
- ...data,
- ...this.lastLinkClickData,
- };
- }
+ this.fillOverlay(browserElement);
+ this.overlay.classList.add('zen-glance-overlay');
- this.#setAnimationState(true);
- const currentTab = ownerTab ?? gBrowser.selectedTab;
- const browserElement = this.#createBrowserElement(data.url, currentTab, existingTab);
-
- this.fillOverlay(browserElement);
- this.overlay.classList.add('zen-glance-overlay');
-
- return this.#animateGlanceOpening(data, browserElement);
- }
-
- /**
- * Set animation state flags
- * @param {boolean} isAnimating - Whether animations are active
- */
- #setAnimationState(isAnimating) {
- this.animatingOpen = isAnimating;
- this._animating = isAnimating;
- }
-
- /**
- * Animate the glance opening process
- * @param {Object} data - Glance data
- * @param {Browser} browserElement - The browser element
- * @returns {Promise} Promise that resolves to the glance tab
- */
- #animateGlanceOpening(data, browserElement) {
- this.#prepareGlanceAnimation(data, browserElement);
- // FIXME(cheffy): We *must* have the call back async (at least,
- // until a better solution is found). If we do it inside the requestAnimationFrame,
- // we see flashing and if we do it directly, the animation does not play at all.
- // eslint-disable-next-line no-async-promise-executor
- return new Promise(async (resolve) => {
- // Recalculate location. When opening from pinned tabs,
- // view splitter doesn't catch if the tab is a glance tab or not.
- gZenViewSplitter.onLocationChange(browserElement);
- if (data.width && data.height) {
- // It is guaranteed that we will animate this opacity later on
- // when we start animating the glance.
- this.contentWrapper.style.opacity = 0;
- data.elementData = await this.#getElementPreviewData(data);
- }
- this.#glances.get(this.#currentGlanceID).elementData = data.elementData;
- this.#executeGlanceAnimation(data, browserElement, resolve);
- });
- }
+ return this.#animateGlanceOpening(data, browserElement);
+ }
- /**
- * Prepare the glance for animation
- * @param {Object} data - Glance data
- * @param {Browser} browserElement - The browser element
- */
- #prepareGlanceAnimation(data, browserElement) {
- this.quickOpenGlance();
- const newButtons = this.#createNewOverlayButtons();
- this.browserWrapper.appendChild(newButtons);
+ /**
+ * Set animation state flags
+ * @param {boolean} isAnimating - Whether animations are active
+ */
+ #setAnimationState(isAnimating) {
+ this.animatingOpen = isAnimating;
+ this._animating = isAnimating;
+ }
- this.#setupGlancePositioning(data);
- this.#configureBrowserElement(browserElement);
- }
+ /**
+ * Animate the glance opening process
+ * @param {Object} data - Glance data
+ * @param {Browser} browserElement - The browser element
+ * @returns {Promise} Promise that resolves to the glance tab
+ */
+ #animateGlanceOpening(data, browserElement) {
+ this.#prepareGlanceAnimation(data, browserElement);
+ // FIXME(cheffy): We *must* have the call back async (at least,
+ // until a better solution is found). If we do it inside the requestAnimationFrame,
+ // we see flashing and if we do it directly, the animation does not play at all.
+ // eslint-disable-next-line no-async-promise-executor
+ return new Promise(async (resolve) => {
+ // Recalculate location. When opening from pinned tabs,
+ // view splitter doesn't catch if the tab is a glance tab or not.
+ gZenViewSplitter.onLocationChange(browserElement);
+ if (data.width && data.height) {
+ // It is guaranteed that we will animate this opacity later on
+ // when we start animating the glance.
+ this.contentWrapper.style.opacity = 0;
+ data.elementData = await this.#getElementPreviewData(data);
+ }
+ this.#glances.get(this.#currentGlanceID).elementData = data.elementData;
+ this.#executeGlanceAnimation(data, browserElement, resolve);
+ });
+ }
- /**
- * Animate the parent background
- */
- #animateParentBackground() {
- const parentSidebarContainer = this.#currentParentTab.linkedBrowser.closest(
- '.browserSidebarContainer'
- );
+ /**
+ * Prepare the glance for animation
+ * @param {Object} data - Glance data
+ * @param {Browser} browserElement - The browser element
+ */
+ #prepareGlanceAnimation(data, browserElement) {
+ this.quickOpenGlance();
+ const newButtons = this.#createNewOverlayButtons();
+ this.browserWrapper.appendChild(newButtons);
- gZenUIManager.motion.animate(
- parentSidebarContainer,
- {
- scale: [1, 0.98],
- opacity: [1, 0.4],
- },
- {
- duration: this.#GLANCE_ANIMATION_DURATION,
- type: 'spring',
- bounce: 0.2,
- }
- );
- }
+ this.#setupGlancePositioning(data);
+ this.#configureBrowserElement(browserElement);
+ }
- /**
- * Set up glance positioning
- * @param {Object} data - Glance data with position and dimensions
- */
- #setupGlancePositioning(data) {
- const { clientX, clientY, width, height } = data;
- const top = clientY + height / 2;
- const left = clientX + width / 2;
-
- this.overlay.removeAttribute('fade-out');
- this.browserWrapper.setAttribute('animate', true);
- this.browserWrapper.style.top = `${top}px`;
- this.browserWrapper.style.left = `${left}px`;
- this.browserWrapper.style.width = `${width}px`;
- this.browserWrapper.style.height = `${height}px`;
-
- this.#storeOriginalPosition();
- this.overlay.style.overflow = 'visible';
- }
-
- /**
- * Store the original position for later restoration
- */
- #storeOriginalPosition() {
- this.#glances.get(this.#currentGlanceID).originalPosition = {
- top: this.browserWrapper.style.top,
- left: this.browserWrapper.style.left,
- width: this.browserWrapper.style.width,
- height: this.browserWrapper.style.height,
- };
- }
+ /**
+ * Animate the parent background
+ */
+ #animateParentBackground() {
+ const parentSidebarContainer = this.#currentParentTab.linkedBrowser.closest(
+ '.browserSidebarContainer'
+ );
+
+ gZenUIManager.motion.animate(
+ parentSidebarContainer,
+ {
+ scale: [1, 0.98],
+ opacity: [1, 0.4],
+ },
+ {
+ duration: this.#GLANCE_ANIMATION_DURATION,
+ type: 'spring',
+ bounce: 0.2,
+ }
+ );
+ }
- #createGlancePreviewElement(src) {
- const imageDataElement = document.createXULElement('image');
- imageDataElement.setAttribute('src', src);
+ /**
+ * Set up glance positioning
+ * @param {Object} data - Glance data with position and dimensions
+ */
+ #setupGlancePositioning(data) {
+ const { clientX, clientY, width, height } = data;
+ const top = clientY + height / 2;
+ const left = clientX + width / 2;
+
+ this.overlay.removeAttribute('fade-out');
+ this.browserWrapper.setAttribute('animate', true);
+ this.browserWrapper.style.top = `${top}px`;
+ this.browserWrapper.style.left = `${left}px`;
+ this.browserWrapper.style.width = `${width}px`;
+ this.browserWrapper.style.height = `${height}px`;
+
+ this.#storeOriginalPosition();
+ this.overlay.style.overflow = 'visible';
+ }
- const parent = document.createElement('div');
- parent.classList.add('zen-glance-element-preview');
- parent.appendChild(imageDataElement);
- return parent;
+ /**
+ * Store the original position for later restoration
+ */
+ #storeOriginalPosition() {
+ this.#glances.get(this.#currentGlanceID).originalPosition = {
+ top: this.browserWrapper.style.top,
+ left: this.browserWrapper.style.left,
+ width: this.browserWrapper.style.width,
+ height: this.browserWrapper.style.height,
+ };
+ }
+
+ #createGlancePreviewElement(src) {
+ const imageDataElement = document.createXULElement('image');
+ imageDataElement.setAttribute('src', src);
+
+ const parent = document.createElement('div');
+ parent.classList.add('zen-glance-element-preview');
+ parent.appendChild(imageDataElement);
+ return parent;
+ }
+
+ /**
+ * Handle element preview if provided
+ * @param {Object} data - Glance data
+ * @returns {Element|null} The preview element or null
+ */
+ #handleElementPreview(data) {
+ if (!data.elementData) {
+ return null;
}
- /**
- * Handle element preview if provided
- * @param {Object} data - Glance data
- * @returns {Element|null} The preview element or null
- */
- #handleElementPreview(data) {
- if (!data.elementData) {
- return null;
+ const imageDataElement = this.#createGlancePreviewElement(data.elementData);
+ this.browserWrapper.prepend(imageDataElement);
+ this.#glances.get(this.#currentGlanceID).elementImageData = data.elementData;
+
+ gZenUIManager.motion.animate(
+ imageDataElement,
+ {
+ opacity: [1, 0],
+ },
+ {
+ duration: this.#GLANCE_ANIMATION_DURATION / 2,
+ easing: 'easeInOut',
}
+ );
- const imageDataElement = this.#createGlancePreviewElement(data.elementData);
- this.browserWrapper.prepend(imageDataElement);
- this.#glances.get(this.#currentGlanceID).elementImageData = data.elementData;
+ return imageDataElement;
+ }
- gZenUIManager.motion.animate(
- imageDataElement,
- {
- opacity: [1, 0],
- },
- {
- duration: this.#GLANCE_ANIMATION_DURATION / 2,
- easing: 'easeInOut',
- }
- );
+ /**
+ * Configure browser element for animation
+ * @param {Browser} browserElement - The browser element
+ */
+ #configureBrowserElement(browserElement) {
+ const rect = window.windowUtils.getBoundsWithoutFlushing(this.browserWrapper.parentElement);
+ const minWidth = rect.width * 0.85;
+ const minHeight = rect.height * 0.85;
- return imageDataElement;
- }
-
- /**
- * Configure browser element for animation
- * @param {Browser} browserElement - The browser element
- */
- #configureBrowserElement(browserElement) {
- const rect = window.windowUtils.getBoundsWithoutFlushing(this.browserWrapper.parentElement);
- const minWidth = rect.width * 0.85;
- const minHeight = rect.height * 0.85;
-
- browserElement.style.minWidth = `${minWidth}px`;
- browserElement.style.minHeight = `${minHeight}px`;
- }
-
- /**
- * Get the transform origin for the animation
- * @param {Object} data - Glance data with position and dimensions
- * @returns {string} The transform origin CSS value
- */
- #getTransformOrigin(data) {
- const { clientX, clientY } = data;
- return `${clientX}px ${clientY}px`;
- }
-
- /**
- * Execute the main glance animation
- * @param {Object} data - Glance data
- * @param {Browser} browserElement - The browser element
- * @param {Function} resolve - Promise resolve function
- */
- #executeGlanceAnimation(data, browserElement, resolve) {
- const imageDataElement = this.#handleElementPreview(data);
-
- // Create curved animation sequence
- const arcSequence = this.#createGlanceArcSequence(data, 'opening');
- const transformOrigin = this.#getTransformOrigin(data);
-
- this.browserWrapper.style.transformOrigin = transformOrigin;
-
- // Only animate if there is element data, so we can apply a
- // nice fade-in effect to the content. But if it doesn't exist,
- // we just fall back to always showing the browser directly.
- if (data.elementData) {
- gZenUIManager.motion
- .animate(
- this.contentWrapper,
- { opacity: [0, 1] },
- {
- duration: this.#GLANCE_ANIMATION_DURATION / 2,
- easing: 'easeInOut',
- }
- )
- .then(() => {
- this.contentWrapper.style.opacity = '';
- });
- }
+ browserElement.style.minWidth = `${minWidth}px`;
+ browserElement.style.minHeight = `${minHeight}px`;
+ }
- this.#animateParentBackground();
+ /**
+ * Get the transform origin for the animation
+ * @param {Object} data - Glance data with position and dimensions
+ * @returns {string} The transform origin CSS value
+ */
+ #getTransformOrigin(data) {
+ const { clientX, clientY } = data;
+ return `${clientX}px ${clientY}px`;
+ }
+
+ /**
+ * Execute the main glance animation
+ * @param {Object} data - Glance data
+ * @param {Browser} browserElement - The browser element
+ * @param {Function} resolve - Promise resolve function
+ */
+ #executeGlanceAnimation(data, browserElement, resolve) {
+ const imageDataElement = this.#handleElementPreview(data);
+
+ // Create curved animation sequence
+ const arcSequence = this.#createGlanceArcSequence(data, 'opening');
+ const transformOrigin = this.#getTransformOrigin(data);
+
+ this.browserWrapper.style.transformOrigin = transformOrigin;
+
+ // Only animate if there is element data, so we can apply a
+ // nice fade-in effect to the content. But if it doesn't exist,
+ // we just fall back to always showing the browser directly.
+ if (data.elementData) {
gZenUIManager.motion
- .animate(this.browserWrapper, arcSequence, {
- duration: gZenUIManager.testingEnabled ? 0 : this.#GLANCE_ANIMATION_DURATION,
- ease: 'easeInOut',
- })
+ .animate(
+ this.contentWrapper,
+ { opacity: [0, 1] },
+ {
+ duration: this.#GLANCE_ANIMATION_DURATION / 2,
+ easing: 'easeInOut',
+ }
+ )
.then(() => {
- this.#finalizeGlanceOpening(imageDataElement, browserElement, resolve);
+ this.contentWrapper.style.opacity = '';
});
}
- /**
- * Create arc animation sequence for glance animations
- * @param {Object} data - Glance data with position and dimensions
- * @param {string} direction - 'opening' or 'closing'
- * @returns {Object} Animation sequence object
- */
- #createGlanceArcSequence(data, direction) {
- const { clientX, clientY, width, height } = data;
-
- // Calculate start and end positions based on direction
- let startPosition, endPosition;
-
- const tabPanelsRect = window.windowUtils.getBoundsWithoutFlushing(gBrowser.tabpanels);
-
- const widthPercent = 0.85;
- if (direction === 'opening') {
- startPosition = {
- x: clientX + width / 2,
- y: clientY + height / 2,
- width: width,
- height: height,
- };
- endPosition = {
- x: tabPanelsRect.width / 2,
- y: tabPanelsRect.height / 2,
- width: tabPanelsRect.width * widthPercent,
- height: tabPanelsRect.height,
- };
- } else {
- // closing
- startPosition = {
- x: tabPanelsRect.width / 2,
- y: tabPanelsRect.height / 2,
- width: tabPanelsRect.width * widthPercent,
- height: tabPanelsRect.height,
- };
- endPosition = {
- x: Math.floor(clientX + width / 2),
- y: Math.floor(clientY + height / 2),
- width: width,
- height: height,
- };
- }
+ this.#animateParentBackground();
+ gZenUIManager.motion
+ .animate(this.browserWrapper, arcSequence, {
+ duration: gZenUIManager.testingEnabled ? 0 : this.#GLANCE_ANIMATION_DURATION,
+ ease: 'easeInOut',
+ })
+ .then(() => {
+ this.#finalizeGlanceOpening(imageDataElement, browserElement, resolve);
+ });
+ }
- // Calculate distance and arc parameters
- const distance = this.#calculateDistance(startPosition, endPosition);
- const { arcHeight, shouldArcDownward } = this.#calculateOptimalArc(
- startPosition,
- endPosition,
- distance
- );
+ /**
+ * Create arc animation sequence for glance animations
+ * @param {Object} data - Glance data with position and dimensions
+ * @param {string} direction - 'opening' or 'closing'
+ * @returns {Object} Animation sequence object
+ */
+ #createGlanceArcSequence(data, direction) {
+ const { clientX, clientY, width, height } = data;
- const sequence = {
- top: [],
- left: [],
- width: [],
- height: [],
- transform: [],
- };
+ // Calculate start and end positions based on direction
+ let startPosition, endPosition;
- const steps = this.#ARC_CONFIG.ARC_STEPS;
- const arcDirection = shouldArcDownward ? 1 : -1;
+ const tabPanelsRect = window.windowUtils.getBoundsWithoutFlushing(gBrowser.tabpanels);
- function easeInOutQuad(t) {
- return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
- }
+ const widthPercent = 0.85;
+ if (direction === 'opening') {
+ startPosition = {
+ x: clientX + width / 2,
+ y: clientY + height / 2,
+ width: width,
+ height: height,
+ };
+ endPosition = {
+ x: tabPanelsRect.width / 2,
+ y: tabPanelsRect.height / 2,
+ width: tabPanelsRect.width * widthPercent,
+ height: tabPanelsRect.height,
+ };
+ } else {
+ // closing
+ startPosition = {
+ x: tabPanelsRect.width / 2,
+ y: tabPanelsRect.height / 2,
+ width: tabPanelsRect.width * widthPercent,
+ height: tabPanelsRect.height,
+ };
+ endPosition = {
+ x: Math.floor(clientX + width / 2),
+ y: Math.floor(clientY + height / 2),
+ width: width,
+ height: height,
+ };
+ }
- function easeOutCubic(t) {
- return 1 - Math.pow(1 - t, 6);
+ // Calculate distance and arc parameters
+ const distance = this.#calculateDistance(startPosition, endPosition);
+ const { arcHeight, shouldArcDownward } = this.#calculateOptimalArc(
+ startPosition,
+ endPosition,
+ distance
+ );
+
+ const sequence = {
+ top: [],
+ left: [],
+ width: [],
+ height: [],
+ transform: [],
+ };
+
+ const steps = this.#ARC_CONFIG.ARC_STEPS;
+ const arcDirection = shouldArcDownward ? 1 : -1;
+
+ function easeInOutQuad(t) {
+ return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t;
+ }
+
+ function easeOutCubic(t) {
+ return 1 - Math.pow(1 - t, 6);
+ }
+
+ // First, create the main animation steps
+ for (let i = 0; i <= steps; i++) {
+ const progress = i / steps;
+ const eased = direction === 'opening' ? easeInOutQuad(progress) : easeOutCubic(progress);
+
+ // Calculate size interpolation
+ const currentWidth = startPosition.width + (endPosition.width - startPosition.width) * eased;
+ const currentHeight =
+ startPosition.height + (endPosition.height - startPosition.height) * eased;
+
+ // Calculate position on arc
+ const distanceX = endPosition.x - startPosition.x;
+ const distanceY = endPosition.y - startPosition.y;
+
+ const x = startPosition.x + distanceX * eased;
+ const y =
+ startPosition.y + distanceY * eased + arcDirection * arcHeight * (1 - (2 * eased - 1) ** 2);
+
+ sequence.transform.push(`translate(-50%, -50%) scale(1)`);
+ sequence.top.push(`${y}px`);
+ sequence.left.push(`${x}px`);
+ sequence.width.push(`${currentWidth}px`);
+ sequence.height.push(`${currentHeight}px`);
+ }
+
+ let scale = 1;
+ const bounceSteps = 60;
+ if (direction === 'opening') {
+ for (let i = 0; i < bounceSteps; i++) {
+ const progress = i / bounceSteps;
+ // Scale up slightly then back to normal
+ scale = 1 + 0.003 * Math.sin(progress * Math.PI);
+ // If we are at the last step, ensure scale is exactly 1
+ if (i === bounceSteps - 1) {
+ scale = 1;
+ }
+ sequence.transform.push(`translate(-50%, -50%) scale(${scale})`);
+ sequence.top.push(sequence.top[sequence.top.length - 1]);
+ sequence.left.push(sequence.left[sequence.left.length - 1]);
+ sequence.width.push(sequence.width[sequence.width.length - 1]);
+ sequence.height.push(sequence.height[sequence.height.length - 1]);
}
+ }
- // First, create the main animation steps
- for (let i = 0; i <= steps; i++) {
- const progress = i / steps;
- const eased = direction === 'opening' ? easeInOutQuad(progress) : easeOutCubic(progress);
-
- // Calculate size interpolation
- const currentWidth =
- startPosition.width + (endPosition.width - startPosition.width) * eased;
- const currentHeight =
- startPosition.height + (endPosition.height - startPosition.height) * eased;
-
- // Calculate position on arc
- const distanceX = endPosition.x - startPosition.x;
- const distanceY = endPosition.y - startPosition.y;
-
- const x = startPosition.x + distanceX * eased;
- const y =
- startPosition.y +
- distanceY * eased +
- arcDirection * arcHeight * (1 - (2 * eased - 1) ** 2);
-
- sequence.transform.push(`translate(-50%, -50%) scale(1)`);
- sequence.top.push(`${y}px`);
- sequence.left.push(`${x}px`);
- sequence.width.push(`${currentWidth}px`);
- sequence.height.push(`${currentHeight}px`);
- }
+ return sequence;
+ }
- let scale = 1;
- const bounceSteps = 60;
- if (direction === 'opening') {
- for (let i = 0; i < bounceSteps; i++) {
- const progress = i / bounceSteps;
- // Scale up slightly then back to normal
- scale = 1 + 0.003 * Math.sin(progress * Math.PI);
- // If we are at the last step, ensure scale is exactly 1
- if (i === bounceSteps - 1) {
- scale = 1;
- }
- sequence.transform.push(`translate(-50%, -50%) scale(${scale})`);
- sequence.top.push(sequence.top[sequence.top.length - 1]);
- sequence.left.push(sequence.left[sequence.left.length - 1]);
- sequence.width.push(sequence.width[sequence.width.length - 1]);
- sequence.height.push(sequence.height[sequence.height.length - 1]);
- }
- }
+ /**
+ * Calculate distance between two positions
+ * @param {Object} start - Start position
+ * @param {Object} end - End position
+ * @returns {number} Distance
+ */
+ #calculateDistance(start, end) {
+ const distanceX = end.x - start.x;
+ const distanceY = end.y - start.y;
+ return Math.sqrt(distanceX * distanceX + distanceY * distanceY);
+ }
- return sequence;
- }
-
- /**
- * Calculate distance between two positions
- * @param {Object} start - Start position
- * @param {Object} end - End position
- * @returns {number} Distance
- */
- #calculateDistance(start, end) {
- const distanceX = end.x - start.x;
- const distanceY = end.y - start.y;
- return Math.sqrt(distanceX * distanceX + distanceY * distanceY);
- }
-
- /**
- * Calculate optimal arc parameters
- * @param {Object} startPosition - Start position
- * @param {Object} endPosition - End position
- * @param {number} distance - Distance between positions
- * @returns {Object} Arc parameters
- */
- #calculateOptimalArc(startPosition, endPosition, distance) {
- // Calculate available space for the arc
- const availableTopSpace = Math.min(startPosition.y, endPosition.y);
- const viewportHeight = window.innerHeight;
- const availableBottomSpace = viewportHeight - Math.max(startPosition.y, endPosition.y);
-
- // Determine if we should arc downward or upward based on available space
- const shouldArcDownward = availableBottomSpace > availableTopSpace;
-
- // Use the space in the direction we're arcing
- const availableSpace = shouldArcDownward ? availableBottomSpace : availableTopSpace;
-
- // Limit arc height to a percentage of the available space
- const arcHeight = Math.min(
- distance * this.#ARC_CONFIG.ARC_HEIGHT_RATIO,
- this.#ARC_CONFIG.MAX_ARC_HEIGHT,
- availableSpace * 0.6
- );
+ /**
+ * Calculate optimal arc parameters
+ * @param {Object} startPosition - Start position
+ * @param {Object} endPosition - End position
+ * @param {number} distance - Distance between positions
+ * @returns {Object} Arc parameters
+ */
+ #calculateOptimalArc(startPosition, endPosition, distance) {
+ // Calculate available space for the arc
+ const availableTopSpace = Math.min(startPosition.y, endPosition.y);
+ const viewportHeight = window.innerHeight;
+ const availableBottomSpace = viewportHeight - Math.max(startPosition.y, endPosition.y);
+
+ // Determine if we should arc downward or upward based on available space
+ const shouldArcDownward = availableBottomSpace > availableTopSpace;
+
+ // Use the space in the direction we're arcing
+ const availableSpace = shouldArcDownward ? availableBottomSpace : availableTopSpace;
+
+ // Limit arc height to a percentage of the available space
+ const arcHeight = Math.min(
+ distance * this.#ARC_CONFIG.ARC_HEIGHT_RATIO,
+ this.#ARC_CONFIG.MAX_ARC_HEIGHT,
+ availableSpace * 0.6
+ );
+
+ return { arcHeight, shouldArcDownward };
+ }
- return { arcHeight, shouldArcDownward };
+ /**
+ * Finalize the glance opening process
+ * @param {Element|null} imageDataElement - The preview element
+ * @param {Browser} browserElement - The browser element
+ * @param {Function} resolve - Promise resolve function
+ */
+ #finalizeGlanceOpening(imageDataElement, browserElement, resolve) {
+ if (imageDataElement) {
+ imageDataElement.remove();
}
- /**
- * Finalize the glance opening process
- * @param {Element|null} imageDataElement - The preview element
- * @param {Browser} browserElement - The browser element
- * @param {Function} resolve - Promise resolve function
- */
- #finalizeGlanceOpening(imageDataElement, browserElement, resolve) {
- if (imageDataElement) {
- imageDataElement.remove();
- }
+ this.browserWrapper.style.transformOrigin = '';
- this.browserWrapper.style.transformOrigin = '';
-
- browserElement.style.minWidth = '';
- browserElement.style.minHeight = '';
-
- this.browserWrapper.style.height = '100%';
- this.browserWrapper.style.width = '85%';
-
- gBrowser.tabContainer._invalidateCachedTabs();
- this.overlay.style.removeProperty('overflow');
- this.browserWrapper.removeAttribute('animate');
- this.browserWrapper.setAttribute('has-finished-animation', true);
-
- this.#setAnimationState(false);
- this.#currentTab.dispatchEvent(new Event('GlanceOpen', { bubbles: true }));
- resolve(this.#currentTab);
- }
-
- /**
- * Clear container styles while preserving inset
- * @param {Element} container - The container element
- */
- #clearContainerStyles(container) {
- const inset = container.style.inset;
- container.removeAttribute('style');
- container.style.inset = inset;
- }
-
- /**
- * Close the current glance
- * @param {Object} options - Close options
- * @param {boolean} options.noAnimation - Skip animation
- * @param {boolean} options.onTabClose - Called during tab close
- * @param {string} options.setNewID - Set new glance ID
- * @param {boolean} options.hasFocused - Has focus confirmation
- * @param {boolean} options.skipPermitUnload - Skip unload permission check
- * @returns {Promise|undefined} Promise if animated, undefined if immediate
- */
- closeGlance({
- noAnimation = false,
- onTabClose = false,
- setNewID = null,
- hasFocused = false,
- skipPermitUnload = false,
- } = {}) {
- if (!this.#canCloseGlance(onTabClose)) {
- return;
- }
+ browserElement.style.minWidth = '';
+ browserElement.style.minHeight = '';
- if (!skipPermitUnload && !this.#checkPermitUnload()) {
- return;
- }
+ this.browserWrapper.style.height = '100%';
+ this.browserWrapper.style.width = '85%';
- const browserSidebarContainer = this.#currentParentTab?.linkedBrowser?.closest(
- '.browserSidebarContainer'
- );
- const sidebarButtons = this.browserWrapper.querySelector('.zen-glance-sidebar-container');
+ gBrowser.tabContainer._invalidateCachedTabs();
+ this.overlay.style.removeProperty('overflow');
+ this.browserWrapper.removeAttribute('animate');
+ this.browserWrapper.setAttribute('has-finished-animation', true);
- if (this.#handleConfirmationTimeout(onTabClose, hasFocused, sidebarButtons)) {
- return;
- }
+ this.#setAnimationState(false);
+ this.#currentTab.dispatchEvent(new Event('GlanceOpen', { bubbles: true }));
+ resolve(this.#currentTab);
+ }
- this.browserWrapper.removeAttribute('has-finished-animation');
+ /**
+ * Clear container styles while preserving inset
+ * @param {Element} container - The container element
+ */
+ #clearContainerStyles(container) {
+ const inset = container.style.inset;
+ container.removeAttribute('style');
+ container.style.inset = inset;
+ }
- if (noAnimation) {
- this.#clearContainerStyles(browserSidebarContainer);
- this.quickCloseGlance({ closeCurrentTab: false });
- return;
- }
+ /**
+ * Close the current glance
+ * @param {Object} options - Close options
+ * @param {boolean} options.noAnimation - Skip animation
+ * @param {boolean} options.onTabClose - Called during tab close
+ * @param {string} options.setNewID - Set new glance ID
+ * @param {boolean} options.hasFocused - Has focus confirmation
+ * @param {boolean} options.skipPermitUnload - Skip unload permission check
+ * @returns {Promise|undefined} Promise if animated, undefined if immediate
+ */
+ closeGlance({
+ noAnimation = false,
+ onTabClose = false,
+ setNewID = null,
+ hasFocused = false,
+ skipPermitUnload = false,
+ } = {}) {
+ if (!this.#canCloseGlance(onTabClose)) {
+ return;
+ }
- return this.#animateGlanceClosing(
- onTabClose,
- browserSidebarContainer,
- sidebarButtons,
- setNewID
- );
+ if (!skipPermitUnload && !this.#checkPermitUnload()) {
+ return;
}
- /**
- * Check if glance can be closed
- * @param {boolean} onTabClose - Whether this is called during tab close
- * @returns {boolean} True if can close
- */
- #canCloseGlance(onTabClose) {
- return !(
- (this._animating && !onTabClose) ||
- !this.#currentBrowser ||
- (this.animatingOpen && !onTabClose) ||
- this.#duringOpening
- );
+ const browserSidebarContainer = this.#currentParentTab?.linkedBrowser?.closest(
+ '.browserSidebarContainer'
+ );
+ const sidebarButtons = this.browserWrapper.querySelector('.zen-glance-sidebar-container');
+
+ if (this.#handleConfirmationTimeout(onTabClose, hasFocused, sidebarButtons)) {
+ return;
}
- /**
- * Check if unload is permitted
- * @returns {boolean} True if unload is permitted
- */
- #checkPermitUnload() {
- const { permitUnload } = this.#currentBrowser.permitUnload();
- return permitUnload;
- }
-
- /**
- * Handle confirmation timeout for focused close
- * @param {boolean} onTabClose - Whether this is called during tab close
- * @param {boolean} hasFocused - Has focus confirmation
- * @param {Element} sidebarButtons - The sidebar buttons element
- * @returns {boolean} True if should return early
- */
- #handleConfirmationTimeout(onTabClose, hasFocused, sidebarButtons) {
- if (onTabClose && hasFocused && !this.#confirmationTimeout && sidebarButtons) {
- const cancelButton = sidebarButtons.querySelector('.zen-glance-sidebar-close');
- cancelButton.setAttribute('waitconfirmation', true);
- this.#confirmationTimeout = setTimeout(() => {
- cancelButton.removeAttribute('waitconfirmation');
- this.#confirmationTimeout = null;
- }, 3000);
- return true;
- }
- return false;
+ this.browserWrapper.removeAttribute('has-finished-animation');
+
+ if (noAnimation) {
+ this.#clearContainerStyles(browserSidebarContainer);
+ this.quickCloseGlance({ closeCurrentTab: false });
+ return;
}
- /**
- * Animate the glance closing process
- * @param {boolean} onTabClose - Whether this is called during tab close
- * @param {Element} browserSidebarContainer - The sidebar container
- * @param {Element} sidebarButtons - The sidebar buttons
- * @param {string} setNewID - New glance ID to set
- * @returns {Promise} Promise that resolves when closing is complete
- */
- #animateGlanceClosing(onTabClose, browserSidebarContainer, sidebarButtons, setNewID) {
- if (this.closingGlance) {
- return;
- }
+ return this.#animateGlanceClosing(
+ onTabClose,
+ browserSidebarContainer,
+ sidebarButtons,
+ setNewID
+ );
+ }
- this.closingGlance = true;
- this._animating = true;
+ /**
+ * Check if glance can be closed
+ * @param {boolean} onTabClose - Whether this is called during tab close
+ * @returns {boolean} True if can close
+ */
+ #canCloseGlance(onTabClose) {
+ return !(
+ (this._animating && !onTabClose) ||
+ !this.#currentBrowser ||
+ (this.animatingOpen && !onTabClose) ||
+ this.#duringOpening
+ );
+ }
- gBrowser.moveTabAfter(this.#currentTab, this.#currentParentTab);
+ /**
+ * Check if unload is permitted
+ * @returns {boolean} True if unload is permitted
+ */
+ #checkPermitUnload() {
+ const { permitUnload } = this.#currentBrowser.permitUnload();
+ return permitUnload;
+ }
- if (onTabClose && gBrowser.tabs.length === 1) {
- BrowserCommands.openTab();
- return;
- }
+ /**
+ * Handle confirmation timeout for focused close
+ * @param {boolean} onTabClose - Whether this is called during tab close
+ * @param {boolean} hasFocused - Has focus confirmation
+ * @param {Element} sidebarButtons - The sidebar buttons element
+ * @returns {boolean} True if should return early
+ */
+ #handleConfirmationTimeout(onTabClose, hasFocused, sidebarButtons) {
+ if (onTabClose && hasFocused && !this.#confirmationTimeout && sidebarButtons) {
+ const cancelButton = sidebarButtons.querySelector('.zen-glance-sidebar-close');
+ cancelButton.setAttribute('waitconfirmation', true);
+ this.#confirmationTimeout = setTimeout(() => {
+ cancelButton.removeAttribute('waitconfirmation');
+ this.#confirmationTimeout = null;
+ }, 3000);
+ return true;
+ }
+ return false;
+ }
- this.#prepareGlanceForClosing();
- this.#animateSidebarButtons(sidebarButtons);
- this.#animateParentBackgroundClose(browserSidebarContainer);
-
- return this.#executeClosingAnimation(setNewID, onTabClose);
- }
-
- /**
- * Prepare glance for closing
- */
- #prepareGlanceForClosing() {
- // Critical: This line must not be touched - it works for unknown reasons
- this.#currentTab.style.display = 'none';
- this.overlay.setAttribute('fade-out', true);
- this.overlay.style.pointerEvents = 'none';
- this.quickCloseGlance({ justAnimateParent: true, clearID: false });
- }
-
- /**
- * Animate sidebar buttons out
- * @param {Element} sidebarButtons - The sidebar buttons element
- */
- #animateSidebarButtons(sidebarButtons) {
- if (sidebarButtons) {
- gZenUIManager.motion
- .animate(
- sidebarButtons,
- { opacity: [1, 0] },
- {
- duration: 0.2,
- type: 'spring',
- bounce: this.#GLANCE_ANIMATION_DURATION - 0.1,
- }
- )
- .then(() => {
- sidebarButtons.remove();
- });
- }
+ /**
+ * Animate the glance closing process
+ * @param {boolean} onTabClose - Whether this is called during tab close
+ * @param {Element} browserSidebarContainer - The sidebar container
+ * @param {Element} sidebarButtons - The sidebar buttons
+ * @param {string} setNewID - New glance ID to set
+ * @returns {Promise} Promise that resolves when closing is complete
+ */
+ #animateGlanceClosing(onTabClose, browserSidebarContainer, sidebarButtons, setNewID) {
+ if (this.closingGlance) {
+ return;
}
- #imageBitmapToBase64(imageBitmap) {
- // 1. Create a canvas with the same size as the ImageBitmap
- const canvas = document.createElement('canvas');
- canvas.width = imageBitmap.width;
- canvas.height = imageBitmap.height;
+ this.closingGlance = true;
+ this._animating = true;
- // 2. Draw the ImageBitmap onto the canvas
- const ctx = canvas.getContext('2d');
- ctx.drawImage(imageBitmap, 0, 0);
+ gBrowser.moveTabAfter(this.#currentTab, this.#currentParentTab);
- // 3. Convert the canvas content to a Base64 string (PNG by default)
- const base64String = canvas.toDataURL('image/png');
- return base64String;
+ if (onTabClose && gBrowser.tabs.length === 1) {
+ BrowserCommands.openTab();
+ return;
}
- /**
- * Animate parent background restoration
- * @param {Element} browserSidebarContainer - The sidebar container
- */
- #animateParentBackgroundClose(browserSidebarContainer) {
+ this.#prepareGlanceForClosing();
+ this.#animateSidebarButtons(sidebarButtons);
+ this.#animateParentBackgroundClose(browserSidebarContainer);
+
+ return this.#executeClosingAnimation(setNewID, onTabClose);
+ }
+
+ /**
+ * Prepare glance for closing
+ */
+ #prepareGlanceForClosing() {
+ // Critical: This line must not be touched - it works for unknown reasons
+ this.#currentTab.style.display = 'none';
+ this.overlay.setAttribute('fade-out', true);
+ this.overlay.style.pointerEvents = 'none';
+ this.quickCloseGlance({ justAnimateParent: true, clearID: false });
+ }
+
+ /**
+ * Animate sidebar buttons out
+ * @param {Element} sidebarButtons - The sidebar buttons element
+ */
+ #animateSidebarButtons(sidebarButtons) {
+ if (sidebarButtons) {
gZenUIManager.motion
.animate(
- browserSidebarContainer,
- {
- scale: [0.98, 1],
- opacity: [0.4, 1],
- },
+ sidebarButtons,
+ { opacity: [1, 0] },
{
- duration: this.#GLANCE_ANIMATION_DURATION / 1.5,
+ duration: 0.2,
type: 'spring',
- bounce: 0,
+ bounce: this.#GLANCE_ANIMATION_DURATION - 0.1,
}
)
.then(() => {
- this.#clearContainerStyles(browserSidebarContainer);
+ sidebarButtons.remove();
+ });
+ }
+ }
+
+ #imageBitmapToBase64(imageBitmap) {
+ // 1. Create a canvas with the same size as the ImageBitmap
+ const canvas = document.createElement('canvas');
+ canvas.width = imageBitmap.width;
+ canvas.height = imageBitmap.height;
+
+ // 2. Draw the ImageBitmap onto the canvas
+ const ctx = canvas.getContext('2d');
+ ctx.drawImage(imageBitmap, 0, 0);
+
+ // 3. Convert the canvas content to a Base64 string (PNG by default)
+ const base64String = canvas.toDataURL('image/png');
+ return base64String;
+ }
+
+ /**
+ * Animate parent background restoration
+ * @param {Element} browserSidebarContainer - The sidebar container
+ */
+ #animateParentBackgroundClose(browserSidebarContainer) {
+ gZenUIManager.motion
+ .animate(
+ browserSidebarContainer,
+ {
+ scale: [0.98, 1],
+ opacity: [0.4, 1],
+ },
+ {
+ duration: this.#GLANCE_ANIMATION_DURATION / 1.5,
+ type: 'spring',
+ bounce: 0,
+ }
+ )
+ .then(() => {
+ this.#clearContainerStyles(browserSidebarContainer);
+ });
+
+ this.browserWrapper.style.opacity = 1;
+ }
+
+ /**
+ * Execute the main closing animation
+ * @param {string} setNewID - New glance ID to set
+ * @param {boolean} onTabClose - Whether this is called during tab close
+ * @returns {Promise} Promise that resolves when complete
+ */
+ #executeClosingAnimation(setNewID, onTabClose) {
+ return new Promise((resolve) => {
+ const originalPosition = this.#glances.get(this.#currentGlanceID).originalPosition;
+ const elementImageData = this.#glances.get(this.#currentGlanceID).elementImageData;
+
+ this.#addElementPreview(elementImageData);
+
+ // Create curved closing animation sequence
+ const closingData = this.#createClosingDataFromOriginalPosition(originalPosition);
+ const arcSequence = this.#createGlanceArcSequence(closingData, 'closing');
+
+ gZenUIManager.motion
+ .animate(this.browserWrapper, arcSequence, {
+ duration: this.#GLANCE_ANIMATION_DURATION,
+ ease: 'easeOut',
+ })
+ .then(() => {
+ // Remove element preview after closing animation
+ const elementPreview = this.browserWrapper.querySelector('.zen-glance-element-preview');
+ if (elementPreview) {
+ elementPreview.remove();
+ }
+ this.#finalizeGlanceClosing(setNewID, resolve, onTabClose);
});
+ });
+ }
- this.browserWrapper.style.opacity = 1;
- }
-
- /**
- * Execute the main closing animation
- * @param {string} setNewID - New glance ID to set
- * @param {boolean} onTabClose - Whether this is called during tab close
- * @returns {Promise} Promise that resolves when complete
- */
- #executeClosingAnimation(setNewID, onTabClose) {
- return new Promise((resolve) => {
- const originalPosition = this.#glances.get(this.#currentGlanceID).originalPosition;
- const elementImageData = this.#glances.get(this.#currentGlanceID).elementImageData;
-
- this.#addElementPreview(elementImageData);
-
- // Create curved closing animation sequence
- const closingData = this.#createClosingDataFromOriginalPosition(originalPosition);
- const arcSequence = this.#createGlanceArcSequence(closingData, 'closing');
-
- gZenUIManager.motion
- .animate(this.browserWrapper, arcSequence, {
- duration: this.#GLANCE_ANIMATION_DURATION,
- ease: 'easeOut',
- })
- .then(() => {
- // Remove element preview after closing animation
- const elementPreview = this.browserWrapper.querySelector('.zen-glance-element-preview');
- if (elementPreview) {
- elementPreview.remove();
- }
- this.#finalizeGlanceClosing(setNewID, resolve, onTabClose);
- });
- });
+ /**
+ * Create closing data from original position for arc animation
+ * @param {Object} originalPosition - Original position object
+ * @returns {Object} Closing data object
+ */
+ #createClosingDataFromOriginalPosition(originalPosition) {
+ // Parse the original position values
+ const top = parseFloat(originalPosition.top) || 0;
+ const left = parseFloat(originalPosition.left) || 0;
+ const width = parseFloat(originalPosition.width) || 0;
+ const height = parseFloat(originalPosition.height) || 0;
+
+ return {
+ clientX: left - width / 2,
+ clientY: top - height / 2,
+ width: width,
+ height: height,
+ };
+ }
+
+ /**
+ * Add element preview if available, used for the closing animation
+ * @param {string} elementImageData - The element image data
+ */
+ #addElementPreview(elementImageData) {
+ if (elementImageData) {
+ const imageDataElement = this.#createGlancePreviewElement(elementImageData);
+ this.browserWrapper.prepend(imageDataElement);
}
+ }
- /**
- * Create closing data from original position for arc animation
- * @param {Object} originalPosition - Original position object
- * @returns {Object} Closing data object
- */
- #createClosingDataFromOriginalPosition(originalPosition) {
- // Parse the original position values
- const top = parseFloat(originalPosition.top) || 0;
- const left = parseFloat(originalPosition.left) || 0;
- const width = parseFloat(originalPosition.width) || 0;
- const height = parseFloat(originalPosition.height) || 0;
-
- return {
- clientX: left - width / 2,
- clientY: top - height / 2,
- width: width,
- height: height,
- };
+ /**
+ * Finalize the glance closing process
+ * @param {string} setNewID - New glance ID to set
+ * @param {Function} resolve - Promise resolve function
+ * @param {boolean} onTabClose - Whether this is called during tab close
+ */
+ #finalizeGlanceClosing(setNewID, resolve, onTabClose) {
+ this.browserWrapper.removeAttribute('animate');
+
+ if (!this.#currentParentTab) {
+ this.closingGlance = false;
+ return;
}
- /**
- * Add element preview if available, used for the closing animation
- * @param {string} elementImageData - The element image data
- */
- #addElementPreview(elementImageData) {
- if (elementImageData) {
- const imageDataElement = this.#createGlancePreviewElement(elementImageData);
- this.browserWrapper.prepend(imageDataElement);
- }
+ if (!onTabClose) {
+ this.quickCloseGlance({ clearID: false });
}
+ this.overlay.style.display = 'none';
+ this.overlay.removeAttribute('fade-out');
+ this.browserWrapper.removeAttribute('animate');
- /**
- * Finalize the glance closing process
- * @param {string} setNewID - New glance ID to set
- * @param {Function} resolve - Promise resolve function
- * @param {boolean} onTabClose - Whether this is called during tab close
- */
- #finalizeGlanceClosing(setNewID, resolve, onTabClose) {
- this.browserWrapper.removeAttribute('animate');
+ const lastCurrentTab = this.#currentTab;
+ this.#cleanupGlanceElements(lastCurrentTab);
+ this.#resetGlanceState(setNewID);
- if (!this.#currentParentTab) {
- this.closingGlance = false;
- return;
- }
+ this.#setAnimationState(false);
+ this.closingGlance = false;
- if (!onTabClose) {
- this.quickCloseGlance({ clearID: false });
- }
- this.overlay.style.display = 'none';
- this.overlay.removeAttribute('fade-out');
- this.browserWrapper.removeAttribute('animate');
+ if (this.#currentGlanceID) {
+ this.quickOpenGlance();
+ }
- const lastCurrentTab = this.#currentTab;
- this.#cleanupGlanceElements(lastCurrentTab);
- this.#resetGlanceState(setNewID);
+ resolve();
+ }
- this.#setAnimationState(false);
- this.closingGlance = false;
+ /**
+ * Clean up glance DOM elements
+ * @param {Tab} lastCurrentTab - The tab being closed
+ */
+ #cleanupGlanceElements(lastCurrentTab) {
+ this.overlay.classList.remove('zen-glance-overlay');
+ gBrowser
+ ._getSwitcher()
+ .setTabStateNoAction(lastCurrentTab, gBrowser.AsyncTabSwitcher.STATE_UNLOADED);
- if (this.#currentGlanceID) {
- this.quickOpenGlance();
- }
+ if (!this.#currentParentTab.selected) {
+ this.#currentParentTab._visuallySelected = false;
+ }
- resolve();
+ if (gBrowser.selectedTab === lastCurrentTab) {
+ gBrowser.selectedTab = this.#currentParentTab;
}
- /**
- * Clean up glance DOM elements
- * @param {Tab} lastCurrentTab - The tab being closed
- */
- #cleanupGlanceElements(lastCurrentTab) {
- this.overlay.classList.remove('zen-glance-overlay');
- gBrowser
- ._getSwitcher()
- .setTabStateNoAction(lastCurrentTab, gBrowser.AsyncTabSwitcher.STATE_UNLOADED);
+ if (
+ this.#currentParentTab.linkedBrowser &&
+ !this.#currentParentTab.hasAttribute('split-view')
+ ) {
+ this.#currentParentTab.linkedBrowser.zenModeActive = false;
+ }
- if (!this.#currentParentTab.selected) {
- this.#currentParentTab._visuallySelected = false;
- }
+ // Reset overlay references
+ this.browserWrapper = null;
+ this.overlay = null;
+ this.contentWrapper = null;
- if (gBrowser.selectedTab === lastCurrentTab) {
- gBrowser.selectedTab = this.#currentParentTab;
- }
+ lastCurrentTab.removeAttribute('zen-glance-tab');
- if (
- this.#currentParentTab.linkedBrowser &&
- !this.#currentParentTab.hasAttribute('split-view')
- ) {
- this.#currentParentTab.linkedBrowser.zenModeActive = false;
- }
+ this.#ignoreClose = true;
+ lastCurrentTab.dispatchEvent(new Event('GlanceClose', { bubbles: true }));
+ gBrowser.removeTab(lastCurrentTab, { animate: true, skipPermitUnload: true });
+ gBrowser.tabContainer._invalidateCachedTabs();
+ }
- // Reset overlay references
- this.browserWrapper = null;
- this.overlay = null;
- this.contentWrapper = null;
+ /**
+ * Reset glance state
+ * @param {string} setNewID - New glance ID to set
+ */
+ #resetGlanceState(setNewID) {
+ this.#currentParentTab.removeAttribute('glance-id');
+ this.#glances.delete(this.#currentGlanceID);
+ this.#currentGlanceID = setNewID;
+ this.#duringOpening = false;
+ }
- lastCurrentTab.removeAttribute('zen-glance-tab');
+ /**
+ * Quickly open glance without animation
+ */
+ quickOpenGlance() {
+ if (!this.#currentBrowser || this.#duringOpening) {
+ return;
+ }
+
+ this.#duringOpening = true;
+ // IMPORTANT: #setGlanceStates() must be called before #configureGlanceElements()
+ // to ensure that the glance state is fully set up before configuring the DOM elements.
+ // This order is required to avoid timing/state issues. Do not reorder without understanding the dependencies.
+ this.#setGlanceStates();
+ this.#configureGlanceElements();
+ this.#duringOpening = false;
+ }
- this.#ignoreClose = true;
- lastCurrentTab.dispatchEvent(new Event('GlanceClose', { bubbles: true }));
- gBrowser.removeTab(lastCurrentTab, { animate: true, skipPermitUnload: true });
- gBrowser.tabContainer._invalidateCachedTabs();
- }
+ /**
+ * Configure glance DOM elements
+ */
+ #configureGlanceElements() {
+ const parentBrowserContainer = this.#currentParentTab.linkedBrowser.closest(
+ '.browserSidebarContainer'
+ );
- /**
- * Reset glance state
- * @param {string} setNewID - New glance ID to set
- */
- #resetGlanceState(setNewID) {
- this.#currentParentTab.removeAttribute('glance-id');
- this.#glances.delete(this.#currentGlanceID);
- this.#currentGlanceID = setNewID;
- this.#duringOpening = false;
- }
+ parentBrowserContainer.classList.add('zen-glance-background');
+ parentBrowserContainer.classList.remove('zen-glance-overlay');
+ parentBrowserContainer.classList.add('deck-selected');
- /**
- * Quickly open glance without animation
- */
- quickOpenGlance() {
- if (!this.#currentBrowser || this.#duringOpening) {
- return;
- }
+ this.overlay.classList.add('deck-selected');
+ this.overlay.classList.add('zen-glance-overlay');
+ }
- this.#duringOpening = true;
- // IMPORTANT: #setGlanceStates() must be called before #configureGlanceElements()
- // to ensure that the glance state is fully set up before configuring the DOM elements.
- // This order is required to avoid timing/state issues. Do not reorder without understanding the dependencies.
- this.#setGlanceStates();
- this.#configureGlanceElements();
- this.#duringOpening = false;
- }
-
- /**
- * Configure glance DOM elements
- */
- #configureGlanceElements() {
- const parentBrowserContainer = this.#currentParentTab.linkedBrowser.closest(
- '.browserSidebarContainer'
- );
+ /**
+ * Set glance browser and tab states
+ */
+ #setGlanceStates() {
+ this.#currentParentTab.linkedBrowser.zenModeActive = true;
+ this.#currentParentTab.linkedBrowser.docShellIsActive = true;
+ this.#currentBrowser.zenModeActive = true;
+ this.#currentBrowser.docShellIsActive = true;
+ this.#currentBrowser.setAttribute('zen-glance-selected', true);
+ this.fillOverlay(this.#currentBrowser);
+ this.#currentParentTab._visuallySelected = true;
+ }
- parentBrowserContainer.classList.add('zen-glance-background');
- parentBrowserContainer.classList.remove('zen-glance-overlay');
- parentBrowserContainer.classList.add('deck-selected');
-
- this.overlay.classList.add('deck-selected');
- this.overlay.classList.add('zen-glance-overlay');
- }
-
- /**
- * Set glance browser and tab states
- */
- #setGlanceStates() {
- this.#currentParentTab.linkedBrowser.zenModeActive = true;
- this.#currentParentTab.linkedBrowser.docShellIsActive = true;
- this.#currentBrowser.zenModeActive = true;
- this.#currentBrowser.docShellIsActive = true;
- this.#currentBrowser.setAttribute('zen-glance-selected', true);
- this.fillOverlay(this.#currentBrowser);
- this.#currentParentTab._visuallySelected = true;
- }
-
- /**
- * Quickly close glance without animation
- * @param {Object} options - Close options
- * @param {boolean} options.closeCurrentTab - Close current tab
- * @param {boolean} options.closeParentTab - Close parent tab
- * @param {boolean} options.justAnimateParent - Only animate parent
- * @param {boolean} options.clearID - Clear current glance ID
- */
- quickCloseGlance({
- closeCurrentTab = true,
- closeParentTab = true,
- justAnimateParent = false,
- clearID = true,
- } = {}) {
- const parentHasBrowser = !!this.#currentParentTab.linkedBrowser;
- const browserContainer = this.#currentParentTab.linkedBrowser.closest(
- '.browserSidebarContainer'
- );
+ /**
+ * Quickly close glance without animation
+ * @param {Object} options - Close options
+ * @param {boolean} options.closeCurrentTab - Close current tab
+ * @param {boolean} options.closeParentTab - Close parent tab
+ * @param {boolean} options.justAnimateParent - Only animate parent
+ * @param {boolean} options.clearID - Clear current glance ID
+ */
+ quickCloseGlance({
+ closeCurrentTab = true,
+ closeParentTab = true,
+ justAnimateParent = false,
+ clearID = true,
+ } = {}) {
+ const parentHasBrowser = !!this.#currentParentTab.linkedBrowser;
+ const browserContainer = this.#currentParentTab.linkedBrowser.closest(
+ '.browserSidebarContainer'
+ );
- this.#removeParentBackground(parentHasBrowser, browserContainer);
+ this.#removeParentBackground(parentHasBrowser, browserContainer);
- if (!justAnimateParent && this.overlay) {
- this.#resetGlanceStates(
- closeCurrentTab,
- closeParentTab,
- parentHasBrowser,
- browserContainer
- );
- }
+ if (!justAnimateParent && this.overlay) {
+ this.#resetGlanceStates(closeCurrentTab, closeParentTab, parentHasBrowser, browserContainer);
+ }
- if (clearID) {
- this.#currentGlanceID = null;
- }
+ if (clearID) {
+ this.#currentGlanceID = null;
}
+ }
- /**
- * Remove parent background styling
- * @param {boolean} parentHasBrowser - Whether parent has browser
- * @param {Element} browserContainer - The browser container
- */
- #removeParentBackground(parentHasBrowser, browserContainer) {
- if (parentHasBrowser) {
- browserContainer.classList.remove('zen-glance-background');
- }
+ /**
+ * Remove parent background styling
+ * @param {boolean} parentHasBrowser - Whether parent has browser
+ * @param {Element} browserContainer - The browser container
+ */
+ #removeParentBackground(parentHasBrowser, browserContainer) {
+ if (parentHasBrowser) {
+ browserContainer.classList.remove('zen-glance-background');
}
+ }
- /**
- * Reset glance states
- * @param {boolean} closeCurrentTab - Whether to close current tab
- * @param {boolean} closeParentTab - Whether to close parent tab
- * @param {boolean} parentHasBrowser - Whether parent has browser
- * @param {Element} browserContainer - The browser container
- */
- #resetGlanceStates(closeCurrentTab, closeParentTab, parentHasBrowser, browserContainer) {
- if (parentHasBrowser && !this.#currentParentTab.hasAttribute('split-view')) {
- if (closeParentTab) {
- browserContainer.classList.remove('deck-selected');
- }
- this.#currentParentTab.linkedBrowser.zenModeActive = false;
+ /**
+ * Reset glance states
+ * @param {boolean} closeCurrentTab - Whether to close current tab
+ * @param {boolean} closeParentTab - Whether to close parent tab
+ * @param {boolean} parentHasBrowser - Whether parent has browser
+ * @param {Element} browserContainer - The browser container
+ */
+ #resetGlanceStates(closeCurrentTab, closeParentTab, parentHasBrowser, browserContainer) {
+ if (parentHasBrowser && !this.#currentParentTab.hasAttribute('split-view')) {
+ if (closeParentTab) {
+ browserContainer.classList.remove('deck-selected');
}
+ this.#currentParentTab.linkedBrowser.zenModeActive = false;
+ }
- this.#currentBrowser.zenModeActive = false;
-
- if (closeParentTab && parentHasBrowser) {
- this.#currentParentTab.linkedBrowser.docShellIsActive = false;
- }
+ this.#currentBrowser.zenModeActive = false;
- if (closeCurrentTab) {
- this.#currentBrowser.docShellIsActive = false;
- this.overlay.classList.remove('deck-selected');
- this.#currentTab._selected = false;
- }
+ if (closeParentTab && parentHasBrowser) {
+ this.#currentParentTab.linkedBrowser.docShellIsActive = false;
+ }
- if (!this.#currentParentTab._visuallySelected && closeParentTab) {
- this.#currentParentTab._visuallySelected = false;
- }
+ if (closeCurrentTab) {
+ this.#currentBrowser.docShellIsActive = false;
+ this.overlay.classList.remove('deck-selected');
+ this.#currentTab._selected = false;
+ }
- this.#currentBrowser.removeAttribute('zen-glance-selected');
- this.overlay.classList.remove('zen-glance-overlay');
- }
-
- /**
- * Open glance on location change if not animating
- * @param {Tab} prevTab - The previous tab
- */
- #onLocationChangeOpenGlance(prevTab) {
- if (!this.animatingOpen) {
- this.quickOpenGlance();
- if (prevTab && prevTab.linkedBrowser) {
- prevTab.linkedBrowser.docShellIsActive = false;
- prevTab.linkedBrowser
- .closest('.browserSidebarContainer')
- .classList.remove('deck-selected');
- }
- }
+ if (!this.#currentParentTab._visuallySelected && closeParentTab) {
+ this.#currentParentTab._visuallySelected = false;
}
- /**
- * Handle location change events
- * Note: Must be sync to avoid timing issues
- * @param {Event} event - The location change event
- */
- onLocationChange(event) {
- const tab = event.target;
- const prevTab = event.detail.previousTab;
+ this.#currentBrowser.removeAttribute('zen-glance-selected');
+ this.overlay.classList.remove('zen-glance-overlay');
+ }
- if (this.animatingFullOpen || this.closingGlance) {
- return;
+ /**
+ * Open glance on location change if not animating
+ * @param {Tab} prevTab - The previous tab
+ */
+ #onLocationChangeOpenGlance(prevTab) {
+ if (!this.animatingOpen) {
+ this.quickOpenGlance();
+ if (prevTab && prevTab.linkedBrowser) {
+ prevTab.linkedBrowser.docShellIsActive = false;
+ prevTab.linkedBrowser.closest('.browserSidebarContainer').classList.remove('deck-selected');
}
+ }
+ }
- if (this.#duringOpening || !tab.hasAttribute('glance-id')) {
- if (this.#currentGlanceID && !this.#duringOpening) {
- this.quickCloseGlance();
- }
- return;
- }
+ /**
+ * Handle location change events
+ * Note: Must be sync to avoid timing issues
+ * @param {Event} event - The location change event
+ */
+ onLocationChange(event) {
+ const tab = event.target;
+ const prevTab = event.detail.previousTab;
- if (this.#currentGlanceID && this.#currentGlanceID !== tab.getAttribute('glance-id')) {
- this.quickCloseGlance();
- }
+ if (this.animatingFullOpen || this.closingGlance) {
+ return;
+ }
- this.#currentGlanceID = tab.getAttribute('glance-id');
- if (gBrowser.selectedTab === this.#currentTab) {
- this.#onLocationChangeOpenGlance(prevTab);
- return;
+ if (this.#duringOpening || !tab.hasAttribute('glance-id')) {
+ if (this.#currentGlanceID && !this.#duringOpening) {
+ this.quickCloseGlance();
}
- this.#currentGlanceID = null;
+ return;
}
- /**
- * Handle tab close events
- * @param {Event} event - The tab close event
- */
- onTabClose(event) {
- if (event.target === this.#currentParentTab) {
- this.closeGlance({ onTabClose: true });
- }
+ if (this.#currentGlanceID && this.#currentGlanceID !== tab.getAttribute('glance-id')) {
+ this.quickCloseGlance();
}
- /**
- * Manage tab close for glance tabs
- * @param {Tab} tab - The tab being closed
- * @returns {boolean} Whether to continue with tab close
- */
- manageTabClose(tab) {
- if (!tab.hasAttribute('glance-id')) {
- return false;
- }
+ this.#currentGlanceID = tab.getAttribute('glance-id');
+ if (gBrowser.selectedTab === this.#currentTab) {
+ this.#onLocationChangeOpenGlance(prevTab);
+ return;
+ }
+ this.#currentGlanceID = null;
+ }
- const oldGlanceID = this.#currentGlanceID;
- const newGlanceID = tab.getAttribute('glance-id');
- this.#currentGlanceID = newGlanceID;
- const isDifferent = newGlanceID !== oldGlanceID;
+ /**
+ * Handle tab close events
+ * @param {Event} event - The tab close event
+ */
+ onTabClose(event) {
+ if (event.target === this.#currentParentTab) {
+ this.closeGlance({ onTabClose: true });
+ }
+ }
- if (this.#ignoreClose) {
- this.#ignoreClose = false;
- return false;
- }
+ /**
+ * Manage tab close for glance tabs
+ * @param {Tab} tab - The tab being closed
+ * @returns {boolean} Whether to continue with tab close
+ */
+ manageTabClose(tab) {
+ if (!tab.hasAttribute('glance-id')) {
+ return false;
+ }
- this.closeGlance({
- onTabClose: true,
- setNewID: isDifferent ? oldGlanceID : null,
- });
+ const oldGlanceID = this.#currentGlanceID;
+ const newGlanceID = tab.getAttribute('glance-id');
+ this.#currentGlanceID = newGlanceID;
+ const isDifferent = newGlanceID !== oldGlanceID;
- // Only continue tab close if we are not on the currently selected tab
- return !isDifferent;
+ if (this.#ignoreClose) {
+ this.#ignoreClose = false;
+ return false;
}
- /**
- * Check if two tabs have different domains
- * @param {Tab} tab1 - First tab
- * @param {nsIURI} url2 - Second URL
- * @returns {boolean} True if domains differ
- */
- tabDomainsDiffer(tab1, url2) {
- try {
- if (!tab1) {
- return true;
- }
-
- const url1 = tab1.linkedBrowser.currentURI.spec;
- if (url1.startsWith('about:')) {
- return true;
- }
+ this.closeGlance({
+ onTabClose: true,
+ setNewID: isDifferent ? oldGlanceID : null,
+ });
- // Only glance up links that are http(s) or file
- // https://github.com/zen-browser/desktop/issues/7173
- const url2Spec = url2.spec;
- if (!this.#isValidGlanceUrl(url2Spec)) {
- return false;
- }
+ // Only continue tab close if we are not on the currently selected tab
+ return !isDifferent;
+ }
- return Services.io.newURI(url1).host !== url2.host;
- } catch {
+ /**
+ * Check if two tabs have different domains
+ * @param {Tab} tab1 - First tab
+ * @param {nsIURI} url2 - Second URL
+ * @returns {boolean} True if domains differ
+ */
+ tabDomainsDiffer(tab1, url2) {
+ try {
+ if (!tab1) {
return true;
}
- }
- /**
- * Check if URL is valid for glance
- * @param {string} urlSpec - The URL spec
- * @returns {boolean} True if valid
- */
- #isValidGlanceUrl(urlSpec) {
- return (
- urlSpec.startsWith('http') || urlSpec.startsWith('https') || urlSpec.startsWith('file')
- );
- }
+ const url1 = tab1.linkedBrowser.currentURI.spec;
+ if (url1.startsWith('about:')) {
+ return true;
+ }
- /**
- * Check if a tab should be opened in glance
- * @param {Tab} tab - The tab to check
- * @param {nsIURI} uri - The URI to check
- * @returns {boolean} True if should open in glance
- */
- shouldOpenTabInGlance(tab, uri) {
- const owner = tab.owner;
+ // Only glance up links that are http(s) or file
+ // https://github.com/zen-browser/desktop/issues/7173
+ const url2Spec = url2.spec;
+ if (!this.#isValidGlanceUrl(url2Spec)) {
+ return false;
+ }
- return (
- owner &&
- owner.pinned &&
- this._lazyPref.SHOULD_OPEN_EXTERNAL_TABS_IN_GLANCE &&
- owner.linkedBrowser?.browsingContext?.isAppTab &&
- this.tabDomainsDiffer(owner, uri) &&
- Services.prefs.getBoolPref('zen.glance.enabled', true)
- );
+ return Services.io.newURI(url1).host !== url2.host;
+ } catch {
+ return true;
}
+ }
- /**
- * Handle tab open events
- * @param {Browser} browser - The browser element
- * @param {nsIURI} uri - The URI being opened
- */
- onTabOpen(browser, uri) {
- const tab = gBrowser.getTabForBrowser(browser);
- if (!tab) {
- return;
- }
+ /**
+ * Check if URL is valid for glance
+ * @param {string} urlSpec - The URL spec
+ * @returns {boolean} True if valid
+ */
+ #isValidGlanceUrl(urlSpec) {
+ return urlSpec.startsWith('http') || urlSpec.startsWith('https') || urlSpec.startsWith('file');
+ }
- try {
- if (this.shouldOpenTabInGlance(tab, uri)) {
- this.#openGlanceForTab(tab);
- }
- } catch (e) {
- console.error('Error opening glance for tab:', e);
- }
- }
+ /**
+ * Check if a tab should be opened in glance
+ * @param {Tab} tab - The tab to check
+ * @param {nsIURI} uri - The URI to check
+ * @returns {boolean} True if should open in glance
+ */
+ shouldOpenTabInGlance(tab, uri) {
+ const owner = tab.owner;
+
+ return (
+ owner &&
+ owner.pinned &&
+ this._lazyPref.SHOULD_OPEN_EXTERNAL_TABS_IN_GLANCE &&
+ owner.linkedBrowser?.browsingContext?.isAppTab &&
+ this.tabDomainsDiffer(owner, uri) &&
+ Services.prefs.getBoolPref('zen.glance.enabled', true)
+ );
+ }
- /**
- * Open glance for a specific tab
- * @param {Tab} tab - The tab to open glance for
- */
- #openGlanceForTab(tab) {
- this.openGlance(
- {
- url: undefined,
- },
- tab,
- tab.owner
- );
+ /**
+ * Handle tab open events
+ * @param {Browser} browser - The browser element
+ * @param {nsIURI} uri - The URI being opened
+ */
+ onTabOpen(browser, uri) {
+ const tab = gBrowser.getTabForBrowser(browser);
+ if (!tab) {
+ return;
}
- /**
- * Finish opening glance and clean up
- */
- finishOpeningGlance() {
- gBrowser.tabContainer._invalidateCachedTabs();
- gZenWorkspaces.updateTabsContainers();
- this.overlay.classList.remove('zen-glance-overlay');
- this.#clearContainerStyles(this.browserWrapper);
- this.animatingFullOpen = false;
- const glanceID = this.#currentGlanceID;
- this.closeGlance({ noAnimation: true, skipPermitUnload: true });
- this.#glances.delete(glanceID);
- }
-
- /**
- * Fully open glance (convert to regular tab)
- * @param {Object} options - Options for full opening
- * @param {boolean} options.forSplit - Whether this is for split view
- */
- async fullyOpenGlance({ forSplit = false } = {}) {
- if (!this.#currentGlanceID || !this.#currentTab) {
- return;
+ try {
+ if (this.shouldOpenTabInGlance(tab, uri)) {
+ this.#openGlanceForTab(tab);
}
+ } catch (e) {
+ console.error('Error opening glance for tab:', e);
+ }
+ }
+
+ /**
+ * Open glance for a specific tab
+ * @param {Tab} tab - The tab to open glance for
+ */
+ #openGlanceForTab(tab) {
+ this.openGlance(
+ {
+ url: undefined,
+ },
+ tab,
+ tab.owner
+ );
+ }
- this.animatingFullOpen = true;
- this.#currentTab.setAttribute('zen-dont-split-glance', true);
+ /**
+ * Finish opening glance and clean up
+ */
+ finishOpeningGlance() {
+ gBrowser.tabContainer._invalidateCachedTabs();
+ gZenWorkspaces.updateTabsContainers();
+ this.overlay.classList.remove('zen-glance-overlay');
+ this.#clearContainerStyles(this.browserWrapper);
+ this.animatingFullOpen = false;
+ const glanceID = this.#currentGlanceID;
+ this.closeGlance({ noAnimation: true, skipPermitUnload: true });
+ this.#glances.delete(glanceID);
+ }
- this.#handleZenFolderPinning();
- gBrowser.moveTabAfter(this.#currentTab, this.#currentParentTab);
+ /**
+ * Fully open glance (convert to regular tab)
+ * @param {Object} options - Options for full opening
+ * @param {boolean} options.forSplit - Whether this is for split view
+ */
+ async fullyOpenGlance({ forSplit = false } = {}) {
+ if (!this.#currentGlanceID || !this.#currentTab) {
+ return;
+ }
- const browserRect = window.windowUtils.getBoundsWithoutFlushing(this.browserWrapper);
- this.#prepareTabForFullOpen();
+ this.animatingFullOpen = true;
+ this.#currentTab.setAttribute('zen-dont-split-glance', true);
- const sidebarButtons = this.browserWrapper.querySelector('.zen-glance-sidebar-container');
- if (sidebarButtons) {
- sidebarButtons.remove();
- }
+ this.#handleZenFolderPinning();
+ gBrowser.moveTabAfter(this.#currentTab, this.#currentParentTab);
- if (forSplit) {
- this.finishOpeningGlance();
- return;
- }
+ const browserRect = window.windowUtils.getBoundsWithoutFlushing(this.browserWrapper);
+ this.#prepareTabForFullOpen();
- if (gReduceMotion) {
- gZenViewSplitter.deactivateCurrentSplitView();
- this.finishOpeningGlance();
- return;
- }
+ const sidebarButtons = this.browserWrapper.querySelector('.zen-glance-sidebar-container');
+ if (sidebarButtons) {
+ sidebarButtons.remove();
+ }
- await this.#animateFullOpen(browserRect);
+ if (forSplit) {
this.finishOpeningGlance();
+ return;
}
- /**
- * Handle Zen folder pinning if applicable
- */
- #handleZenFolderPinning() {
- const isZenFolder = this.#currentParentTab?.group?.isZenFolder;
- if (Services.prefs.getBoolPref('zen.folders.owned-tabs-in-folder') && isZenFolder) {
- gBrowser.pinTab(this.#currentTab);
- }
+ if (gReduceMotion) {
+ gZenViewSplitter.deactivateCurrentSplitView();
+ this.finishOpeningGlance();
+ return;
}
- /**
- * Prepare tab for full opening
- */
- #prepareTabForFullOpen() {
- this.#currentTab.removeAttribute('zen-glance-tab');
- this.#clearContainerStyles(this.browserWrapper);
- this.#currentTab.removeAttribute('glance-id');
- this.#currentParentTab.removeAttribute('glance-id');
- gBrowser.selectedTab = this.#currentTab;
+ await this.#animateFullOpen(browserRect);
+ this.finishOpeningGlance();
+ }
- this.#currentParentTab.linkedBrowser
- .closest('.browserSidebarContainer')
- .classList.remove('zen-glance-background');
- this.#currentParentTab._visuallySelected = false;
- gBrowser.TabStateFlusher.flush(this.#currentTab.linkedBrowser);
+ /**
+ * Handle Zen folder pinning if applicable
+ */
+ #handleZenFolderPinning() {
+ const isZenFolder = this.#currentParentTab?.group?.isZenFolder;
+ if (Services.prefs.getBoolPref('zen.folders.owned-tabs-in-folder') && isZenFolder) {
+ gBrowser.pinTab(this.#currentTab);
}
+ }
- /**
- * Animate the full opening process
- * @param {Object} browserRect - The browser rectangle
- */
- async #animateFullOpen(browserRect) {
- // Write styles early to avoid flickering
- this.browserWrapper.style.opacity = 1;
- this.browserWrapper.style.width = `${browserRect.width}px`;
- this.browserWrapper.style.height = `${browserRect.height}px`;
+ /**
+ * Prepare tab for full opening
+ */
+ #prepareTabForFullOpen() {
+ this.#currentTab.removeAttribute('zen-glance-tab');
+ this.#clearContainerStyles(this.browserWrapper);
+ this.#currentTab.removeAttribute('glance-id');
+ this.#currentParentTab.removeAttribute('glance-id');
+ gBrowser.selectedTab = this.#currentTab;
+
+ this.#currentParentTab.linkedBrowser
+ .closest('.browserSidebarContainer')
+ .classList.remove('zen-glance-background');
+ this.#currentParentTab._visuallySelected = false;
+ gBrowser.TabStateFlusher.flush(this.#currentTab.linkedBrowser);
+ }
- await gZenUIManager.motion.animate(
- this.browserWrapper,
- {
- width: ['85%', '100%'],
- height: ['100%', '100%'],
- },
- {
- duration: this.#GLANCE_ANIMATION_DURATION,
- type: 'spring',
- bounce: 0,
- }
- );
+ /**
+ * Animate the full opening process
+ * @param {Object} browserRect - The browser rectangle
+ */
+ async #animateFullOpen(browserRect) {
+ // Write styles early to avoid flickering
+ this.browserWrapper.style.opacity = 1;
+ this.browserWrapper.style.width = `${browserRect.width}px`;
+ this.browserWrapper.style.height = `${browserRect.height}px`;
+
+ await gZenUIManager.motion.animate(
+ this.browserWrapper,
+ {
+ width: ['85%', '100%'],
+ height: ['100%', '100%'],
+ },
+ {
+ duration: this.#GLANCE_ANIMATION_DURATION,
+ type: 'spring',
+ bounce: 0,
+ }
+ );
+
+ this.browserWrapper.style.width = '';
+ this.browserWrapper.style.height = '';
+ this.browserWrapper.style.opacity = '';
+ gZenViewSplitter.deactivateCurrentSplitView({ removeDeckSelected: true });
+ }
- this.browserWrapper.style.width = '';
- this.browserWrapper.style.height = '';
- this.browserWrapper.style.opacity = '';
- gZenViewSplitter.deactivateCurrentSplitView({ removeDeckSelected: true });
- }
+ /**
+ * Open glance for bookmark activation
+ * @param {Event} event - The bookmark click event
+ * @returns {boolean} False to prevent default behavior
+ */
+ openGlanceForBookmark(event) {
+ const activationMethod = Services.prefs.getStringPref('zen.glance.activation-method', 'ctrl');
- /**
- * Open glance for bookmark activation
- * @param {Event} event - The bookmark click event
- * @returns {boolean} False to prevent default behavior
- */
- openGlanceForBookmark(event) {
- const activationMethod = Services.prefs.getStringPref('zen.glance.activation-method', 'ctrl');
+ if (!this.#isActivationKeyPressed(event, activationMethod)) {
+ return;
+ }
- if (!this.#isActivationKeyPressed(event, activationMethod)) {
- return;
- }
+ event.preventDefault();
+ event.stopPropagation();
- event.preventDefault();
- event.stopPropagation();
+ const data = this.#createGlanceDataFromBookmark(event);
+ this.openGlance(data);
- const data = this.#createGlanceDataFromBookmark(event);
- this.openGlance(data);
+ return false;
+ }
- return false;
- }
+ /**
+ * Check if the correct activation key is pressed
+ * @param {Event} event - The event
+ * @param {string} activationMethod - The activation method
+ * @returns {boolean} True if key is pressed
+ */
+ #isActivationKeyPressed(event, activationMethod) {
+ const keyMap = {
+ ctrl: event.ctrlKey,
+ alt: event.altKey,
+ shift: event.shiftKey,
+ meta: event.metaKey,
+ };
+
+ return keyMap[activationMethod] || false;
+ }
- /**
- * Check if the correct activation key is pressed
- * @param {Event} event - The event
- * @param {string} activationMethod - The activation method
- * @returns {boolean} True if key is pressed
- */
- #isActivationKeyPressed(event, activationMethod) {
- const keyMap = {
- ctrl: event.ctrlKey,
- alt: event.altKey,
- shift: event.shiftKey,
- meta: event.metaKey,
- };
+ /**
+ * Create glance data from bookmark event
+ * @param {Event} event - The bookmark event
+ * @returns {Object} Glance data object
+ */
+ #createGlanceDataFromBookmark(event) {
+ const rect = window.windowUtils.getBoundsWithoutFlushing(event.target);
+ const tabPanelRect = window.windowUtils.getBoundsWithoutFlushing(gBrowser.tabpanels);
+ // the bookmark is most likely outisde the tabpanel, so we need to give a negative number
+ // so it can be corrected later
+ let top = rect.top - tabPanelRect.top;
+ let left = rect.left - tabPanelRect.left;
+ return {
+ url: event.target._placesNode.uri,
+ clientX: left,
+ clientY: top,
+ width: rect.width,
+ height: rect.height,
+ };
+ }
- return keyMap[activationMethod] || false;
- }
-
- /**
- * Create glance data from bookmark event
- * @param {Event} event - The bookmark event
- * @returns {Object} Glance data object
- */
- #createGlanceDataFromBookmark(event) {
- const rect = window.windowUtils.getBoundsWithoutFlushing(event.target);
- const tabPanelRect = window.windowUtils.getBoundsWithoutFlushing(gBrowser.tabpanels);
- // the bookmark is most likely outisde the tabpanel, so we need to give a negative number
- // so it can be corrected later
- let top = rect.top - tabPanelRect.top;
- let left = rect.left - tabPanelRect.left;
- return {
- url: event.target._placesNode.uri,
- clientX: left,
- clientY: top,
- width: rect.width,
- height: rect.height,
- };
- }
+ /**
+ * Get the focused tab based on direction
+ * @param {number} aDir - Direction (-1 for parent, 1 for current)
+ * @returns {Tab} The focused tab
+ */
+ getFocusedTab(aDir) {
+ return aDir < 0 ? this.#currentParentTab : this.#currentTab;
+ }
- /**
- * Get the focused tab based on direction
- * @param {number} aDir - Direction (-1 for parent, 1 for current)
- * @returns {Tab} The focused tab
- */
- getFocusedTab(aDir) {
- return aDir < 0 ? this.#currentParentTab : this.#currentTab;
+ /**
+ * Split the current glance into a split view
+ */
+ async splitGlance() {
+ if (!this.#currentGlanceID) {
+ return;
}
- /**
- * Split the current glance into a split view
- */
- async splitGlance() {
- if (!this.#currentGlanceID) {
- return;
- }
-
- const currentTab = this.#currentTab;
- const currentParentTab = this.#currentParentTab;
+ const currentTab = this.#currentTab;
+ const currentParentTab = this.#currentParentTab;
- this.#handleZenFolderPinningForSplit(currentParentTab);
- await this.fullyOpenGlance({ forSplit: true });
+ this.#handleZenFolderPinningForSplit(currentParentTab);
+ await this.fullyOpenGlance({ forSplit: true });
- gZenViewSplitter.splitTabs([currentTab, currentParentTab], 'vsep', 1);
+ gZenViewSplitter.splitTabs([currentTab, currentParentTab], 'vsep', 1);
- const browserContainer = currentTab.linkedBrowser?.closest('.browserSidebarContainer');
- if (!gReduceMotion && browserContainer) {
- gZenViewSplitter.animateBrowserDrop(browserContainer);
- }
+ const browserContainer = currentTab.linkedBrowser?.closest('.browserSidebarContainer');
+ if (!gReduceMotion && browserContainer) {
+ gZenViewSplitter.animateBrowserDrop(browserContainer);
}
+ }
- /**
- * Handle Zen folder pinning for split view
- * @param {Tab} parentTab - The parent tab
- */
- #handleZenFolderPinningForSplit(parentTab) {
- const isZenFolder = parentTab?.group?.isZenFolder;
- if (Services.prefs.getBoolPref('zen.folders.owned-tabs-in-folder') && isZenFolder) {
- gBrowser.pinTab(this.#currentTab);
- }
+ /**
+ * Handle Zen folder pinning for split view
+ * @param {Tab} parentTab - The parent tab
+ */
+ #handleZenFolderPinningForSplit(parentTab) {
+ const isZenFolder = parentTab?.group?.isZenFolder;
+ if (Services.prefs.getBoolPref('zen.folders.owned-tabs-in-folder') && isZenFolder) {
+ gBrowser.pinTab(this.#currentTab);
}
+ }
- /**
- * Get the tab or its glance parent
- * @param {Tab} tab - The tab to check
- * @returns {Tab} The tab or its parent
- */
- getTabOrGlanceParent(tab) {
- if (tab?.hasAttribute('glance-id') && this.#glances) {
- const parentTab = this.#glances.get(tab.getAttribute('glance-id'))?.parentTab;
- if (parentTab) {
- return parentTab;
- }
+ /**
+ * Get the tab or its glance parent
+ * @param {Tab} tab - The tab to check
+ * @returns {Tab} The tab or its parent
+ */
+ getTabOrGlanceParent(tab) {
+ if (tab?.hasAttribute('glance-id') && this.#glances) {
+ const parentTab = this.#glances.get(tab.getAttribute('glance-id'))?.parentTab;
+ if (parentTab) {
+ return parentTab;
}
- return tab;
- }
-
- /**
- * Get the tab or its glance child
- * @param {Tab} tab - The tab to check
- * @returns {Tab} The tab or its child
- */
- getTabOrGlanceChild(tab) {
- return tab?.glanceTab || tab;
}
+ return tab;
+ }
- /**
- * Check if deck should remain selected
- * @param {Element} currentPanel - Current panel
- * @param {Element} oldPanel - Previous panel
- * @returns {boolean} True if deck should remain selected
- */
- shouldShowDeckSelected(currentPanel, oldPanel) {
- const currentBrowser = currentPanel?.querySelector('browser');
- const oldBrowser = oldPanel?.querySelector('browser');
-
- if (!currentBrowser || !oldBrowser) {
- return false;
- }
-
- const currentTab = gBrowser.getTabForBrowser(currentBrowser);
- const oldTab = gBrowser.getTabForBrowser(oldBrowser);
+ /**
+ * Get the tab or its glance child
+ * @param {Tab} tab - The tab to check
+ * @returns {Tab} The tab or its child
+ */
+ getTabOrGlanceChild(tab) {
+ return tab?.glanceTab || tab;
+ }
- if (!currentTab || !oldTab) {
- return false;
- }
+ /**
+ * Check if deck should remain selected
+ * @param {Element} currentPanel - Current panel
+ * @param {Element} oldPanel - Previous panel
+ * @returns {boolean} True if deck should remain selected
+ */
+ shouldShowDeckSelected(currentPanel, oldPanel) {
+ const currentBrowser = currentPanel?.querySelector('browser');
+ const oldBrowser = oldPanel?.querySelector('browser');
- const currentGlanceID = currentTab.getAttribute('glance-id');
- const oldGlanceID = oldTab.getAttribute('glance-id');
+ if (!currentBrowser || !oldBrowser) {
+ return false;
+ }
- if (currentGlanceID && oldGlanceID) {
- return (
- currentGlanceID === oldGlanceID && oldPanel.classList.contains('zen-glance-background')
- );
- }
+ const currentTab = gBrowser.getTabForBrowser(currentBrowser);
+ const oldTab = gBrowser.getTabForBrowser(oldBrowser);
+ if (!currentTab || !oldTab) {
return false;
}
- /**
- * Handle search select command
- * @param {string} where - Where to open the search result
- */
- onSearchSelectCommand(where) {
- if (!this.#isGlanceEnabledForSearch()) {
- return;
- }
-
- if (where !== 'tab') {
- return;
- }
+ const currentGlanceID = currentTab.getAttribute('glance-id');
+ const oldGlanceID = oldTab.getAttribute('glance-id');
- const currentTab = gBrowser.selectedTab;
- const parentTab = currentTab.owner;
+ if (currentGlanceID && oldGlanceID) {
+ return (
+ currentGlanceID === oldGlanceID && oldPanel.classList.contains('zen-glance-background')
+ );
+ }
- if (!parentTab || parentTab.hasAttribute('glance-id')) {
- return;
- }
+ return false;
+ }
- this.#openGlanceForSearch(currentTab, parentTab);
+ /**
+ * Handle search select command
+ * @param {string} where - Where to open the search result
+ */
+ onSearchSelectCommand(where) {
+ if (!this.#isGlanceEnabledForSearch()) {
+ return;
}
- /**
- * Check if glance is enabled for search
- * @returns {boolean} True if enabled
- */
- #isGlanceEnabledForSearch() {
- return (
- Services.prefs.getBoolPref('zen.glance.enabled', false) &&
- Services.prefs.getBoolPref('zen.glance.enable-contextmenu-search', true)
- );
+ if (where !== 'tab') {
+ return;
}
- /**
- * Open glance for search result
- * @param {Tab} currentTab - Current tab
- * @param {Tab} parentTab - Parent tab
- */
- #openGlanceForSearch(currentTab, parentTab) {
- const browserRect = window.windowUtils.getBoundsWithoutFlushing(gBrowser.tabbox);
- const clickPosition = gZenUIManager._lastClickPosition || {
- clientX: browserRect.width / 2,
- clientY: browserRect.height / 2,
- };
+ const currentTab = gBrowser.selectedTab;
+ const parentTab = currentTab.owner;
- this.openGlance(
- {
- url: undefined,
- ...clickPosition,
- width: 0,
- height: 0,
- },
- currentTab,
- parentTab
- );
+ if (!parentTab || parentTab.hasAttribute('glance-id')) {
+ return;
}
+
+ this.#openGlanceForSearch(currentTab, parentTab);
}
- window.gZenGlanceManager = new nsZenGlanceManager();
+ /**
+ * Check if glance is enabled for search
+ * @returns {boolean} True if enabled
+ */
+ #isGlanceEnabledForSearch() {
+ return (
+ Services.prefs.getBoolPref('zen.glance.enabled', false) &&
+ Services.prefs.getBoolPref('zen.glance.enable-contextmenu-search', true)
+ );
+ }
+
+ /**
+ * Open glance for search result
+ * @param {Tab} currentTab - Current tab
+ * @param {Tab} parentTab - Parent tab
+ */
+ #openGlanceForSearch(currentTab, parentTab) {
+ const browserRect = window.windowUtils.getBoundsWithoutFlushing(gBrowser.tabbox);
+ const clickPosition = gZenUIManager._lastClickPosition || {
+ clientX: browserRect.width / 2,
+ clientY: browserRect.height / 2,
+ };
+
+ this.openGlance(
+ {
+ url: undefined,
+ ...clickPosition,
+ width: 0,
+ height: 0,
+ },
+ currentTab,
+ parentTab
+ );
+ }
}
+
+window.gZenGlanceManager = new nsZenGlanceManager();
diff --git a/src/zen/glance/actors/ZenGlanceChild.sys.mjs b/src/zen/glance/actors/ZenGlanceChild.sys.mjs
index 47337790aa..7d39efdc9d 100644
--- a/src/zen/glance/actors/ZenGlanceChild.sys.mjs
+++ b/src/zen/glance/actors/ZenGlanceChild.sys.mjs
@@ -3,9 +3,11 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
export class ZenGlanceChild extends JSWindowActorChild {
#activationMethod;
+ #glanceTarget = null;
constructor() {
super();
+ this.mousemoveCallback = this.mousemoveCallback.bind(this);
}
async handleEvent(event) {
@@ -80,11 +82,30 @@ export class ZenGlanceChild extends JSWindowActorChild {
} else if (activationMethod === 'meta' && !event.metaKey) {
return;
}
- if (target) {
+ this.#glanceTarget = target;
+ this.contentWindow.addEventListener('mousemove', this.mousemoveCallback, { once: true });
+ }
+
+ on_mouseup() {
+ if (this.#glanceTarget) {
+ // Don't clear the glance target here, we need it in the click handler
+ // See issue https://github.com/zen-browser/desktop/issues/11409
+ this.#openGlance(this.#glanceTarget);
+ }
+ this.contentWindow.removeEventListener('mousemove', this.mousemoveCallback);
+ }
+
+ on_click(event) {
+ if (this.#glanceTarget) {
event.preventDefault();
event.stopPropagation();
+ this.#glanceTarget = null;
+ }
+ }
- this.#openGlance(target);
+ mousemoveCallback() {
+ if (this.#glanceTarget) {
+ this.#glanceTarget = null;
}
}
diff --git a/src/zen/glance/jar.inc.mn b/src/zen/glance/jar.inc.mn
new file mode 100644
index 0000000000..caeadcbe26
--- /dev/null
+++ b/src/zen/glance/jar.inc.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+ content/browser/zen-components/ZenGlanceManager.mjs (../../zen/glance/ZenGlanceManager.mjs)
+ content/browser/zen-styles/zen-glance.css (../../zen/glance/zen-glance.css)
\ No newline at end of file
diff --git a/src/zen/images/jar.inc.mn b/src/zen/images/jar.inc.mn
new file mode 100644
index 0000000000..087e54654c
--- /dev/null
+++ b/src/zen/images/jar.inc.mn
@@ -0,0 +1,25 @@
+# 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/.
+
+ content/browser/zen-images/brand-header.svg (../../zen/images/brand-header.svg)
+ content/browser/zen-images/layouts/collapsed.png (../../zen/images/layouts/collapsed.png)
+ content/browser/zen-images/layouts/multiple-toolbar.png (../../zen/images/layouts/multiple-toolbar.png)
+ content/browser/zen-images/layouts/single-toolbar.png (../../zen/images/layouts/single-toolbar.png)
+ content/browser/zen-images/grain-bg.png (../../zen/images/grain-bg.png)
+ content/browser/zen-images/note-indicator.svg (../../zen/images/note-indicator.svg)
+
+ content/browser/zen-images/downloads/download.svg (../../zen/images/downloads/download.svg)
+ content/browser/zen-images/downloads/archive.svg (../../zen/images/downloads/archive.svg)
+
+ # FavIcons for startup
+ content/browser/zen-images/favicons/calendar.svg (../../zen/images/favicons/calendar.svg)
+ content/browser/zen-images/favicons/discord.svg (../../zen/images/favicons/discord.svg)
+ content/browser/zen-images/favicons/figma.svg (../../zen/images/favicons/figma.svg)
+ content/browser/zen-images/favicons/github.svg (../../zen/images/favicons/github.svg)
+ content/browser/zen-images/favicons/notion.svg (../../zen/images/favicons/notion.svg)
+ content/browser/zen-images/favicons/obsidian.svg (../../zen/images/favicons/obsidian.svg)
+ content/browser/zen-images/favicons/slack.svg (../../zen/images/favicons/slack.svg)
+ content/browser/zen-images/favicons/reddit.svg (../../zen/images/favicons/reddit.svg)
+ content/browser/zen-images/favicons/x.svg (../../zen/images/favicons/x.svg)
+ content/browser/zen-images/favicons/trello.svg (../../zen/images/favicons/trello.svg)
diff --git a/src/zen/kbs/ZenKeyboardShortcuts.mjs b/src/zen/kbs/ZenKeyboardShortcuts.mjs
index 55421898e0..ced9f9488f 100644
--- a/src/zen/kbs/ZenKeyboardShortcuts.mjs
+++ b/src/zen/kbs/ZenKeyboardShortcuts.mjs
@@ -2,6 +2,8 @@
// 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/.
+import { nsZenMultiWindowFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
+
const KEYCODE_MAP = {
F1: 'VK_F1',
F2: 'VK_F2',
@@ -123,14 +125,14 @@ const fixedL10nIds = {
const ZEN_MAIN_KEYSET_ID = 'mainKeyset';
const ZEN_DEVTOOLS_KEYSET_ID = 'devtoolsKeyset';
-const ZEN_KEYSET_ID = 'zenKeyset';
+window.ZEN_KEYSET_ID = 'zenKeyset';
const ZEN_COMPACT_MODE_SHORTCUTS_GROUP = 'zen-compact-mode';
const ZEN_WORKSPACE_SHORTCUTS_GROUP = 'zen-workspace';
const ZEN_OTHER_SHORTCUTS_GROUP = 'zen-other';
const ZEN_SPLIT_VIEW_SHORTCUTS_GROUP = 'zen-split-view';
const FIREFOX_SHORTCUTS_GROUP = 'zen-kbs-invalid';
-const VALID_SHORTCUT_GROUPS = [
+window.VALID_SHORTCUT_GROUPS = [
ZEN_COMPACT_MODE_SHORTCUTS_GROUP,
ZEN_WORKSPACE_SHORTCUTS_GROUP,
ZEN_SPLIT_VIEW_SHORTCUTS_GROUP,
@@ -139,7 +141,7 @@ const VALID_SHORTCUT_GROUPS = [
'other',
];
-class nsKeyShortcutModifiers {
+export class nsKeyShortcutModifiers {
#control = false;
#alt = false;
#shift = false;
@@ -320,7 +322,7 @@ class KeyShortcut {
this.#key = key?.toLowerCase();
this.#keycode = keycode;
- if (!VALID_SHORTCUT_GROUPS.includes(group)) {
+ if (!window.VALID_SHORTCUT_GROUPS.includes(group)) {
throw new Error('Illegal group value: ' + group);
}
@@ -1100,7 +1102,7 @@ class nsZenKeyboardShortcutsVersioner {
}
}
-var gZenKeyboardShortcutsManager = {
+window.gZenKeyboardShortcutsManager = {
loader: new nsZenKeyboardShortcutsLoader(),
_hasToLoadDevtools: false,
_inlineCommands: [],
@@ -1400,7 +1402,7 @@ document.addEventListener(
'MozBeforeInitialXULLayout',
() => {
if (Services.prefs.getBoolPref('zen.keyboard.shortcuts.enabled', false)) {
- gZenKeyboardShortcutsManager.beforeInit();
+ window.gZenKeyboardShortcutsManager.beforeInit();
}
},
{ once: true }
diff --git a/src/zen/kbs/jar.inc.mn b/src/zen/kbs/jar.inc.mn
new file mode 100644
index 0000000000..d8b3563d54
--- /dev/null
+++ b/src/zen/kbs/jar.inc.mn
@@ -0,0 +1,5 @@
+# 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/.
+
+ content/browser/zen-components/ZenKeyboardShortcuts.mjs (../../zen/kbs/ZenKeyboardShortcuts.mjs)
diff --git a/src/zen/media/ZenMediaController.mjs b/src/zen/media/ZenMediaController.mjs
index 9b3c18ee8c..f3566c028c 100644
--- a/src/zen/media/ZenMediaController.mjs
+++ b/src/zen/media/ZenMediaController.mjs
@@ -1,688 +1,686 @@
// 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/.
-{
- const lazy = {};
- XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- 'RESPECT_PIP_DISABLED',
- 'media.videocontrols.picture-in-picture.respect-disablePictureInPicture',
- true
- );
-
- class nsZenMediaController {
- _currentMediaController = null;
- _currentBrowser = null;
- _mediaUpdateInterval = null;
-
- mediaTitle = null;
- mediaArtist = null;
- mediaControlBar = null;
- mediaProgressBar = null;
- mediaCurrentTime = null;
- mediaDuration = null;
- mediaFocusButton = null;
- mediaProgressBarContainer = null;
-
- supportedKeys = ['playpause', 'previoustrack', 'nexttrack'];
- mediaControllersMap = new Map();
-
- _tabTimeout = null;
- _controllerSwitchTimeout = null;
-
- #isSeeking = false;
-
- init() {
- if (!Services.prefs.getBoolPref('zen.mediacontrols.enabled', true)) return;
-
- this.mediaTitle = document.querySelector('#zen-media-title');
- this.mediaArtist = document.querySelector('#zen-media-artist');
- this.mediaControlBar = document.querySelector('#zen-media-controls-toolbar');
- this.mediaProgressBar = document.querySelector('#zen-media-progress-bar');
- this.mediaCurrentTime = document.querySelector('#zen-media-current-time');
- this.mediaDuration = document.querySelector('#zen-media-duration');
- this.mediaFocusButton = document.querySelector('#zen-media-focus-button');
- this.mediaProgressBarContainer = document.querySelector('#zen-media-progress-hbox');
-
- this.onPositionstateChange = this._onPositionstateChange.bind(this);
- this.onPlaybackstateChange = this._onPlaybackstateChange.bind(this);
- this.onSupportedKeysChange = this._onSupportedKeysChange.bind(this);
- this.onMetadataChange = this._onMetadataChange.bind(this);
- this.onDeactivated = this._onDeactivated.bind(this);
- this.onPipModeChange = this._onPictureInPictureModeChange.bind(this);
-
- this.#initEventListeners();
- }
-
- #initEventListeners() {
- this.mediaControlBar.addEventListener('mousedown', (event) => {
- if (event.target.closest(':is(toolbarbutton,#zen-media-progress-hbox)')) return;
- else this.onMediaFocus();
- });
- this.mediaControlBar.addEventListener('command', (event) => {
- const button = event.target.closest('toolbarbutton');
- if (!button) return;
- switch (button.id) {
- case 'zen-media-pip-button':
- this.onMediaPip();
- break;
- case 'zen-media-close-button':
- this.onControllerClose();
- break;
- case 'zen-media-focus-button':
- this.onMediaFocus();
- break;
- case 'zen-media-mute-button':
- this.onMediaMute();
- break;
- case 'zen-media-previoustrack-button':
- this.onMediaPlayPrev();
- break;
- case 'zen-media-nexttrack-button':
- this.onMediaPlayNext();
- break;
- case 'zen-media-playpause-button':
- this.onMediaToggle();
- break;
- case 'zen-media-mute-mic-button':
- this.onMicrophoneMuteToggle();
- break;
- case 'zen-media-mute-camera-button':
- this.onCameraMuteToggle();
- break;
- }
- });
+const lazy = {};
+XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ 'RESPECT_PIP_DISABLED',
+ 'media.videocontrols.picture-in-picture.respect-disablePictureInPicture',
+ true
+);
+
+class nsZenMediaController {
+ _currentMediaController = null;
+ _currentBrowser = null;
+ _mediaUpdateInterval = null;
+
+ mediaTitle = null;
+ mediaArtist = null;
+ mediaControlBar = null;
+ mediaProgressBar = null;
+ mediaCurrentTime = null;
+ mediaDuration = null;
+ mediaFocusButton = null;
+ mediaProgressBarContainer = null;
+
+ supportedKeys = ['playpause', 'previoustrack', 'nexttrack'];
+ mediaControllersMap = new Map();
+
+ _tabTimeout = null;
+ _controllerSwitchTimeout = null;
+
+ #isSeeking = false;
+
+ init() {
+ if (!Services.prefs.getBoolPref('zen.mediacontrols.enabled', true)) return;
+
+ this.mediaTitle = document.querySelector('#zen-media-title');
+ this.mediaArtist = document.querySelector('#zen-media-artist');
+ this.mediaControlBar = document.querySelector('#zen-media-controls-toolbar');
+ this.mediaProgressBar = document.querySelector('#zen-media-progress-bar');
+ this.mediaCurrentTime = document.querySelector('#zen-media-current-time');
+ this.mediaDuration = document.querySelector('#zen-media-duration');
+ this.mediaFocusButton = document.querySelector('#zen-media-focus-button');
+ this.mediaProgressBarContainer = document.querySelector('#zen-media-progress-hbox');
+
+ this.onPositionstateChange = this._onPositionstateChange.bind(this);
+ this.onPlaybackstateChange = this._onPlaybackstateChange.bind(this);
+ this.onSupportedKeysChange = this._onSupportedKeysChange.bind(this);
+ this.onMetadataChange = this._onMetadataChange.bind(this);
+ this.onDeactivated = this._onDeactivated.bind(this);
+ this.onPipModeChange = this._onPictureInPictureModeChange.bind(this);
+
+ this.#initEventListeners();
+ }
- this.mediaProgressBar.addEventListener('input', this.onMediaSeekDrag.bind(this));
- this.mediaProgressBar.addEventListener('change', this.onMediaSeekComplete.bind(this));
+ #initEventListeners() {
+ this.mediaControlBar.addEventListener('mousedown', (event) => {
+ if (event.target.closest(':is(toolbarbutton,#zen-media-progress-hbox)')) return;
+ else this.onMediaFocus();
+ });
+
+ this.mediaControlBar.addEventListener('command', (event) => {
+ const button = event.target.closest('toolbarbutton');
+ if (!button) return;
+ switch (button.id) {
+ case 'zen-media-pip-button':
+ this.onMediaPip();
+ break;
+ case 'zen-media-close-button':
+ this.onControllerClose();
+ break;
+ case 'zen-media-focus-button':
+ this.onMediaFocus();
+ break;
+ case 'zen-media-mute-button':
+ this.onMediaMute();
+ break;
+ case 'zen-media-previoustrack-button':
+ this.onMediaPlayPrev();
+ break;
+ case 'zen-media-nexttrack-button':
+ this.onMediaPlayNext();
+ break;
+ case 'zen-media-playpause-button':
+ this.onMediaToggle();
+ break;
+ case 'zen-media-mute-mic-button':
+ this.onMicrophoneMuteToggle();
+ break;
+ case 'zen-media-mute-camera-button':
+ this.onCameraMuteToggle();
+ break;
+ }
+ });
- window.addEventListener('TabSelect', (event) => {
- if (this.isSharing) return;
+ this.mediaProgressBar.addEventListener('input', this.onMediaSeekDrag.bind(this));
+ this.mediaProgressBar.addEventListener('change', this.onMediaSeekComplete.bind(this));
- const linkedBrowser = event.target.linkedBrowser;
- this.switchController();
+ window.addEventListener('TabSelect', (event) => {
+ if (this.isSharing) return;
- if (this._currentBrowser) {
- if (linkedBrowser.browserId === this._currentBrowser.browserId) {
- if (this._tabTimeout) {
- clearTimeout(this._tabTimeout);
- this._tabTimeout = null;
- }
+ const linkedBrowser = event.target.linkedBrowser;
+ this.switchController();
- this.hideMediaControls();
- } else {
- this._tabTimeout = setTimeout(() => {
- if (!this.mediaControlBar.hasAttribute('pip')) this.showMediaControls();
- else this._tabTimeout = null;
- }, 500);
+ if (this._currentBrowser) {
+ if (linkedBrowser.browserId === this._currentBrowser.browserId) {
+ if (this._tabTimeout) {
+ clearTimeout(this._tabTimeout);
+ this._tabTimeout = null;
}
- }
- });
- const onTabDiscardedOrClosed = this.onTabDiscardedOrClosed.bind(this);
-
- window.addEventListener('TabClose', onTabDiscardedOrClosed);
- window.addEventListener('TabBrowserDiscarded', onTabDiscardedOrClosed);
-
- window.addEventListener('DOMAudioPlaybackStarted', (event) => {
- setTimeout(() => {
- if (
- this._currentMediaController?.isPlaying &&
- this.mediaControlBar.hasAttribute('hidden') &&
- !this.mediaControlBar.hasAttribute('pip')
- ) {
- const { selectedBrowser } = gBrowser;
- if (selectedBrowser.browserId !== this._currentBrowser.browserId) {
- this.showMediaControls();
- }
+ this.hideMediaControls();
+ } else {
+ this._tabTimeout = setTimeout(() => {
+ if (!this.mediaControlBar.hasAttribute('pip')) this.showMediaControls();
+ else this._tabTimeout = null;
+ }, 500);
+ }
+ }
+ });
+
+ const onTabDiscardedOrClosed = this.onTabDiscardedOrClosed.bind(this);
+
+ window.addEventListener('TabClose', onTabDiscardedOrClosed);
+ window.addEventListener('TabBrowserDiscarded', onTabDiscardedOrClosed);
+
+ window.addEventListener('DOMAudioPlaybackStarted', (event) => {
+ setTimeout(() => {
+ if (
+ this._currentMediaController?.isPlaying &&
+ this.mediaControlBar.hasAttribute('hidden') &&
+ !this.mediaControlBar.hasAttribute('pip')
+ ) {
+ const { selectedBrowser } = gBrowser;
+ if (selectedBrowser.browserId !== this._currentBrowser.browserId) {
+ this.showMediaControls();
}
- }, 1000);
+ }
+ }, 1000);
- this.activateMediaControls(event.target.browsingContext.mediaController, event.target);
- });
+ this.activateMediaControls(event.target.browsingContext.mediaController, event.target);
+ });
- window.addEventListener('DOMAudioPlaybackStopped', () => this.updateMuteState());
- }
+ window.addEventListener('DOMAudioPlaybackStopped', () => this.updateMuteState());
+ }
- onTabDiscardedOrClosed(event) {
- const { linkedBrowser } = event.target;
- const isCurrentBrowser = linkedBrowser?.browserId === this._currentBrowser?.browserId;
+ onTabDiscardedOrClosed(event) {
+ const { linkedBrowser } = event.target;
+ const isCurrentBrowser = linkedBrowser?.browserId === this._currentBrowser?.browserId;
- if (isCurrentBrowser) {
- this.isSharing = false;
- this.hideMediaControls();
- }
+ if (isCurrentBrowser) {
+ this.isSharing = false;
+ this.hideMediaControls();
+ }
- if (linkedBrowser?.browsingContext?.mediaController) {
- this.deinitMediaController(
- linkedBrowser.browsingContext.mediaController,
- true,
- isCurrentBrowser,
- true
- );
- }
+ if (linkedBrowser?.browsingContext?.mediaController) {
+ this.deinitMediaController(
+ linkedBrowser.browsingContext.mediaController,
+ true,
+ isCurrentBrowser,
+ true
+ );
}
+ }
- async deinitMediaController(
- mediaController,
- shouldForget = true,
- shouldOverride = true,
- shouldHide = true
- ) {
- if (shouldForget && mediaController) {
- mediaController.removeEventListener('pictureinpicturemodechange', this.onPipModeChange);
- mediaController.removeEventListener('positionstatechange', this.onPositionstateChange);
- mediaController.removeEventListener('playbackstatechange', this.onPlaybackstateChange);
- mediaController.removeEventListener('supportedkeyschange', this.onSupportedKeysChange);
- mediaController.removeEventListener('metadatachange', this.onMetadataChange);
- mediaController.removeEventListener('deactivated', this.onDeactivated);
-
- this.mediaControllersMap.delete(mediaController.id);
- }
+ async deinitMediaController(
+ mediaController,
+ shouldForget = true,
+ shouldOverride = true,
+ shouldHide = true
+ ) {
+ if (shouldForget && mediaController) {
+ mediaController.removeEventListener('pictureinpicturemodechange', this.onPipModeChange);
+ mediaController.removeEventListener('positionstatechange', this.onPositionstateChange);
+ mediaController.removeEventListener('playbackstatechange', this.onPlaybackstateChange);
+ mediaController.removeEventListener('supportedkeyschange', this.onSupportedKeysChange);
+ mediaController.removeEventListener('metadatachange', this.onMetadataChange);
+ mediaController.removeEventListener('deactivated', this.onDeactivated);
- if (shouldOverride) {
- this._currentMediaController = null;
- this._currentBrowser = null;
+ this.mediaControllersMap.delete(mediaController.id);
+ }
- if (this._mediaUpdateInterval) {
- clearInterval(this._mediaUpdateInterval);
- this._mediaUpdateInterval = null;
- }
+ if (shouldOverride) {
+ this._currentMediaController = null;
+ this._currentBrowser = null;
- if (shouldHide) await this.hideMediaControls();
- this.mediaControlBar.removeAttribute('muted');
- this.mediaControlBar.classList.remove('playing');
+ if (this._mediaUpdateInterval) {
+ clearInterval(this._mediaUpdateInterval);
+ this._mediaUpdateInterval = null;
}
- }
- get isSharing() {
- return this.mediaControlBar.hasAttribute('media-sharing');
+ if (shouldHide) await this.hideMediaControls();
+ this.mediaControlBar.removeAttribute('muted');
+ this.mediaControlBar.classList.remove('playing');
}
+ }
- set isSharing(value) {
- if (this._currentBrowser?.browsingContext && !value) {
- const webRTC = this._currentBrowser.browsingContext.currentWindowGlobal.getActor('WebRTC');
- webRTC.sendAsyncMessage('webrtc:UnmuteMicrophone');
- webRTC.sendAsyncMessage('webrtc:UnmuteCamera');
- }
+ get isSharing() {
+ return this.mediaControlBar.hasAttribute('media-sharing');
+ }
- if (!value) {
- this.mediaControlBar.removeAttribute('mic-muted');
- this.mediaControlBar.removeAttribute('camera-muted');
- } else {
- this.mediaControlBar.setAttribute('media-position-hidden', '');
- this.mediaControlBar.setAttribute('media-sharing', '');
- }
+ set isSharing(value) {
+ if (this._currentBrowser?.browsingContext && !value) {
+ const webRTC = this._currentBrowser.browsingContext.currentWindowGlobal.getActor('WebRTC');
+ webRTC.sendAsyncMessage('webrtc:UnmuteMicrophone');
+ webRTC.sendAsyncMessage('webrtc:UnmuteCamera');
}
- hideMediaControls() {
- if (this.mediaControlBar.hasAttribute('hidden')) return;
-
- return gZenUIManager.motion
- .animate(
- this.mediaControlBar,
- {
- opacity: [1, 0],
- y: [0, 10],
- },
- {
- duration: 0.1,
- }
- )
- .then(() => {
- this.mediaControlBar.setAttribute('hidden', 'true');
- this.mediaControlBar.removeAttribute('media-sharing');
- gZenUIManager.updateTabsToolbar();
- });
+ if (!value) {
+ this.mediaControlBar.removeAttribute('mic-muted');
+ this.mediaControlBar.removeAttribute('camera-muted');
+ } else {
+ this.mediaControlBar.setAttribute('media-position-hidden', '');
+ this.mediaControlBar.setAttribute('media-sharing', '');
}
+ }
- showMediaControls() {
- if (!this.mediaControlBar.hasAttribute('hidden')) return;
-
- if (!this.isSharing) {
- if (!this._currentMediaController) return;
- if (this._currentMediaController.isBeingUsedInPIPModeOrFullscreen)
- return this.hideMediaControls();
-
- this.updatePipButton();
- }
-
- const mediaInfoElements = [this.mediaTitle, this.mediaArtist];
- for (const element of mediaInfoElements) {
- element.removeAttribute('overflow'); // So we can properly recalculate the overflow
- }
-
- this.mediaControlBar.removeAttribute('hidden');
- window.requestAnimationFrame(() => {
- this.mediaControlBar.style.height =
- this.mediaControlBar.querySelector('toolbaritem').getBoundingClientRect().height + 'px';
- this.mediaControlBar.style.opacity = 0;
+ hideMediaControls() {
+ if (this.mediaControlBar.hasAttribute('hidden')) return;
+
+ return gZenUIManager.motion
+ .animate(
+ this.mediaControlBar,
+ {
+ opacity: [1, 0],
+ y: [0, 10],
+ },
+ {
+ duration: 0.1,
+ }
+ )
+ .then(() => {
+ this.mediaControlBar.setAttribute('hidden', 'true');
+ this.mediaControlBar.removeAttribute('media-sharing');
gZenUIManager.updateTabsToolbar();
- gZenUIManager.motion.animate(
- this.mediaControlBar,
- {
- opacity: [0, 1],
- y: [10, 0],
- },
- {}
- );
- this.addLabelOverflows(mediaInfoElements);
});
- }
+ }
- addLabelOverflows(elements) {
- for (const element of elements) {
- const parent = element.parentElement;
- if (element.scrollWidth > parent.clientWidth) {
- element.setAttribute('overflow', '');
- } else {
- element.removeAttribute('overflow');
- }
- }
- }
+ showMediaControls() {
+ if (!this.mediaControlBar.hasAttribute('hidden')) return;
- setupMediaController(mediaController, browser) {
- this._currentMediaController = mediaController;
- this._currentBrowser = browser;
+ if (!this.isSharing) {
+ if (!this._currentMediaController) return;
+ if (this._currentMediaController.isBeingUsedInPIPModeOrFullscreen)
+ return this.hideMediaControls();
this.updatePipButton();
}
- setupMediaControlUI(metadata, positionState) {
- this.updatePipButton();
+ const mediaInfoElements = [this.mediaTitle, this.mediaArtist];
+ for (const element of mediaInfoElements) {
+ element.removeAttribute('overflow'); // So we can properly recalculate the overflow
+ }
+
+ this.mediaControlBar.removeAttribute('hidden');
+ window.requestAnimationFrame(() => {
+ this.mediaControlBar.style.height =
+ this.mediaControlBar.querySelector('toolbaritem').getBoundingClientRect().height + 'px';
+ this.mediaControlBar.style.opacity = 0;
+ gZenUIManager.updateTabsToolbar();
+ gZenUIManager.motion.animate(
+ this.mediaControlBar,
+ {
+ opacity: [0, 1],
+ y: [10, 0],
+ },
+ {}
+ );
+ this.addLabelOverflows(mediaInfoElements);
+ });
+ }
- if (
- !this.mediaControlBar.classList.contains('playing') &&
- this._currentMediaController.isPlaying
- ) {
- this.mediaControlBar.classList.add('playing');
+ addLabelOverflows(elements) {
+ for (const element of elements) {
+ const parent = element.parentElement;
+ if (element.scrollWidth > parent.clientWidth) {
+ element.setAttribute('overflow', '');
+ } else {
+ element.removeAttribute('overflow');
}
+ }
+ }
- const iconURL =
- this._currentBrowser.mIconURL || `page-icon:${this._currentBrowser.currentURI.spec}`;
- this.mediaFocusButton.style.listStyleImage = `url(${iconURL})`;
+ setupMediaController(mediaController, browser) {
+ this._currentMediaController = mediaController;
+ this._currentBrowser = browser;
- this.mediaTitle.textContent = metadata.title || '';
- this.mediaArtist.textContent = metadata.artist || '';
+ this.updatePipButton();
+ }
- gZenUIManager.updateTabsToolbar();
+ setupMediaControlUI(metadata, positionState) {
+ this.updatePipButton();
- this._currentPosition = positionState.position;
- this._currentDuration = positionState.duration;
- this._currentPlaybackRate = positionState.playbackRate;
+ if (
+ !this.mediaControlBar.classList.contains('playing') &&
+ this._currentMediaController.isPlaying
+ ) {
+ this.mediaControlBar.classList.add('playing');
+ }
- this.updateMediaPosition();
+ const iconURL =
+ this._currentBrowser.mIconURL || `page-icon:${this._currentBrowser.currentURI.spec}`;
+ this.mediaFocusButton.style.listStyleImage = `url(${iconURL})`;
- for (const key of this.supportedKeys) {
- const button = this.mediaControlBar.querySelector(`#zen-media-${key}-button`);
- button.disabled = !this._currentMediaController.supportedKeys.includes(key);
- }
- }
+ this.mediaTitle.textContent = metadata.title || '';
+ this.mediaArtist.textContent = metadata.artist || '';
- activateMediaControls(mediaController, browser) {
- this.updateMuteState();
- this.switchController();
+ gZenUIManager.updateTabsToolbar();
- if (!mediaController.isActive || this._currentBrowser?.browserId === browser.browserId)
- return;
-
- const metadata = mediaController.getMetadata();
- const positionState = mediaController.getPositionState();
- this.mediaControllersMap.set(mediaController.id, {
- controller: mediaController,
- browser,
- position: positionState.position,
- duration: positionState.duration,
- playbackRate: positionState.playbackRate,
- lastUpdated: Date.now(),
- });
+ this._currentPosition = positionState.position;
+ this._currentDuration = positionState.duration;
+ this._currentPlaybackRate = positionState.playbackRate;
- if (!this._currentBrowser && !this.isSharing) {
- this.setupMediaController(mediaController, browser);
- this.setupMediaControlUI(metadata, positionState);
- }
+ this.updateMediaPosition();
- mediaController.addEventListener('pictureinpicturemodechange', this.onPipModeChange);
- mediaController.addEventListener('positionstatechange', this.onPositionstateChange);
- mediaController.addEventListener('playbackstatechange', this.onPlaybackstateChange);
- mediaController.addEventListener('supportedkeyschange', this.onSupportedKeysChange);
- mediaController.addEventListener('metadatachange', this.onMetadataChange);
- mediaController.addEventListener('deactivated', this.onDeactivated);
+ for (const key of this.supportedKeys) {
+ const button = this.mediaControlBar.querySelector(`#zen-media-${key}-button`);
+ button.disabled = !this._currentMediaController.supportedKeys.includes(key);
}
+ }
- activateMediaDeviceControls(browser) {
- if (browser?.browsingContext.currentWindowGlobal.hasActivePeerConnections()) {
- this.mediaControlBar.removeAttribute('can-pip');
- this._currentBrowser = browser;
+ activateMediaControls(mediaController, browser) {
+ this.updateMuteState();
+ this.switchController();
+
+ if (!mediaController.isActive || this._currentBrowser?.browserId === browser.browserId) return;
+
+ const metadata = mediaController.getMetadata();
+ const positionState = mediaController.getPositionState();
+ this.mediaControllersMap.set(mediaController.id, {
+ controller: mediaController,
+ browser,
+ position: positionState.position,
+ duration: positionState.duration,
+ playbackRate: positionState.playbackRate,
+ lastUpdated: Date.now(),
+ });
+
+ if (!this._currentBrowser && !this.isSharing) {
+ this.setupMediaController(mediaController, browser);
+ this.setupMediaControlUI(metadata, positionState);
+ }
+
+ mediaController.addEventListener('pictureinpicturemodechange', this.onPipModeChange);
+ mediaController.addEventListener('positionstatechange', this.onPositionstateChange);
+ mediaController.addEventListener('playbackstatechange', this.onPlaybackstateChange);
+ mediaController.addEventListener('supportedkeyschange', this.onSupportedKeysChange);
+ mediaController.addEventListener('metadatachange', this.onMetadataChange);
+ mediaController.addEventListener('deactivated', this.onDeactivated);
+ }
- const tab = window.gBrowser.getTabForBrowser(browser);
- const iconURL = browser.mIconURL || `page-icon:${browser.currentURI.spec}`;
+ activateMediaDeviceControls(browser) {
+ if (browser?.browsingContext.currentWindowGlobal.hasActivePeerConnections()) {
+ this.mediaControlBar.removeAttribute('can-pip');
+ this._currentBrowser = browser;
- this.isSharing = true;
+ const tab = window.gBrowser.getTabForBrowser(browser);
+ const iconURL = browser.mIconURL || `page-icon:${browser.currentURI.spec}`;
- this.mediaFocusButton.style.listStyleImage = `url(${iconURL})`;
- this.mediaTitle.textContent = tab.label;
- this.mediaArtist.textContent = '';
+ this.isSharing = true;
- this.showMediaControls();
- }
+ this.mediaFocusButton.style.listStyleImage = `url(${iconURL})`;
+ this.mediaTitle.textContent = tab.label;
+ this.mediaArtist.textContent = '';
+
+ this.showMediaControls();
}
+ }
- updateMediaSharing(data) {
- const { windowId, showCameraIndicator, showMicrophoneIndicator } = data;
-
- for (const browser of window.gBrowser.browsers) {
- const isMatch = browser.innerWindowID === windowId;
- const isCurrentBrowser = this._currentBrowser?.browserId === browser.browserId;
- const shouldShow = showCameraIndicator || showMicrophoneIndicator;
-
- if (!isMatch) continue;
- if (shouldShow && !(isCurrentBrowser && this.isSharing)) {
- const webRTC = browser.browsingContext.currentWindowGlobal.getActor('WebRTC');
- webRTC.sendAsyncMessage('webrtc:UnmuteMicrophone');
- webRTC.sendAsyncMessage('webrtc:UnmuteCamera');
-
- if (this._currentBrowser) this.isSharing = false;
- if (this._currentMediaController) {
- this._currentMediaController.pause();
- this.deinitMediaController(this._currentMediaController, true, true).then(() =>
- this.activateMediaDeviceControls(browser)
- );
- } else this.activateMediaDeviceControls(browser);
- } else if (!shouldShow && isCurrentBrowser && this.isSharing) {
- this.isSharing = false;
- this._currentBrowser = null;
- this.hideMediaControls();
- }
+ updateMediaSharing(data) {
+ const { windowId, showCameraIndicator, showMicrophoneIndicator } = data;
- break;
- }
- }
+ for (const browser of window.gBrowser.browsers) {
+ const isMatch = browser.innerWindowID === windowId;
+ const isCurrentBrowser = this._currentBrowser?.browserId === browser.browserId;
+ const shouldShow = showCameraIndicator || showMicrophoneIndicator;
- _onDeactivated(event) {
- this.deinitMediaController(
- event.target,
- true,
- event.target.id === this._currentMediaController.id,
- true
- );
- this.switchController();
- }
+ if (!isMatch) continue;
+ if (shouldShow && !(isCurrentBrowser && this.isSharing)) {
+ const webRTC = browser.browsingContext.currentWindowGlobal.getActor('WebRTC');
+ webRTC.sendAsyncMessage('webrtc:UnmuteMicrophone');
+ webRTC.sendAsyncMessage('webrtc:UnmuteCamera');
- _onPlaybackstateChange() {
- if (this._currentMediaController?.isPlaying) {
- this.mediaControlBar.classList.add('playing');
- } else {
- this.switchController();
- this.mediaControlBar.classList.remove('playing');
+ if (this._currentBrowser) this.isSharing = false;
+ if (this._currentMediaController) {
+ this._currentMediaController.pause();
+ this.deinitMediaController(this._currentMediaController, true, true).then(() =>
+ this.activateMediaDeviceControls(browser)
+ );
+ } else this.activateMediaDeviceControls(browser);
+ } else if (!shouldShow && isCurrentBrowser && this.isSharing) {
+ this.isSharing = false;
+ this._currentBrowser = null;
+ this.hideMediaControls();
}
- }
- _onSupportedKeysChange(event) {
- if (event.target.id !== this._currentMediaController?.id) return;
- for (const key of this.supportedKeys) {
- const button = this.mediaControlBar.querySelector(`#zen-media-${key}-button`);
- button.disabled = !event.target.supportedKeys.includes(key);
- }
+ break;
}
+ }
- _onPositionstateChange(event) {
- const mediaController = this.mediaControllersMap.get(event.target.id);
- this.mediaControllersMap.set(event.target.id, {
- ...mediaController,
- position: event.position,
- duration: event.duration,
- playbackRate: event.playbackRate,
- lastUpdated: Date.now(),
- });
-
- if (event.target.id !== this._currentMediaController?.id) return;
+ _onDeactivated(event) {
+ this.deinitMediaController(
+ event.target,
+ true,
+ event.target.id === this._currentMediaController.id,
+ true
+ );
+ this.switchController();
+ }
- this._currentPosition = event.position;
- this._currentDuration = event.duration;
- this._currentPlaybackRate = event.playbackRate;
+ _onPlaybackstateChange() {
+ if (this._currentMediaController?.isPlaying) {
+ this.mediaControlBar.classList.add('playing');
+ } else {
+ this.switchController();
+ this.mediaControlBar.classList.remove('playing');
+ }
+ }
- this.updateMediaPosition();
+ _onSupportedKeysChange(event) {
+ if (event.target.id !== this._currentMediaController?.id) return;
+ for (const key of this.supportedKeys) {
+ const button = this.mediaControlBar.querySelector(`#zen-media-${key}-button`);
+ button.disabled = !event.target.supportedKeys.includes(key);
}
+ }
- switchController(force = false) {
- let timeout = 3000;
+ _onPositionstateChange(event) {
+ const mediaController = this.mediaControllersMap.get(event.target.id);
+ this.mediaControllersMap.set(event.target.id, {
+ ...mediaController,
+ position: event.position,
+ duration: event.duration,
+ playbackRate: event.playbackRate,
+ lastUpdated: Date.now(),
+ });
- if (this.isSharing) return;
- if (this.#isSeeking) return;
+ if (event.target.id !== this._currentMediaController?.id) return;
- if (this._controllerSwitchTimeout) {
- clearTimeout(this._controllerSwitchTimeout);
- this._controllerSwitchTimeout = null;
- }
+ this._currentPosition = event.position;
+ this._currentDuration = event.duration;
+ this._currentPlaybackRate = event.playbackRate;
- if (this.mediaControllersMap.size === 1) timeout = 0;
- this._controllerSwitchTimeout = setTimeout(() => {
- if (!this._currentMediaController?.isPlaying || force) {
- const nextController = Array.from(this.mediaControllersMap.values())
- .filter(
- (ctrl) =>
- ctrl.controller.isPlaying &&
- gBrowser.selectedBrowser.browserId !== ctrl.browser.browserId &&
- ctrl.controller.id !== this._currentMediaController?.id
- )
- .sort((a, b) => b.lastUpdated - a.lastUpdated)
- .shift();
-
- if (nextController) {
- this.deinitMediaController(this._currentMediaController, false, true).then(() => {
- this.setupMediaController(nextController.controller, nextController.browser);
- const elapsedTime = Math.floor((Date.now() - nextController.lastUpdated) / 1000);
-
- this.setupMediaControlUI(nextController.controller.getMetadata(), {
- position:
- nextController.position + (nextController.controller.isPlaying ? elapsedTime : 0),
- duration: nextController.duration,
- playbackRate: nextController.playbackRate,
- });
-
- this.showMediaControls();
+ this.updateMediaPosition();
+ }
+
+ switchController(force = false) {
+ let timeout = 3000;
+
+ if (this.isSharing) return;
+ if (this.#isSeeking) return;
+
+ if (this._controllerSwitchTimeout) {
+ clearTimeout(this._controllerSwitchTimeout);
+ this._controllerSwitchTimeout = null;
+ }
+
+ if (this.mediaControllersMap.size === 1) timeout = 0;
+ this._controllerSwitchTimeout = setTimeout(() => {
+ if (!this._currentMediaController?.isPlaying || force) {
+ const nextController = Array.from(this.mediaControllersMap.values())
+ .filter(
+ (ctrl) =>
+ ctrl.controller.isPlaying &&
+ gBrowser.selectedBrowser.browserId !== ctrl.browser.browserId &&
+ ctrl.controller.id !== this._currentMediaController?.id
+ )
+ .sort((a, b) => b.lastUpdated - a.lastUpdated)
+ .shift();
+
+ if (nextController) {
+ this.deinitMediaController(this._currentMediaController, false, true).then(() => {
+ this.setupMediaController(nextController.controller, nextController.browser);
+ const elapsedTime = Math.floor((Date.now() - nextController.lastUpdated) / 1000);
+
+ this.setupMediaControlUI(nextController.controller.getMetadata(), {
+ position:
+ nextController.position + (nextController.controller.isPlaying ? elapsedTime : 0),
+ duration: nextController.duration,
+ playbackRate: nextController.playbackRate,
});
- }
+
+ this.showMediaControls();
+ });
}
+ }
+
+ this._controllerSwitchTimeout = null;
+ }, timeout);
+ }
- this._controllerSwitchTimeout = null;
- }, timeout);
+ updateMediaPosition() {
+ if (this._mediaUpdateInterval) {
+ clearInterval(this._mediaUpdateInterval);
+ this._mediaUpdateInterval = null;
}
- updateMediaPosition() {
- if (this._mediaUpdateInterval) {
+ if (this._currentDuration >= 900_000)
+ return this.mediaControlBar.setAttribute('media-position-hidden', 'true');
+ else this.mediaControlBar.removeAttribute('media-position-hidden');
+
+ if (!this._currentDuration) return;
+
+ this.mediaCurrentTime.textContent = this.formatSecondsToTime(this._currentPosition);
+ this.mediaDuration.textContent = this.formatSecondsToTime(this._currentDuration);
+ this.mediaProgressBar.value = (this._currentPosition / this._currentDuration) * 100;
+
+ this._mediaUpdateInterval = setInterval(() => {
+ if (this._currentMediaController?.isPlaying) {
+ this._currentPosition += 1 * this._currentPlaybackRate;
+ if (this._currentPosition > this._currentDuration) {
+ this._currentPosition = this._currentDuration;
+ }
+ this.mediaCurrentTime.textContent = this.formatSecondsToTime(this._currentPosition);
+ this.mediaProgressBar.value = (this._currentPosition / this._currentDuration) * 100;
+ } else {
clearInterval(this._mediaUpdateInterval);
this._mediaUpdateInterval = null;
}
+ }, 1000);
+ }
- if (this._currentDuration >= 900_000)
- return this.mediaControlBar.setAttribute('media-position-hidden', 'true');
- else this.mediaControlBar.removeAttribute('media-position-hidden');
-
- if (!this._currentDuration) return;
+ formatSecondsToTime(seconds) {
+ if (!seconds || isNaN(seconds)) return '0:00';
- this.mediaCurrentTime.textContent = this.formatSecondsToTime(this._currentPosition);
- this.mediaDuration.textContent = this.formatSecondsToTime(this._currentDuration);
- this.mediaProgressBar.value = (this._currentPosition / this._currentDuration) * 100;
+ const totalSeconds = Math.max(0, Math.ceil(seconds));
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds % 3600) / 60).toString();
+ const secs = (totalSeconds % 60).toString();
- this._mediaUpdateInterval = setInterval(() => {
- if (this._currentMediaController?.isPlaying) {
- this._currentPosition += 1 * this._currentPlaybackRate;
- if (this._currentPosition > this._currentDuration) {
- this._currentPosition = this._currentDuration;
- }
- this.mediaCurrentTime.textContent = this.formatSecondsToTime(this._currentPosition);
- this.mediaProgressBar.value = (this._currentPosition / this._currentDuration) * 100;
- } else {
- clearInterval(this._mediaUpdateInterval);
- this._mediaUpdateInterval = null;
- }
- }, 1000);
+ if (hours > 0) {
+ return `${hours}:${minutes.padStart(2, '0')}:${secs.padStart(2, '0')}`;
}
- formatSecondsToTime(seconds) {
- if (!seconds || isNaN(seconds)) return '0:00';
+ return `${minutes}:${secs.padStart(2, '0')}`;
+ }
- const totalSeconds = Math.max(0, Math.ceil(seconds));
- const hours = Math.floor(totalSeconds / 3600);
- const minutes = Math.floor((totalSeconds % 3600) / 60).toString();
- const secs = (totalSeconds % 60).toString();
+ _onMetadataChange(event) {
+ if (event.target.id !== this._currentMediaController?.id) return;
+ this.updatePipButton();
- if (hours > 0) {
- return `${hours}:${minutes.padStart(2, '0')}:${secs.padStart(2, '0')}`;
- }
+ const metadata = event.target.getMetadata();
+ this.mediaTitle.textContent = metadata.title || '';
+ this.mediaArtist.textContent = metadata.artist || '';
- return `${minutes}:${secs.padStart(2, '0')}`;
+ const mediaInfoElements = [this.mediaTitle, this.mediaArtist];
+ for (const element of mediaInfoElements) {
+ element.removeAttribute('overflow');
}
- _onMetadataChange(event) {
- if (event.target.id !== this._currentMediaController?.id) return;
- this.updatePipButton();
-
- const metadata = event.target.getMetadata();
- this.mediaTitle.textContent = metadata.title || '';
- this.mediaArtist.textContent = metadata.artist || '';
+ this.addLabelOverflows(mediaInfoElements);
+ }
- const mediaInfoElements = [this.mediaTitle, this.mediaArtist];
- for (const element of mediaInfoElements) {
- element.removeAttribute('overflow');
+ _onPictureInPictureModeChange(event) {
+ if (event.target.id !== this._currentMediaController?.id) return;
+ if (event.target.isBeingUsedInPIPModeOrFullscreen) {
+ this.hideMediaControls();
+ this.mediaControlBar.setAttribute('pip', '');
+ } else {
+ const { selectedBrowser } = gBrowser;
+ if (selectedBrowser.browserId !== this._currentBrowser.browserId) {
+ this.showMediaControls();
}
- this.addLabelOverflows(mediaInfoElements);
+ this.mediaControlBar.removeAttribute('pip');
}
+ }
- _onPictureInPictureModeChange(event) {
- if (event.target.id !== this._currentMediaController?.id) return;
- if (event.target.isBeingUsedInPIPModeOrFullscreen) {
- this.hideMediaControls();
- this.mediaControlBar.setAttribute('pip', '');
- } else {
- const { selectedBrowser } = gBrowser;
- if (selectedBrowser.browserId !== this._currentBrowser.browserId) {
- this.showMediaControls();
- }
-
- this.mediaControlBar.removeAttribute('pip');
- }
+ onMediaPlayPrev() {
+ if (this._currentMediaController?.supportedKeys.includes('previoustrack')) {
+ this._currentMediaController.prevTrack();
}
+ }
- onMediaPlayPrev() {
- if (this._currentMediaController?.supportedKeys.includes('previoustrack')) {
- this._currentMediaController.prevTrack();
- }
+ onMediaPlayNext() {
+ if (this._currentMediaController?.supportedKeys.includes('nexttrack')) {
+ this._currentMediaController.nextTrack();
}
+ }
- onMediaPlayNext() {
- if (this._currentMediaController?.supportedKeys.includes('nexttrack')) {
- this._currentMediaController.nextTrack();
- }
- }
+ onMediaSeekDrag(event) {
+ this.#isSeeking = true;
- onMediaSeekDrag(event) {
- this.#isSeeking = true;
+ this._currentMediaController?.pause();
+ const newTime = (event.target.value / 100) * this._currentDuration;
+ this.mediaCurrentTime.textContent = this.formatSecondsToTime(newTime);
+ }
- this._currentMediaController?.pause();
- const newTime = (event.target.value / 100) * this._currentDuration;
- this.mediaCurrentTime.textContent = this.formatSecondsToTime(newTime);
+ onMediaSeekComplete(event) {
+ const newPosition = (event.target.value / 100) * this._currentDuration;
+ if (this._currentMediaController?.supportedKeys.includes('seekto')) {
+ this._currentMediaController.seekTo(newPosition);
+ this._currentMediaController.play();
}
- onMediaSeekComplete(event) {
- const newPosition = (event.target.value / 100) * this._currentDuration;
- if (this._currentMediaController?.supportedKeys.includes('seekto')) {
- this._currentMediaController.seekTo(newPosition);
- this._currentMediaController.play();
- }
-
- this.#isSeeking = false;
- }
+ this.#isSeeking = false;
+ }
- onMediaFocus() {
- if (!this._currentBrowser) return;
+ onMediaFocus() {
+ if (!this._currentBrowser) return;
- if (this._currentMediaController) this._currentMediaController.focus();
- else if (this._currentBrowser) {
- const tab = window.gBrowser.getTabForBrowser(this._currentBrowser);
- if (tab) window.gZenWorkspaces.switchTabIfNeeded(tab);
- }
+ if (this._currentMediaController) this._currentMediaController.focus();
+ else if (this._currentBrowser) {
+ const tab = window.gBrowser.getTabForBrowser(this._currentBrowser);
+ if (tab) window.gZenWorkspaces.switchTabIfNeeded(tab);
}
+ }
- onMediaMute() {
- const tab = window.gBrowser.getTabForBrowser(this._currentBrowser);
- if (tab) {
- tab.toggleMuteAudio();
- this.updateMuteState();
- }
+ onMediaMute() {
+ const tab = window.gBrowser.getTabForBrowser(this._currentBrowser);
+ if (tab) {
+ tab.toggleMuteAudio();
+ this.updateMuteState();
}
+ }
- onMediaToggle() {
- if (this.mediaControlBar.classList.contains('playing')) {
- this._currentMediaController?.pause();
- } else {
- this._currentMediaController?.play();
- }
+ onMediaToggle() {
+ if (this.mediaControlBar.classList.contains('playing')) {
+ this._currentMediaController?.pause();
+ } else {
+ this._currentMediaController?.play();
}
+ }
- onControllerClose() {
- if (this._currentMediaController) {
- this._currentMediaController.pause();
- this.deinitMediaController(this._currentMediaController);
- } else if (this.isSharing) this.isSharing = false;
+ onControllerClose() {
+ if (this._currentMediaController) {
+ this._currentMediaController.pause();
+ this.deinitMediaController(this._currentMediaController);
+ } else if (this.isSharing) this.isSharing = false;
- this.hideMediaControls();
- this.switchController(true);
- }
+ this.hideMediaControls();
+ this.switchController(true);
+ }
+
+ onMediaPip() {
+ this._currentBrowser.browsingContext.currentWindowGlobal
+ .getActor('PictureInPictureLauncher')
+ .sendAsyncMessage('PictureInPicture:KeyToggle');
+ }
+
+ onMicrophoneMuteToggle() {
+ if (this._currentBrowser) {
+ const shouldMute = this.mediaControlBar.hasAttribute('mic-muted')
+ ? 'webrtc:UnmuteMicrophone'
+ : 'webrtc:MuteMicrophone';
- onMediaPip() {
this._currentBrowser.browsingContext.currentWindowGlobal
- .getActor('PictureInPictureLauncher')
- .sendAsyncMessage('PictureInPicture:KeyToggle');
+ .getActor('WebRTC')
+ .sendAsyncMessage(shouldMute);
+ this.mediaControlBar.toggleAttribute('mic-muted');
}
+ }
- onMicrophoneMuteToggle() {
- if (this._currentBrowser) {
- const shouldMute = this.mediaControlBar.hasAttribute('mic-muted')
- ? 'webrtc:UnmuteMicrophone'
- : 'webrtc:MuteMicrophone';
-
- this._currentBrowser.browsingContext.currentWindowGlobal
- .getActor('WebRTC')
- .sendAsyncMessage(shouldMute);
- this.mediaControlBar.toggleAttribute('mic-muted');
- }
- }
+ onCameraMuteToggle() {
+ if (this._currentBrowser) {
+ const shouldMute = this.mediaControlBar.hasAttribute('camera-muted')
+ ? 'webrtc:UnmuteCamera'
+ : 'webrtc:MuteCamera';
- onCameraMuteToggle() {
- if (this._currentBrowser) {
- const shouldMute = this.mediaControlBar.hasAttribute('camera-muted')
- ? 'webrtc:UnmuteCamera'
- : 'webrtc:MuteCamera';
-
- this._currentBrowser.browsingContext.currentWindowGlobal
- .getActor('WebRTC')
- .sendAsyncMessage(shouldMute);
- this.mediaControlBar.toggleAttribute('camera-muted');
- }
+ this._currentBrowser.browsingContext.currentWindowGlobal
+ .getActor('WebRTC')
+ .sendAsyncMessage(shouldMute);
+ this.mediaControlBar.toggleAttribute('camera-muted');
}
+ }
- updateMuteState() {
- if (!this._currentBrowser) return;
- this.mediaControlBar.toggleAttribute('muted', this._currentBrowser.audioMuted);
- }
+ updateMuteState() {
+ if (!this._currentBrowser) return;
+ this.mediaControlBar.toggleAttribute('muted', this._currentBrowser.audioMuted);
+ }
- updatePipButton() {
- if (!this._currentBrowser) return;
- if (this.isSharing) return;
+ updatePipButton() {
+ if (!this._currentBrowser) return;
+ if (this.isSharing) return;
- const { totalPipCount, totalPipDisabled } = PictureInPicture.getEligiblePipVideoCount(
- this._currentBrowser
- );
- const canPip = totalPipCount === 1 || (totalPipDisabled > 0 && lazy.RESPECT_PIP_DISABLED);
+ const { totalPipCount, totalPipDisabled } = PictureInPicture.getEligiblePipVideoCount(
+ this._currentBrowser
+ );
+ const canPip = totalPipCount === 1 || (totalPipDisabled > 0 && lazy.RESPECT_PIP_DISABLED);
- this.mediaControlBar.toggleAttribute('can-pip', canPip);
- }
+ this.mediaControlBar.toggleAttribute('can-pip', canPip);
}
-
- window.gZenMediaController = new nsZenMediaController();
}
+
+window.gZenMediaController = new nsZenMediaController();
diff --git a/src/zen/media/jar.inc.mn b/src/zen/media/jar.inc.mn
new file mode 100644
index 0000000000..41aa8b311e
--- /dev/null
+++ b/src/zen/media/jar.inc.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+ content/browser/zen-components/ZenMediaController.mjs (../../zen/media/ZenMediaController.mjs)
+ content/browser/zen-styles/zen-media-controls.css (../../zen/media/zen-media-controls.css)
diff --git a/src/zen/media/zen-media-controls.css b/src/zen/media/zen-media-controls.css
index e3e1e451f1..4358a1609c 100644
--- a/src/zen/media/zen-media-controls.css
+++ b/src/zen/media/zen-media-controls.css
@@ -9,8 +9,8 @@
--button-spacing: 2px;
display: flex;
- justify-content: space-between;
min-width: 0;
+ justify-content: space-between;
background: transparent;
container-type: inline-size;
@@ -151,11 +151,6 @@
}
& > toolbaritem {
- --zen-media-control-bg: color-mix(
- in srgb,
- var(--zen-primary-color) 15%,
- light-dark(white, black)
- );
flex-grow: 1;
padding: 0;
transition: padding 0.3s ease-out;
@@ -164,8 +159,8 @@
bottom: 0;
padding: 4px 6px;
border-radius: var(--border-radius-medium);
- box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
- background-color: var(--zen-media-control-bg);
+ box-shadow: var(--zen-sidebar-notification-shadow);
+ background-color: var(--zen-sidebar-notification-bg);
width: 100%;
}
@@ -239,14 +234,14 @@
overflow-x: visible;
white-space: nowrap;
/* Overflow inner box shadow from the left to simulate overflow */
- mask-image: linear-gradient(to left, transparent, var(--zen-media-control-bg) 0.6em);
+ mask-image: linear-gradient(to left, transparent, var(--zen-sidebar-notification-bg) 0.6em);
min-width: 1px;
&::before {
content: '';
position: absolute;
width: 0.6em;
- background: linear-gradient(to right, var(--zen-media-control-bg) 0%, transparent 100%);
+ background: linear-gradient(to right, var(--zen-sidebar-notification-bg) 0%, transparent 100%);
pointer-events: none;
top: 6px;
left: 0;
diff --git a/src/zen/mods/ZenMods.mjs b/src/zen/mods/ZenMods.mjs
index 762a2c8261..49eab0b31e 100644
--- a/src/zen/mods/ZenMods.mjs
+++ b/src/zen/mods/ZenMods.mjs
@@ -2,669 +2,660 @@
// 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 nsZenMods extends nsZenPreloadedFeature {
- // private properties start
- #kZenStylesheetModHeader = '/* Zen Mods - Generated by ZenMods.';
- #kZenStylesheetModHeaderBody = `* DO NOT EDIT THIS FILE DIRECTLY!
+import {
+ nsZenPreloadedFeature,
+ nsZenMultiWindowFeature,
+} from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
+
+class nsZenMods extends nsZenPreloadedFeature {
+ // private properties start
+ #kZenStylesheetModHeader = '/* Zen Mods - Generated by ZenMods.';
+ #kZenStylesheetModHeaderBody = `* DO NOT EDIT THIS FILE DIRECTLY!
* Your changes will be overwritten.
* Instead, go to the preferences and edit the mods there.
*/
`;
- #kZenStylesheetModFooter = `
+ #kZenStylesheetModFooter = `
/* End of Zen Mods */
`;
- #getCurrentDateTime = () =>
- new Intl.DateTimeFormat('en-US', {
- dateStyle: 'full',
- timeStyle: 'full',
- }).format(new Date().getTime());
-
- constructor() {
- super();
- }
+ #getCurrentDateTime = () =>
+ new Intl.DateTimeFormat('en-US', {
+ dateStyle: 'full',
+ timeStyle: 'full',
+ }).format(new Date().getTime());
+
+ constructor() {
+ super();
+ }
- // Stylesheet service
- #_modsBackend = null;
+ // Stylesheet service
+ #_modsBackend = null;
- get #modsBackend() {
- if (!this.#_modsBackend) {
- this.#_modsBackend = Cc['@mozilla.org/zen/mods-backend;1'].getService(Ci.nsIZenModsBackend);
- }
- return this.#_modsBackend;
+ get #modsBackend() {
+ if (!this.#_modsBackend) {
+ this.#_modsBackend = Cc['@mozilla.org/zen/mods-backend;1'].getService(Ci.nsIZenModsBackend);
}
+ return this.#_modsBackend;
+ }
- get #styleSheetPath() {
- return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes.css');
- }
+ get #styleSheetPath() {
+ return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes.css');
+ }
- async #handleDisableMods() {
- await this.#rebuildModsStylesheet();
- }
+ async #handleDisableMods() {
+ await this.#rebuildModsStylesheet();
+ }
- #getStylesheetPathForMod(mod) {
- return PathUtils.join(this.getModFolder(mod.id), 'chrome.css');
- }
+ #getStylesheetPathForMod(mod) {
+ return PathUtils.join(this.getModFolder(mod.id), 'chrome.css');
+ }
- async #readStylesheet() {
- const path = this.modsRootPath;
- if (!(await IOUtils.exists(path))) {
- return '';
- }
- return await IOUtils.readUTF8(this.#styleSheetPath);
+ async #readStylesheet() {
+ const path = this.modsRootPath;
+ if (!(await IOUtils.exists(path))) {
+ return '';
}
+ return await IOUtils.readUTF8(this.#styleSheetPath);
+ }
- async #insertStylesheet() {
- try {
- const content = await this.#readStylesheet();
- this.#modsBackend.rebuildModsStyles(content);
- } catch (e) {
- console.warn('[ZenMods]: Error rebuilding mods styles:', e);
- }
+ async #insertStylesheet() {
+ try {
+ const content = await this.#readStylesheet();
+ this.#modsBackend.rebuildModsStyles(content);
+ } catch (e) {
+ console.warn('[ZenMods]: Error rebuilding mods styles:', e);
}
+ }
- async #rebuildModsStylesheet() {
- const mods = await this.#getEnabledMods();
+ async #rebuildModsStylesheet() {
+ const mods = await this.#getEnabledMods();
- await this.#writeStylesheet(mods);
+ await this.#writeStylesheet(mods);
- const modsWithPreferences = await Promise.all(
- mods.map(async (mod) => {
- const preferences = await this.getModPreferences(mod);
+ const modsWithPreferences = await Promise.all(
+ mods.map(async (mod) => {
+ const preferences = await this.getModPreferences(mod);
- return {
- name: mod.name,
- enabled: mod.enabled,
- preferences,
- };
- })
- );
+ return {
+ name: mod.name,
+ enabled: mod.enabled,
+ preferences,
+ };
+ })
+ );
- this.#setDefaults(modsWithPreferences);
- this.#writeToDom(modsWithPreferences);
+ this.#setDefaults(modsWithPreferences);
+ this.#writeToDom(modsWithPreferences);
- await this.#insertStylesheet();
+ await this.#insertStylesheet();
+ }
+
+ async #getEnabledMods() {
+ if (Services.prefs.getBoolPref('zen.themes.disable-all', false)) {
+ console.log('[ZenMods]: Mods are disabled by user preference.');
+ return [];
}
+ const modsObject = await this.getMods();
+ const mods = Object.values(modsObject).filter(
+ (mod) => mod.enabled === undefined || mod.enabled
+ );
- async #getEnabledMods() {
- if (Services.prefs.getBoolPref('zen.themes.disable-all', false)) {
- console.log('[ZenMods]: Mods are disabled by user preference.');
- return [];
- }
- const modsObject = await this.getMods();
- const mods = Object.values(modsObject).filter(
- (mod) => mod.enabled === undefined || mod.enabled
- );
+ const modList = mods.map(({ name }) => name).join(', ');
- const modList = mods.map(({ name }) => name).join(', ');
+ const message =
+ modList !== ''
+ ? `[ZenMods]: Loading enabled Zen mods: ${modList}.`
+ : '[ZenMods]: No enabled Zen mods.';
- const message =
- modList !== ''
- ? `[ZenMods]: Loading enabled Zen mods: ${modList}.`
- : '[ZenMods]: No enabled Zen mods.';
+ console.log(message);
- console.log(message);
+ return mods;
+ }
- return mods;
- }
+ #setDefaults(modsWithPreferences) {
+ for (const { preferences, enabled } of modsWithPreferences) {
+ if (enabled !== undefined && !enabled) {
+ continue;
+ }
- #setDefaults(modsWithPreferences) {
- for (const { preferences, enabled } of modsWithPreferences) {
- if (enabled !== undefined && !enabled) {
+ for (const { type, property, defaultValue } of preferences) {
+ if (defaultValue === undefined) {
continue;
}
- for (const { type, property, defaultValue } of preferences) {
- if (defaultValue === undefined) {
- continue;
- }
-
- const getProperty =
- type === 'checkbox' ? Services.prefs.getBoolPref : Services.prefs.getStringPref;
- const setProperty =
- type === 'checkbox' ? Services.prefs.setBoolPref : Services.prefs.setStringPref;
-
- try {
- getProperty(property);
- } catch {
- console.debug(
- `[ZenMods]: Setting default value for ${property} to ${defaultValue} (${typeof defaultValue})`
- );
+ const getProperty =
+ type === 'checkbox' ? Services.prefs.getBoolPref : Services.prefs.getStringPref;
+ const setProperty =
+ type === 'checkbox' ? Services.prefs.setBoolPref : Services.prefs.setStringPref;
- if (
- typeof defaultValue !== 'boolean' &&
- typeof defaultValue !== 'string' &&
- typeof defaultValue !== 'number'
- ) {
- console.warn(
- `[ZenMods]: Warning, invalid data type received (${typeof defaultValue}), skipping.`
- );
- continue;
- }
+ try {
+ getProperty(property);
+ } catch {
+ console.debug(
+ `[ZenMods]: Setting default value for ${property} to ${defaultValue} (${typeof defaultValue})`
+ );
- setProperty(
- property,
- typeof defaultValue === 'boolean' ? defaultValue : defaultValue.toString()
+ if (
+ typeof defaultValue !== 'boolean' &&
+ typeof defaultValue !== 'string' &&
+ typeof defaultValue !== 'number'
+ ) {
+ console.warn(
+ `[ZenMods]: Warning, invalid data type received (${typeof defaultValue}), skipping.`
);
+ continue;
}
+
+ setProperty(
+ property,
+ typeof defaultValue === 'boolean' ? defaultValue : defaultValue.toString()
+ );
}
}
}
+ }
- #writeToDom(modsWithPreferences) {
- for (const browser of nsZenMultiWindowFeature.browsers) {
- for (const { enabled, preferences, name } of modsWithPreferences) {
- const sanitizedName = this.sanitizeModName(name);
-
- if (enabled !== undefined && !enabled) {
- const element = browser.document.getElementById(sanitizedName);
+ #writeToDom(modsWithPreferences) {
+ for (const browser of nsZenMultiWindowFeature.browsers) {
+ for (const { enabled, preferences, name } of modsWithPreferences) {
+ const sanitizedName = this.sanitizeModName(name);
- if (element) {
- element.remove();
- }
+ if (enabled !== undefined && !enabled) {
+ const element = browser.document.getElementById(sanitizedName);
- for (const { property } of preferences.filter(({ type }) => type !== 'checkbox')) {
- const sanitizedProperty = property?.replaceAll(/\./g, '-');
+ if (element) {
+ element.remove();
+ }
- browser.document
- .querySelector(':root')
- .style.removeProperty(`--${sanitizedProperty}`);
- }
+ for (const { property } of preferences.filter(({ type }) => type !== 'checkbox')) {
+ const sanitizedProperty = property?.replaceAll(/\./g, '-');
- continue;
+ browser.document.querySelector(':root').style.removeProperty(`--${sanitizedProperty}`);
}
- for (const { property, type } of preferences) {
- const value = Services.prefs.getStringPref(property, '');
- const sanitizedProperty = property?.replaceAll(/\./g, '-');
+ continue;
+ }
- switch (type) {
- case 'dropdown': {
- if (value !== '') {
- let element = browser.document.getElementById(sanitizedName);
+ for (const { property, type } of preferences) {
+ const value = Services.prefs.getStringPref(property, '');
+ const sanitizedProperty = property?.replaceAll(/\./g, '-');
- if (!element) {
- element = browser.document.createElement('div');
+ switch (type) {
+ case 'dropdown': {
+ if (value !== '') {
+ let element = browser.document.getElementById(sanitizedName);
- element.style.display = 'none';
- element.setAttribute('id', sanitizedName);
+ if (!element) {
+ element = browser.document.createElement('div');
- browser.document.body.appendChild(element);
- }
+ element.style.display = 'none';
+ element.setAttribute('id', sanitizedName);
- element.setAttribute(sanitizedProperty, value);
+ browser.document.body.appendChild(element);
}
- break;
+
+ element.setAttribute(sanitizedProperty, value);
}
+ break;
+ }
- case 'string': {
- if (value === '') {
- browser.document
- .querySelector(':root')
- .style.removeProperty(`--${sanitizedProperty}`);
- } else {
- browser.document
- .querySelector(':root')
- .style.setProperty(`--${sanitizedProperty}`, value);
- }
- break;
+ case 'string': {
+ if (value === '') {
+ browser.document
+ .querySelector(':root')
+ .style.removeProperty(`--${sanitizedProperty}`);
+ } else {
+ browser.document
+ .querySelector(':root')
+ .style.setProperty(`--${sanitizedProperty}`, value);
}
+ break;
}
}
}
}
}
+ }
- async #writeStylesheet(modList = []) {
- const mods = [];
+ async #writeStylesheet(modList = []) {
+ const mods = [];
+
+ for (let mod of modList) {
+ mod._filePath = this.#getStylesheetPathForMod(mod);
+ mods.push(mod);
+ }
- for (let mod of modList) {
- mod._filePath = this.#getStylesheetPathForMod(mod);
- mods.push(mod);
+ let content = this.#kZenStylesheetModHeader;
+ content += `\n* FILE GENERATED AT: ${this.#getCurrentDateTime()}\n`;
+ content += this.#kZenStylesheetModHeaderBody;
+
+ for (let mod of mods) {
+ if (mod.enabled !== undefined && !mod.enabled) {
+ continue;
}
- let content = this.#kZenStylesheetModHeader;
- content += `\n* FILE GENERATED AT: ${this.#getCurrentDateTime()}\n`;
- content += this.#kZenStylesheetModHeaderBody;
+ content += `\n/* Name: ${mod.name} */\n`;
+ content += `/* Description: ${mod.description} */\n`;
+ content += `/* Author: @${mod.author} */\n`;
- for (let mod of mods) {
- if (mod.enabled !== undefined && !mod.enabled) {
- continue;
- }
+ if (mod._readmeURL) {
+ content += `/* Readme: ${mod.readme} */\n`;
+ }
- content += `\n/* Name: ${mod.name} */\n`;
- content += `/* Description: ${mod.description} */\n`;
- content += `/* Author: @${mod.author} */\n`;
+ const chromeContent = await IOUtils.readUTF8(mod._filePath);
+ content += chromeContent;
+ }
- if (mod._readmeURL) {
- content += `/* Readme: ${mod.readme} */\n`;
- }
+ content += this.#kZenStylesheetModFooter;
- const chromeContent = await IOUtils.readUTF8(mod._filePath);
- content += chromeContent;
- }
+ const buffer = new TextEncoder().encode(content);
- content += this.#kZenStylesheetModFooter;
+ await IOUtils.write(this.#styleSheetPath, buffer);
+ }
- const buffer = new TextEncoder().encode(content);
+ #compareVersions(version1, version2) {
+ let result = false;
- await IOUtils.write(this.#styleSheetPath, buffer);
+ if (typeof version1 !== 'object') {
+ version1 = version1.toString().split('.');
}
- #compareVersions(version1, version2) {
- let result = false;
+ if (typeof version2 !== 'object') {
+ version2 = version2.toString().split('.');
+ }
- if (typeof version1 !== 'object') {
- version1 = version1.toString().split('.');
+ for (let i = 0; i < Math.max(version1.length, version2.length); i++) {
+ if (version1[i] == undefined) {
+ version1[i] = 0;
}
-
- if (typeof version2 !== 'object') {
- version2 = version2.toString().split('.');
+ if (version2[i] == undefined) {
+ version2[i] = 0;
}
-
- for (let i = 0; i < Math.max(version1.length, version2.length); i++) {
- if (version1[i] == undefined) {
- version1[i] = 0;
- }
- if (version2[i] == undefined) {
- version2[i] = 0;
- }
- if (Number(version1[i]) < Number(version2[i])) {
- result = true;
- break;
- }
- if (version1[i] != version2[i]) {
- break;
- }
+ if (Number(version1[i]) < Number(version2[i])) {
+ result = true;
+ break;
+ }
+ if (version1[i] != version2[i]) {
+ break;
}
- return result;
}
+ return result;
+ }
- #composeModApiUrl(modId) {
- // keeping theme here as it would require changes to CI to change the name
- return `https://zen-browser.github.io/theme-store/themes/${modId}/theme.json`;
- }
+ #composeModApiUrl(modId) {
+ // keeping theme here as it would require changes to CI to change the name
+ return `https://zen-browser.github.io/theme-store/themes/${modId}/theme.json`;
+ }
- /* eslint-disable no-unused-vars */
- async #downloadUrlToFile(url, path, isStyleSheet = false, maxRetries = 3, retryDelayMs = 500) {
- let attempt = 0;
+ /* eslint-disable no-unused-vars */
+ async #downloadUrlToFile(url, path, isStyleSheet = false, maxRetries = 3, retryDelayMs = 500) {
+ let attempt = 0;
- while (attempt < maxRetries) {
- try {
- const response = await fetch(url);
+ while (attempt < maxRetries) {
+ try {
+ const response = await fetch(url);
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status} for url: ${url}`);
- }
+ if (!response.ok) {
+ throw new Error(`HTTP error! status: ${response.status} for url: ${url}`);
+ }
- const data = await response.text();
+ const data = await response.text();
- // convert the data into a Uint8Array
- const buffer = new TextEncoder().encode(data);
- await IOUtils.write(path, buffer);
+ // convert the data into a Uint8Array
+ const buffer = new TextEncoder().encode(data);
+ await IOUtils.write(path, buffer);
- return; // to exit the loop
- } catch (e) {
- attempt++;
- if (attempt >= maxRetries) {
- console.error('[ZenMods]: Error downloading file after retries', url, e);
- } else {
- console.warn(
- `[ZenMods]: Download failed (attempt ${attempt} of ${maxRetries}), retrying in ${retryDelayMs}ms...`,
- url,
- e
- );
- await new Promise((res) => setTimeout(res, retryDelayMs));
- }
+ return; // to exit the loop
+ } catch (e) {
+ attempt++;
+ if (attempt >= maxRetries) {
+ console.error('[ZenMods]: Error downloading file after retries', url, e);
+ } else {
+ console.warn(
+ `[ZenMods]: Download failed (attempt ${attempt} of ${maxRetries}), retrying in ${retryDelayMs}ms...`,
+ url,
+ e
+ );
+ await new Promise((res) => setTimeout(res, retryDelayMs));
}
}
}
+ }
- // private properties end
-
- // public properties start
-
- throttle(mainFunction, delay) {
- let timerFlag = null;
+ // private properties end
- return (...args) => {
- if (timerFlag === null) {
- mainFunction(...args);
- timerFlag = setTimeout(() => {
- timerFlag = null;
- }, delay);
- }
- };
- }
+ // public properties start
- debounce(mainFunction, wait) {
- let timerFlag;
+ throttle(mainFunction, delay) {
+ let timerFlag = null;
- return (...args) => {
- clearTimeout(timerFlag);
+ return (...args) => {
+ if (timerFlag === null) {
+ mainFunction(...args);
timerFlag = setTimeout(() => {
- mainFunction(...args);
- }, wait);
- };
- }
-
- sanitizeModName(name) {
- // Do not change to "mod-" for backwards compatibility
- return `theme-${name?.replaceAll(/\s/g, '-')?.replaceAll(/[^A-Za-z_-]+/g, '')}`;
- }
-
- get updatePref() {
- return 'zen.mods.updated-value-observer';
- }
+ timerFlag = null;
+ }, delay);
+ }
+ };
+ }
- get modsRootPath() {
- return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes');
- }
+ debounce(mainFunction, wait) {
+ let timerFlag;
- get modsDataFile() {
- return PathUtils.join(PathUtils.profileDir, 'zen-themes.json');
- }
+ return (...args) => {
+ clearTimeout(timerFlag);
+ timerFlag = setTimeout(() => {
+ mainFunction(...args);
+ }, wait);
+ };
+ }
- getModFolder(modId) {
- return PathUtils.join(this.modsRootPath, modId);
- }
+ sanitizeModName(name) {
+ // Do not change to "mod-" for backwards compatibility
+ return `theme-${name?.replaceAll(/\s/g, '-')?.replaceAll(/[^A-Za-z_-]+/g, '')}`;
+ }
- async getMods() {
- if (!(await IOUtils.exists(this.modsDataFile))) {
- await IOUtils.writeJSON(this.modsDataFile, {});
+ get updatePref() {
+ return 'zen.mods.updated-value-observer';
+ }
- return {};
- }
+ get modsRootPath() {
+ return PathUtils.join(PathUtils.profileDir, 'chrome', 'zen-themes');
+ }
- let mods = {};
+ get modsDataFile() {
+ return PathUtils.join(PathUtils.profileDir, 'zen-themes.json');
+ }
- try {
- mods = await IOUtils.readJSON(this.modsDataFile);
+ getModFolder(modId) {
+ return PathUtils.join(this.modsRootPath, modId);
+ }
- if (mods === null || typeof mods !== 'object') {
- throw new Error('Mods data file is invalid');
- }
- } catch {
- // If we have a corrupted file, reset it
- await IOUtils.writeJSON(this.modsDataFile, {});
-
- Services.wm
- .getMostRecentWindow('navigator:browser')
- .gZenUIManager.showToast('zen-themes-corrupted', {
- timeout: 8000,
- });
- }
+ async getMods() {
+ if (!(await IOUtils.exists(this.modsDataFile))) {
+ await IOUtils.writeJSON(this.modsDataFile, {});
- return mods;
+ return {};
}
- async getModPreferences(mod) {
- const modPath = PathUtils.join(this.modsRootPath, mod.id, 'preferences.json');
+ let mods = {};
- if (!(await IOUtils.exists(modPath)) || !mod.preferences) {
- return [];
- }
+ try {
+ mods = await IOUtils.readJSON(this.modsDataFile);
- try {
- const preferences = await IOUtils.readJSON(modPath);
-
- return preferences.filter(({ disabledOn = [] }) => {
- return !disabledOn.includes(gZenOperatingSystemCommonUtils.currentOperatingSystem);
- });
- } catch (e) {
- console.error(`[ZenMods]: Error reading mod preferences for ${mod.name}:`, e);
- return [];
+ if (mods === null || typeof mods !== 'object') {
+ throw new Error('Mods data file is invalid');
}
+ } catch {
+ // If we have a corrupted file, reset it
+ await IOUtils.writeJSON(this.modsDataFile, {});
+
+ Services.wm
+ .getMostRecentWindow('navigator:browser')
+ .gZenUIManager.showToast('zen-themes-corrupted', {
+ timeout: 8000,
+ });
}
- async init() {
- try {
- await SessionStore.promiseInitialized;
-
- if (
- Services.prefs.getBoolPref('zen.themes.disable-all', false) ||
- Services.appinfo.inSafeMode
- ) {
- console.log('[ZenMods]: Mods disabled by user or in safe mode.');
- return;
- }
+ return mods;
+ }
- await this.getMods(); // Check for any errors in the themes data file
- const mods = await this.#getEnabledMods();
+ async getModPreferences(mod) {
+ const modPath = PathUtils.join(this.modsRootPath, mod.id, 'preferences.json');
- const modsWithPreferences = await Promise.all(
- mods.map(async (mod) => {
- const preferences = await this.getModPreferences(mod);
+ if (!(await IOUtils.exists(modPath)) || !mod.preferences) {
+ return [];
+ }
- return {
- name: mod.name,
- enabled: mod.enabled,
- preferences,
- };
- })
- );
+ try {
+ const preferences = await IOUtils.readJSON(modPath);
- this.#writeToDom(modsWithPreferences);
+ return preferences.filter(({ disabledOn = [] }) => {
+ return !disabledOn.includes(gZenOperatingSystemCommonUtils.currentOperatingSystem);
+ });
+ } catch (e) {
+ console.error(`[ZenMods]: Error reading mod preferences for ${mod.name}:`, e);
+ return [];
+ }
+ }
- await this.#insertStylesheet();
+ async init() {
+ try {
+ await SessionStore.promiseInitialized;
- this.#setNewMilestoneIfNeeded();
- if (this.#shouldAutoUpdate()) {
- requestIdleCallback(
- () => {
- if (!window.closed) {
- requestAnimationFrame(() => {
- this.checkForModsUpdates();
- });
- }
- },
- { timeout: 1000 }
- );
- }
- } catch (e) {
- console.error('[ZenMods]: Error loading Zen Mods:', e);
+ if (
+ Services.prefs.getBoolPref('zen.themes.disable-all', false) ||
+ Services.appinfo.inSafeMode
+ ) {
+ console.log('[ZenMods]: Mods disabled by user or in safe mode.');
+ return;
}
- Services.prefs.addObserver(this.updatePref, this.#rebuildModsStylesheet.bind(this), false);
- Services.prefs.addObserver(
- 'zen.themes.disable-all',
- this.#handleDisableMods.bind(this),
- false
+ await this.getMods(); // Check for any errors in the themes data file
+ const mods = await this.#getEnabledMods();
+
+ const modsWithPreferences = await Promise.all(
+ mods.map(async (mod) => {
+ const preferences = await this.getModPreferences(mod);
+
+ return {
+ name: mod.name,
+ enabled: mod.enabled,
+ preferences,
+ };
+ })
);
- }
- #setNewMilestoneIfNeeded() {
- const previousMilestone = Services.prefs.getStringPref('zen.mods.milestone', '');
- if (previousMilestone != Services.appinfo.version) {
- Services.prefs.setStringPref('zen.mods.milestone', Services.appinfo.version);
- Services.prefs.clearUserPref('zen.mods.last-update');
+ this.#writeToDom(modsWithPreferences);
+
+ await this.#insertStylesheet();
+
+ this.#setNewMilestoneIfNeeded();
+ if (this.#shouldAutoUpdate()) {
+ requestIdleCallback(
+ () => {
+ if (!window.closed) {
+ requestAnimationFrame(() => {
+ this.checkForModsUpdates();
+ });
+ }
+ },
+ { timeout: 1000 }
+ );
}
+ } catch (e) {
+ console.error('[ZenMods]: Error loading Zen Mods:', e);
}
- #shouldAutoUpdate() {
- const daysBeforeUpdate = Services.prefs.getIntPref('zen.mods.auto-update-days');
- const lastUpdatedSec = Services.prefs.getIntPref('zen.mods.last-update', -1);
- const nowSec = Math.floor(Date.now() / 1000);
- const daysSinceUpdate = (nowSec - lastUpdatedSec) / (60 * 60 * 24);
+ Services.prefs.addObserver(this.updatePref, this.#rebuildModsStylesheet.bind(this), false);
+ Services.prefs.addObserver('zen.themes.disable-all', this.#handleDisableMods.bind(this), false);
+ }
- return (
- (Services.prefs.getBoolPref('zen.mods.auto-update', true) &&
- daysSinceUpdate >= daysBeforeUpdate) ||
- lastUpdatedSec < 0
- );
+ #setNewMilestoneIfNeeded() {
+ const previousMilestone = Services.prefs.getStringPref('zen.mods.milestone', '');
+ if (previousMilestone != Services.appinfo.version) {
+ Services.prefs.setStringPref('zen.mods.milestone', Services.appinfo.version);
+ Services.prefs.clearUserPref('zen.mods.last-update');
}
+ }
- async checkForModsUpdates() {
- const mods = await this.getMods();
-
- const updates = await Promise.all(
- Object.values(mods).map(async (currentMod) => {
- try {
- const possibleNewModVersion = await this.requestMod(currentMod.id);
+ #shouldAutoUpdate() {
+ const daysBeforeUpdate = Services.prefs.getIntPref('zen.mods.auto-update-days');
+ const lastUpdatedSec = Services.prefs.getIntPref('zen.mods.last-update', -1);
+ const nowSec = Math.floor(Date.now() / 1000);
+ const daysSinceUpdate = (nowSec - lastUpdatedSec) / (60 * 60 * 24);
+
+ return (
+ (Services.prefs.getBoolPref('zen.mods.auto-update', true) &&
+ daysSinceUpdate >= daysBeforeUpdate) ||
+ lastUpdatedSec < 0
+ );
+ }
- if (!possibleNewModVersion) {
- return null;
- }
+ async checkForModsUpdates() {
+ const mods = await this.getMods();
- if (
- !this.#compareVersions(
- possibleNewModVersion.version,
- currentMod.version ?? '0.0.0'
- ) &&
- possibleNewModVersion.version != currentMod.version
- ) {
- console.log(
- `[ZenMods]: Mod update found for mod ${currentMod.name} (${currentMod.id}), current: ${currentMod.version}, new: ${possibleNewModVersion.version}`
- );
+ const updates = await Promise.all(
+ Object.values(mods).map(async (currentMod) => {
+ try {
+ const possibleNewModVersion = await this.requestMod(currentMod.id);
- possibleNewModVersion.enabled = currentMod.enabled;
+ if (!possibleNewModVersion) {
+ return null;
+ }
- await this.removeMod(currentMod.id, false);
+ if (
+ !this.#compareVersions(possibleNewModVersion.version, currentMod.version ?? '0.0.0') &&
+ possibleNewModVersion.version != currentMod.version
+ ) {
+ console.log(
+ `[ZenMods]: Mod update found for mod ${currentMod.name} (${currentMod.id}), current: ${currentMod.version}, new: ${possibleNewModVersion.version}`
+ );
- mods[currentMod.id] = possibleNewModVersion;
+ possibleNewModVersion.enabled = currentMod.enabled;
- return possibleNewModVersion;
- }
+ await this.removeMod(currentMod.id, false);
- return null;
- } catch (e) {
- console.error('[ZenMods]: Error checking for mod updates', e);
+ mods[currentMod.id] = possibleNewModVersion;
- return null;
+ return possibleNewModVersion;
}
- })
- );
- await this.updateMods(mods);
- Services.prefs.setIntPref('zen.mods.last-update', Math.floor(Date.now() / 1000));
- return updates.filter((update) => {
- return update !== null;
- });
- }
+ return null;
+ } catch (e) {
+ console.error('[ZenMods]: Error checking for mod updates', e);
- async removeMod(modId, triggerUpdate = true) {
- const modPath = this.getModFolder(modId);
+ return null;
+ }
+ })
+ );
+
+ await this.updateMods(mods);
+ Services.prefs.setIntPref('zen.mods.last-update', Math.floor(Date.now() / 1000));
+ return updates.filter((update) => {
+ return update !== null;
+ });
+ }
- console.log(`[ZenMods]: Removing mod ${modPath}`);
+ async removeMod(modId, triggerUpdate = true) {
+ const modPath = this.getModFolder(modId);
- await IOUtils.remove(modPath, { recursive: true, ignoreAbsent: true });
+ console.log(`[ZenMods]: Removing mod ${modPath}`);
- const mods = await this.getMods();
+ await IOUtils.remove(modPath, { recursive: true, ignoreAbsent: true });
- delete mods[modId];
+ const mods = await this.getMods();
- await IOUtils.writeJSON(this.modsDataFile, mods);
+ delete mods[modId];
- if (triggerUpdate) {
- this.triggerModsUpdate();
- }
- }
+ await IOUtils.writeJSON(this.modsDataFile, mods);
- async enableMod(modId) {
- const mods = await this.getMods();
- const mod = mods[modId];
+ if (triggerUpdate) {
+ this.triggerModsUpdate();
+ }
+ }
- console.log(`[ZenMods]: Enabling mod ${mod.name}`);
+ async enableMod(modId) {
+ const mods = await this.getMods();
+ const mod = mods[modId];
- mod.enabled = true;
+ console.log(`[ZenMods]: Enabling mod ${mod.name}`);
- await IOUtils.writeJSON(this.modsDataFile, mods);
- }
+ mod.enabled = true;
- async disableMod(modId) {
- const mods = await this.getMods();
- const mod = mods[modId];
+ await IOUtils.writeJSON(this.modsDataFile, mods);
+ }
- console.log(`[ZenMods]: Disabling mod ${mod.name}`);
+ async disableMod(modId) {
+ const mods = await this.getMods();
+ const mod = mods[modId];
- mod.enabled = false;
+ console.log(`[ZenMods]: Disabling mod ${mod.name}`);
- await IOUtils.writeJSON(this.modsDataFile, mods);
- }
+ mod.enabled = false;
- async updateMods(mods = undefined) {
- if (!mods) {
- mods = await this.getMods();
- }
+ await IOUtils.writeJSON(this.modsDataFile, mods);
+ }
- await IOUtils.writeJSON(this.modsDataFile, mods);
- await this.checkForModChanges();
+ async updateMods(mods = undefined) {
+ if (!mods) {
+ mods = await this.getMods();
}
- triggerModsUpdate() {
- Services.prefs.setBoolPref(this.updatePref, !Services.prefs.getBoolPref(this.updatePref));
- }
+ await IOUtils.writeJSON(this.modsDataFile, mods);
+ await this.checkForModChanges();
+ }
- async installMod(mod) {
- try {
- const modPath = PathUtils.join(this.modsRootPath, mod.id);
- await IOUtils.makeDirectory(modPath, { ignoreExisting: true });
+ triggerModsUpdate() {
+ Services.prefs.setBoolPref(this.updatePref, !Services.prefs.getBoolPref(this.updatePref));
+ }
- await this.#downloadUrlToFile(mod.style, PathUtils.join(modPath, 'chrome.css'), true);
- await this.#downloadUrlToFile(mod.readme, PathUtils.join(modPath, 'readme.md'));
+ async installMod(mod) {
+ try {
+ const modPath = PathUtils.join(this.modsRootPath, mod.id);
+ await IOUtils.makeDirectory(modPath, { ignoreExisting: true });
- if (mod.preferences) {
- await this.#downloadUrlToFile(
- mod.preferences,
- PathUtils.join(modPath, 'preferences.json')
- );
- }
- } catch (e) {
- console.error('[ZenMods]: Error installing mod', mod.id, e);
+ await this.#downloadUrlToFile(mod.style, PathUtils.join(modPath, 'chrome.css'), true);
+ await this.#downloadUrlToFile(mod.readme, PathUtils.join(modPath, 'readme.md'));
+
+ if (mod.preferences) {
+ await this.#downloadUrlToFile(mod.preferences, PathUtils.join(modPath, 'preferences.json'));
}
+ } catch (e) {
+ console.error('[ZenMods]: Error installing mod', mod.id, e);
}
+ }
- async checkForModChanges() {
- const mods = await this.getMods();
+ async checkForModChanges() {
+ const mods = await this.getMods();
- for (const [modId, mod] of Object.entries(mods)) {
- try {
- if (!mod) {
- continue;
- }
+ for (const [modId, mod] of Object.entries(mods)) {
+ try {
+ if (!mod) {
+ continue;
+ }
- if (!(await IOUtils.exists(this.getModFolder(modId)))) {
- await this.installMod(mod);
- }
- } catch (e) {
- console.error('[ZenMods]: Error checking for mod changes', e);
+ if (!(await IOUtils.exists(this.getModFolder(modId)))) {
+ await this.installMod(mod);
}
+ } catch (e) {
+ console.error('[ZenMods]: Error checking for mod changes', e);
}
-
- this.triggerModsUpdate();
}
- async requestMod(modId) {
- const url = this.#composeModApiUrl(modId);
+ this.triggerModsUpdate();
+ }
- console.debug(`[ZenMods]: Fetching mod ${modId} info from ${url}`);
+ async requestMod(modId) {
+ const url = this.#composeModApiUrl(modId);
- const data = await fetch(url, {
- mode: 'no-cors',
- });
+ console.debug(`[ZenMods]: Fetching mod ${modId} info from ${url}`);
- if (data.ok) {
- try {
- const obj = await data.json();
+ const data = await fetch(url, {
+ mode: 'no-cors',
+ });
- return obj;
- } catch (e) {
- console.error(`[ZenMods]: Error parsing mod ${modId} info:`, e);
- }
- } else {
- console.error(`[ZenMods]: Error fetching mod ${modId} info:`, data.status);
- }
+ if (data.ok) {
+ try {
+ const obj = await data.json();
- return null;
+ return obj;
+ } catch (e) {
+ console.error(`[ZenMods]: Error parsing mod ${modId} info:`, e);
+ }
+ } else {
+ console.error(`[ZenMods]: Error fetching mod ${modId} info:`, data.status);
}
- async isModInstalled(modId) {
- const mods = await this.getMods();
- return Boolean(mods?.[modId]);
- }
+ return null;
+ }
- // public properties end
+ async isModInstalled(modId) {
+ const mods = await this.getMods();
+ return Boolean(mods?.[modId]);
}
- window.gZenMods = new nsZenMods();
+ // public properties end
}
+
+window.gZenMods = new nsZenMods();
diff --git a/src/zen/mods/jar.inc.mn b/src/zen/mods/jar.inc.mn
new file mode 100644
index 0000000000..e15776368f
--- /dev/null
+++ b/src/zen/mods/jar.inc.mn
@@ -0,0 +1,5 @@
+# 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/.
+
+ content/browser/zen-components/ZenMods.mjs (../../zen/mods/ZenMods.mjs)
diff --git a/src/zen/split-view/ZenViewSplitter.mjs b/src/zen/split-view/ZenViewSplitter.mjs
index ab3912b3a6..89488cd436 100644
--- a/src/zen/split-view/ZenViewSplitter.mjs
+++ b/src/zen/split-view/ZenViewSplitter.mjs
@@ -2,6 +2,8 @@
// 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/.
+import { nsZenDOMOperatedFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
+
class nsSplitLeafNode {
/**
* The percentage of the size of the parent the node takes up, dependent on parent direction this is either
diff --git a/src/zen/split-view/jar.inc.mn b/src/zen/split-view/jar.inc.mn
new file mode 100644
index 0000000000..cc855a1cc7
--- /dev/null
+++ b/src/zen/split-view/jar.inc.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+* content/browser/zen-styles/zen-decks.css (../../zen/split-view/zen-decks.css)
+ content/browser/zen-components/ZenViewSplitter.mjs (../../zen/split-view/ZenViewSplitter.mjs)
\ No newline at end of file
diff --git a/src/zen/tabs/ZenPinnedTabManager.mjs b/src/zen/tabs/ZenPinnedTabManager.mjs
index 68104fd182..8598858a4d 100644
--- a/src/zen/tabs/ZenPinnedTabManager.mjs
+++ b/src/zen/tabs/ZenPinnedTabManager.mjs
@@ -1,1096 +1,1095 @@
// 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/.
-{
- const lazy = {};
-
- class ZenPinnedTabsObserver {
- static ALL_EVENTS = [
- 'TabPinned',
- 'TabUnpinned',
- 'TabMove',
- 'TabGroupCreate',
- 'TabGroupRemoved',
- 'TabGroupMoved',
- 'ZenFolderRenamed',
- 'ZenFolderIconChanged',
- 'TabGroupCollapse',
- 'TabGroupExpand',
- 'TabGrouped',
- 'TabUngrouped',
- 'ZenFolderChangedWorkspace',
- 'TabAddedToEssentials',
- 'TabRemovedFromEssentials',
- ];
-
- #listeners = [];
-
- constructor() {
- XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- 'zenPinnedTabRestorePinnedTabsToPinnedUrl',
- 'zen.pinned-tab-manager.restore-pinned-tabs-to-pinned-url',
- false
- );
- XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- 'zenPinnedTabCloseShortcutBehavior',
- 'zen.pinned-tab-manager.close-shortcut-behavior',
- 'switch'
- );
- XPCOMUtils.defineLazyPreferenceGetter(
- lazy,
- 'zenTabsEssentialsMax',
- 'zen.tabs.essentials.max',
- 12
- );
- ChromeUtils.defineESModuleGetters(lazy, {
- E10SUtils: 'resource://gre/modules/E10SUtils.sys.mjs',
- });
- this.#listenPinnedTabEvents();
- }
- #listenPinnedTabEvents() {
- const eventListener = this.#eventListener.bind(this);
- for (const event of ZenPinnedTabsObserver.ALL_EVENTS) {
- window.addEventListener(event, eventListener);
- }
- window.addEventListener('unload', () => {
- for (const event of ZenPinnedTabsObserver.ALL_EVENTS) {
- window.removeEventListener(event, eventListener);
- }
- });
- }
+import { nsZenDOMOperatedFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
+
+const lazy = {};
+
+class ZenPinnedTabsObserver {
+ static ALL_EVENTS = [
+ 'TabPinned',
+ 'TabUnpinned',
+ 'TabMove',
+ 'TabGroupCreate',
+ 'TabGroupRemoved',
+ 'TabGroupMoved',
+ 'ZenFolderRenamed',
+ 'ZenFolderIconChanged',
+ 'TabGroupCollapse',
+ 'TabGroupExpand',
+ 'TabGrouped',
+ 'TabUngrouped',
+ 'ZenFolderChangedWorkspace',
+ 'TabAddedToEssentials',
+ 'TabRemovedFromEssentials',
+ ];
+
+ #listeners = [];
+
+ constructor() {
+ XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ 'zenPinnedTabRestorePinnedTabsToPinnedUrl',
+ 'zen.pinned-tab-manager.restore-pinned-tabs-to-pinned-url',
+ false
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ 'zenPinnedTabCloseShortcutBehavior',
+ 'zen.pinned-tab-manager.close-shortcut-behavior',
+ 'switch'
+ );
+ XPCOMUtils.defineLazyPreferenceGetter(
+ lazy,
+ 'zenTabsEssentialsMax',
+ 'zen.tabs.essentials.max',
+ 12
+ );
+ ChromeUtils.defineESModuleGetters(lazy, {
+ E10SUtils: 'resource://gre/modules/E10SUtils.sys.mjs',
+ });
+ this.#listenPinnedTabEvents();
+ }
- #eventListener(event) {
- for (const listener of this.#listeners) {
- listener(event.type, event);
- }
+ #listenPinnedTabEvents() {
+ const eventListener = this.#eventListener.bind(this);
+ for (const event of ZenPinnedTabsObserver.ALL_EVENTS) {
+ window.addEventListener(event, eventListener);
}
+ window.addEventListener('unload', () => {
+ for (const event of ZenPinnedTabsObserver.ALL_EVENTS) {
+ window.removeEventListener(event, eventListener);
+ }
+ });
+ }
- addPinnedTabListener(listener) {
- this.#listeners.push(listener);
+ #eventListener(event) {
+ for (const listener of this.#listeners) {
+ listener(event.type, event);
}
}
- class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
- hasInitializedPins = false;
- promiseInitializedPinned = new Promise((resolve) => {
- this._resolvePinnedInitializedInternal = resolve;
- });
-
- async init() {
- if (!this.enabled) {
- return;
- }
- this._canLog = Services.prefs.getBoolPref('zen.pinned-tab-manager.debug', false);
- this.observer = new ZenPinnedTabsObserver();
- this._initClosePinnedTabShortcut();
- this._insertItemsIntoTabContextMenu();
- this.observer.addPinnedTabListener(this._onPinnedTabEvent.bind(this));
+ addPinnedTabListener(listener) {
+ this.#listeners.push(listener);
+ }
+}
- this._zenClickEventListener = this._onTabClick.bind(this);
+class nsZenPinnedTabManager extends nsZenDOMOperatedFeature {
+ hasInitializedPins = false;
+ promiseInitializedPinned = new Promise((resolve) => {
+ this._resolvePinnedInitializedInternal = resolve;
+ });
- gZenWorkspaces._resolvePinnedInitialized();
+ async init() {
+ if (!this.enabled) {
+ return;
}
+ this._canLog = Services.prefs.getBoolPref('zen.pinned-tab-manager.debug', false);
+ this.observer = new ZenPinnedTabsObserver();
+ this._initClosePinnedTabShortcut();
+ this._insertItemsIntoTabContextMenu();
+ this.observer.addPinnedTabListener(this._onPinnedTabEvent.bind(this));
- log(message) {
- if (this._canLog) {
- console.log(`[ZenPinnedTabManager] ${message}`);
- }
- }
+ this._zenClickEventListener = this._onTabClick.bind(this);
- onTabIconChanged(tab, url = null) {
- const iconUrl = url ?? tab.iconImage.src;
- if (!iconUrl && tab.hasAttribute('zen-pin-id')) {
- try {
- setTimeout(async () => {
- const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI);
- if (favicon) {
- gBrowser.setIcon(tab, favicon);
- }
- });
- } catch {
- // Handle error
- }
- } else {
- if (tab.hasAttribute('zen-essential')) {
- tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`);
- }
- }
+ gZenWorkspaces._resolvePinnedInitialized();
+ }
+
+ log(message) {
+ if (this._canLog) {
+ console.log(`[ZenPinnedTabManager] ${message}`);
}
+ }
- _onTabResetPinButton(event, tab) {
- event.stopPropagation();
- const pin = this._pinsCache?.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
- if (!pin) {
- return;
+ onTabIconChanged(tab, url = null) {
+ const iconUrl = url ?? tab.iconImage.src;
+ if (!iconUrl && tab.hasAttribute('zen-pin-id')) {
+ try {
+ setTimeout(async () => {
+ const favicon = await this.getFaviconAsBase64(tab.linkedBrowser.currentURI);
+ if (favicon) {
+ gBrowser.setIcon(tab, favicon);
+ }
+ });
+ } catch {
+ // Handle error
}
- let userContextId;
- if (tab.hasAttribute('usercontextid')) {
- userContextId = tab.getAttribute('usercontextid');
+ } else {
+ if (tab.hasAttribute('zen-essential')) {
+ tab.style.setProperty('--zen-essential-tab-icon', `url(${iconUrl})`);
}
- const pinnedUrl = Services.io.newURI(pin.url);
- const browser = tab.linkedBrowser;
- browser.loadURI(pinnedUrl, {
- triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({
- userContextId,
- }),
- });
- this.resetPinChangedUrl(tab);
}
+ }
- get enabled() {
- return !gZenWorkspaces.privateWindowOrDisabled;
+ _onTabResetPinButton(event, tab) {
+ event.stopPropagation();
+ const pin = this._pinsCache?.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
+ if (!pin) {
+ return;
}
-
- get maxEssentialTabs() {
- return lazy.zenTabsEssentialsMax;
+ let userContextId;
+ if (tab.hasAttribute('usercontextid')) {
+ userContextId = tab.getAttribute('usercontextid');
}
+ const pinnedUrl = Services.io.newURI(pin.url);
+ const browser = tab.linkedBrowser;
+ browser.loadURI(pinnedUrl, {
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({
+ userContextId,
+ }),
+ });
+ this.resetPinChangedUrl(tab);
+ }
- async refreshPinnedTabs({ init = false } = {}) {
- if (!this.enabled) {
- return;
- }
- await ZenPinnedTabsStorage.promiseInitialized;
- await this.#initializePinsCache();
- setTimeout(async () => {
- // Execute in a separate task to avoid blocking the main thread
- await SessionStore.promiseAllWindowsRestored;
- await gZenWorkspaces.promiseInitialized;
- await this.#initializePinnedTabs(init);
- if (init) {
- this._hasFinishedLoading = true;
- }
- }, 10);
+ get enabled() {
+ return !gZenWorkspaces.privateWindowOrDisabled;
+ }
+
+ get maxEssentialTabs() {
+ return lazy.zenTabsEssentialsMax;
+ }
+
+ async refreshPinnedTabs({ init = false } = {}) {
+ if (!this.enabled) {
+ return;
}
+ await ZenPinnedTabsStorage.promiseInitialized;
+ await this.#initializePinsCache();
+ setTimeout(async () => {
+ // Execute in a separate task to avoid blocking the main thread
+ await SessionStore.promiseAllWindowsRestored;
+ await gZenWorkspaces.promiseInitialized;
+ await this.#initializePinnedTabs(init);
+ if (init) {
+ this._hasFinishedLoading = true;
+ }
+ }, 10);
+ }
- async #initializePinsCache() {
- try {
- // Get pin data
- const pins = await ZenPinnedTabsStorage.getPins();
-
- // Enhance pins with favicons
- this._pinsCache = await Promise.all(
- pins.map(async (pin) => {
- try {
- if (pin.isGroup) {
- return pin; // Skip groups for now
- }
- const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url));
- return {
- ...pin,
- iconUrl: image || null,
- };
- } catch {
- // If favicon fetch fails, continue without icon
- return {
- ...pin,
- iconUrl: null,
- };
+ async #initializePinsCache() {
+ try {
+ // Get pin data
+ const pins = await ZenPinnedTabsStorage.getPins();
+
+ // Enhance pins with favicons
+ this._pinsCache = await Promise.all(
+ pins.map(async (pin) => {
+ try {
+ if (pin.isGroup) {
+ return pin; // Skip groups for now
}
- })
- );
- } catch (ex) {
- console.error('Failed to initialize pins cache:', ex);
- this._pinsCache = [];
- }
-
- this.log(`Initialized pins cache with ${this._pinsCache.length} pins`);
- return this._pinsCache;
+ const image = await this.getFaviconAsBase64(Services.io.newURI(pin.url));
+ return {
+ ...pin,
+ iconUrl: image || null,
+ };
+ } catch {
+ // If favicon fetch fails, continue without icon
+ return {
+ ...pin,
+ iconUrl: null,
+ };
+ }
+ })
+ );
+ } catch (ex) {
+ console.error('Failed to initialize pins cache:', ex);
+ this._pinsCache = [];
}
- #finishedInitializingPins() {
- if (this.hasInitializedPins) {
- return;
- }
- this._resolvePinnedInitializedInternal();
- delete this._resolvePinnedInitializedInternal;
- this.hasInitializedPins = true;
+ this.log(`Initialized pins cache with ${this._pinsCache.length} pins`);
+ return this._pinsCache;
+ }
+
+ #finishedInitializingPins() {
+ if (this.hasInitializedPins) {
+ return;
}
+ this._resolvePinnedInitializedInternal();
+ delete this._resolvePinnedInitializedInternal;
+ this.hasInitializedPins = true;
+ }
- async #initializePinnedTabs(init = false) {
- const pins = this._pinsCache;
- if (!pins?.length || !init) {
- this.#finishedInitializingPins();
- return;
- }
+ async #initializePinnedTabs(init = false) {
+ const pins = this._pinsCache;
+ if (!pins?.length || !init) {
+ this.#finishedInitializingPins();
+ return;
+ }
- const pinnedTabsByUUID = new Map();
- const pinsToCreate = new Set(pins.map((p) => p.uuid));
+ const pinnedTabsByUUID = new Map();
+ const pinsToCreate = new Set(pins.map((p) => p.uuid));
- // First pass: identify existing tabs and remove those without pins
- for (let tab of gZenWorkspaces.allStoredTabs) {
- const pinId = tab.getAttribute('zen-pin-id');
- if (!pinId) {
- continue;
- }
+ // First pass: identify existing tabs and remove those without pins
+ for (let tab of gZenWorkspaces.allStoredTabs) {
+ const pinId = tab.getAttribute('zen-pin-id');
+ if (!pinId) {
+ continue;
+ }
- if (pinsToCreate.has(pinId)) {
- // This is a valid pinned tab that matches a pin
- pinnedTabsByUUID.set(pinId, tab);
- pinsToCreate.delete(pinId);
+ if (pinsToCreate.has(pinId)) {
+ // This is a valid pinned tab that matches a pin
+ pinnedTabsByUUID.set(pinId, tab);
+ pinsToCreate.delete(pinId);
- if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl && init) {
- this._resetTabToStoredState(tab);
- }
- } else {
- // This is a pinned tab that no longer has a corresponding pin
- gBrowser.removeTab(tab);
+ if (lazy.zenPinnedTabRestorePinnedTabsToPinnedUrl && init) {
+ this._resetTabToStoredState(tab);
}
+ } else {
+ // This is a pinned tab that no longer has a corresponding pin
+ gBrowser.removeTab(tab);
}
+ }
- for (const group of gZenWorkspaces.allTabGroups) {
- const pinId = group.getAttribute('zen-pin-id');
- if (!pinId) {
- continue;
- }
- if (pinsToCreate.has(pinId)) {
- // This is a valid pinned group that matches a pin
- pinsToCreate.delete(pinId);
- }
+ for (const group of gZenWorkspaces.allTabGroups) {
+ const pinId = group.getAttribute('zen-pin-id');
+ if (!pinId) {
+ continue;
}
+ if (pinsToCreate.has(pinId)) {
+ // This is a valid pinned group that matches a pin
+ pinsToCreate.delete(pinId);
+ }
+ }
- // Second pass: For every existing tab, update its label
- // and set 'zen-has-static-label' attribute if it's been edited
- for (let pin of pins) {
- const tab = pinnedTabsByUUID.get(pin.uuid);
- if (!tab) {
- continue;
- }
+ // Second pass: For every existing tab, update its label
+ // and set 'zen-has-static-label' attribute if it's been edited
+ for (let pin of pins) {
+ const tab = pinnedTabsByUUID.get(pin.uuid);
+ if (!tab) {
+ continue;
+ }
- tab.removeAttribute('zen-has-static-label'); // So we can set it again
- if (pin.title && pin.editedTitle) {
- gBrowser._setTabLabel(tab, pin.title, { beforeTabOpen: true });
- tab.setAttribute('zen-has-static-label', 'true');
- }
+ tab.removeAttribute('zen-has-static-label'); // So we can set it again
+ if (pin.title && pin.editedTitle) {
+ gBrowser._setTabLabel(tab, pin.title, { beforeTabOpen: true });
+ tab.setAttribute('zen-has-static-label', 'true');
}
+ }
- const groups = new Map();
- const pendingTabsInsideGroups = {};
+ const groups = new Map();
+ const pendingTabsInsideGroups = {};
- // Third pass: create new tabs for pins that don't have tabs
- for (let pin of pins) {
- try {
- if (!pinsToCreate.has(pin.uuid)) {
- continue; // Skip pins that already have tabs
- }
+ // Third pass: create new tabs for pins that don't have tabs
+ for (let pin of pins) {
+ try {
+ if (!pinsToCreate.has(pin.uuid)) {
+ continue; // Skip pins that already have tabs
+ }
- if (pin.isGroup) {
- const tabs = [];
- // If there's already existing tabs, let's use them
- for (const [uuid, existingTab] of pinnedTabsByUUID) {
- const pinObject = this._pinsCache.find((p) => p.uuid === uuid);
- if (pinObject && pinObject.parentUuid === pin.uuid) {
- tabs.push(existingTab);
- }
+ if (pin.isGroup) {
+ const tabs = [];
+ // If there's already existing tabs, let's use them
+ for (const [uuid, existingTab] of pinnedTabsByUUID) {
+ const pinObject = this._pinsCache.find((p) => p.uuid === uuid);
+ if (pinObject && pinObject.parentUuid === pin.uuid) {
+ tabs.push(existingTab);
}
- // We still need to iterate through pending tabs since the database
- // query doesn't guarantee the order of insertion
- for (const [parentUuid, folderTabs] of Object.entries(pendingTabsInsideGroups)) {
- if (parentUuid === pin.uuid) {
- tabs.push(...folderTabs);
- }
+ }
+ // We still need to iterate through pending tabs since the database
+ // query doesn't guarantee the order of insertion
+ for (const [parentUuid, folderTabs] of Object.entries(pendingTabsInsideGroups)) {
+ if (parentUuid === pin.uuid) {
+ tabs.push(...folderTabs);
}
- const group = gZenFolders.createFolder(tabs, {
- label: pin.title,
- collapsed: pin.isFolderCollapsed,
- initialPinId: pin.uuid,
- workspaceId: pin.workspaceUuid,
- insertAfter:
- groups.get(pin.parentUuid)?.querySelector('.tab-group-container')?.lastChild ||
- null,
- });
- gZenFolders.setFolderUserIcon(group, pin.folderIcon);
- groups.set(pin.uuid, group);
- continue;
}
+ const group = gZenFolders.createFolder(tabs, {
+ label: pin.title,
+ collapsed: pin.isFolderCollapsed,
+ initialPinId: pin.uuid,
+ workspaceId: pin.workspaceUuid,
+ insertAfter:
+ groups.get(pin.parentUuid)?.querySelector('.tab-group-container')?.lastChild || null,
+ });
+ gZenFolders.setFolderUserIcon(group, pin.folderIcon);
+ groups.set(pin.uuid, group);
+ continue;
+ }
- let params = {
- skipAnimation: true,
- allowInheritPrincipal: false,
- skipBackgroundNotify: true,
- userContextId: pin.containerTabId || 0,
- createLazyBrowser: true,
- skipLoad: true,
- noInitialLabel: false,
- };
-
- // Create and initialize the tab
- let newTab = gBrowser.addTrustedTab(pin.url, params);
- newTab.setAttribute('zenDefaultUserContextId', true);
-
- // Set initial label/title
- if (pin.title) {
- gBrowser.setInitialTabTitle(newTab, pin.title);
- }
+ let params = {
+ skipAnimation: true,
+ allowInheritPrincipal: false,
+ skipBackgroundNotify: true,
+ userContextId: pin.containerTabId || 0,
+ createLazyBrowser: true,
+ skipLoad: true,
+ noInitialLabel: false,
+ };
+
+ // Create and initialize the tab
+ let newTab = gBrowser.addTrustedTab(pin.url, params);
+ newTab.setAttribute('zenDefaultUserContextId', true);
+
+ // Set initial label/title
+ if (pin.title) {
+ gBrowser.setInitialTabTitle(newTab, pin.title);
+ }
- // Set the icon if we have it cached
- if (pin.iconUrl) {
- gBrowser.setIcon(newTab, pin.iconUrl);
- }
+ // Set the icon if we have it cached
+ if (pin.iconUrl) {
+ gBrowser.setIcon(newTab, pin.iconUrl);
+ }
- newTab.setAttribute('zen-pin-id', pin.uuid);
+ newTab.setAttribute('zen-pin-id', pin.uuid);
- if (pin.workspaceUuid) {
- newTab.setAttribute('zen-workspace-id', pin.workspaceUuid);
- }
+ if (pin.workspaceUuid) {
+ newTab.setAttribute('zen-workspace-id', pin.workspaceUuid);
+ }
- if (pin.isEssential) {
- newTab.setAttribute('zen-essential', 'true');
- }
+ if (pin.isEssential) {
+ newTab.setAttribute('zen-essential', 'true');
+ }
- if (pin.editedTitle) {
- newTab.setAttribute('zen-has-static-label', 'true');
- }
+ if (pin.editedTitle) {
+ newTab.setAttribute('zen-has-static-label', 'true');
+ }
- // Initialize browser state if needed
- if (!newTab.linkedBrowser._remoteAutoRemoved) {
- let state = {
- entries: [
- {
- url: pin.url,
- title: pin.title,
- triggeringPrincipal_base64: E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
- },
- ],
- userContextId: pin.containerTabId || 0,
- image: pin.iconUrl,
- };
+ // Initialize browser state if needed
+ if (!newTab.linkedBrowser._remoteAutoRemoved) {
+ let state = {
+ entries: [
+ {
+ url: pin.url,
+ title: pin.title,
+ triggeringPrincipal_base64: E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
+ },
+ ],
+ userContextId: pin.containerTabId || 0,
+ image: pin.iconUrl,
+ };
- SessionStore.setTabState(newTab, state);
- }
+ SessionStore.setTabState(newTab, state);
+ }
- this.log(`Created new pinned tab for pin ${pin.uuid} (isEssential: ${pin.isEssential})`);
- gBrowser.pinTab(newTab);
+ this.log(`Created new pinned tab for pin ${pin.uuid} (isEssential: ${pin.isEssential})`);
+ gBrowser.pinTab(newTab);
- if (pin.parentUuid) {
- const parentGroup = groups.get(pin.parentUuid);
- if (parentGroup) {
- parentGroup.querySelector('.tab-group-container').appendChild(newTab);
- } else {
- if (pendingTabsInsideGroups[pin.parentUuid]) {
- pendingTabsInsideGroups[pin.parentUuid].push(newTab);
- } else {
- pendingTabsInsideGroups[pin.parentUuid] = [newTab];
- }
- }
+ if (pin.parentUuid) {
+ const parentGroup = groups.get(pin.parentUuid);
+ if (parentGroup) {
+ parentGroup.querySelector('.tab-group-container').appendChild(newTab);
} else {
- if (!pin.isEssential) {
- const container = gZenWorkspaces.workspaceElement(
- pin.workspaceUuid
- )?.pinnedTabsContainer;
- if (container) {
- container.insertBefore(newTab, container.lastChild);
- }
+ if (pendingTabsInsideGroups[pin.parentUuid]) {
+ pendingTabsInsideGroups[pin.parentUuid].push(newTab);
} else {
- gZenWorkspaces.getEssentialsSection(pin.containerTabId).appendChild(newTab);
+ pendingTabsInsideGroups[pin.parentUuid] = [newTab];
}
}
-
- gBrowser.tabContainer._invalidateCachedTabs();
- newTab.initialize();
- } catch (ex) {
- console.error('Failed to initialize pinned tabs:', ex);
+ } else {
+ if (!pin.isEssential) {
+ const container = gZenWorkspaces.workspaceElement(
+ pin.workspaceUuid
+ )?.pinnedTabsContainer;
+ if (container) {
+ container.insertBefore(newTab, container.lastChild);
+ }
+ } else {
+ gZenWorkspaces.getEssentialsSection(pin.containerTabId).appendChild(newTab);
+ }
}
+
+ gBrowser.tabContainer._invalidateCachedTabs();
+ newTab.initialize();
+ } catch (ex) {
+ console.error('Failed to initialize pinned tabs:', ex);
}
+ }
- setTimeout(() => {
- this.#finishedInitializingPins();
- }, 0);
+ setTimeout(() => {
+ this.#finishedInitializingPins();
+ }, 0);
- gBrowser._updateTabBarForPinnedTabs();
- gZenUIManager.updateTabsToolbar();
+ gBrowser._updateTabBarForPinnedTabs();
+ gZenUIManager.updateTabsToolbar();
+ }
+
+ _onPinnedTabEvent(action, event) {
+ if (!this.enabled) return;
+ const tab = event.target;
+ if (this._ignoreNextTabPinnedEvent) {
+ delete this._ignoreNextTabPinnedEvent;
+ return;
+ }
+ switch (action) {
+ case 'TabPinned':
+ case 'TabAddedToEssentials':
+ tab._zenClickEventListener = this._zenClickEventListener;
+ tab.addEventListener('click', tab._zenClickEventListener);
+ this._setPinnedAttributes(tab);
+ break;
+ case 'TabRemovedFromEssentials':
+ if (tab.pinned) {
+ this.#onTabMove(tab);
+ break;
+ }
+ // [Fall through]
+ case 'TabUnpinned':
+ this._removePinnedAttributes(tab);
+ if (tab._zenClickEventListener) {
+ tab.removeEventListener('click', tab._zenClickEventListener);
+ delete tab._zenClickEventListener;
+ }
+ break;
+ case 'TabMove':
+ this.#onTabMove(tab);
+ break;
+ case 'TabGroupCreate':
+ this.#onTabGroupCreate(event);
+ break;
+ case 'TabGroupRemoved':
+ this.#onTabGroupRemoved(event);
+ break;
+ case 'TabGroupMoved':
+ this.#onTabGroupMoved(event);
+ break;
+ case 'ZenFolderRenamed':
+ case 'ZenFolderIconChanged':
+ case 'TabGroupCollapse':
+ case 'TabGroupExpand':
+ case 'ZenFolderChangedWorkspace':
+ this.#updateGroupInfo(event.originalTarget, action);
+ break;
+ case 'TabGrouped':
+ this.#onTabGrouped(event);
+ break;
+ case 'TabUngrouped':
+ this.#onTabUngrouped(event);
+ break;
+ default:
+ console.warn('ZenPinnedTabManager: Unhandled tab event', action);
+ break;
}
+ }
- _onPinnedTabEvent(action, event) {
- if (!this.enabled) return;
- const tab = event.target;
- if (this._ignoreNextTabPinnedEvent) {
- delete this._ignoreNextTabPinnedEvent;
- return;
+ async #onTabGroupCreate(event) {
+ const group = event.originalTarget;
+ if (!group.isZenFolder) {
+ return;
+ }
+ if (group.hasAttribute('zen-pin-id')) {
+ return; // Group already exists in storage
+ }
+ const workspaceId = group.getAttribute('zen-workspace-id');
+ let id = await ZenPinnedTabsStorage.createGroup(
+ group.name,
+ group.iconURL,
+ group.collapsed,
+ workspaceId,
+ group.getAttribute('zen-pin-id'),
+ group._pPos
+ );
+ group.setAttribute('zen-pin-id', id);
+ for (const tab of group.tabs) {
+ // Only add it if the tab is directly under the group
+ if (
+ tab.pinned &&
+ tab.hasAttribute('zen-pin-id') &&
+ tab.group === group &&
+ this.hasInitializedPins
+ ) {
+ const tabPinId = tab.getAttribute('zen-pin-id');
+ await ZenPinnedTabsStorage.addTabToGroup(tabPinId, id, /* position */ tab._pPos);
}
+ }
+ await this.refreshPinnedTabs();
+ }
+
+ async #onTabGrouped(event) {
+ const tab = event.detail;
+ const group = tab.group;
+ if (!group.isZenFolder) {
+ return;
+ }
+ const pinId = group.getAttribute('zen-pin-id');
+ const tabPinId = tab.getAttribute('zen-pin-id');
+ const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId);
+ if (!tabPin || !tabPin.group) {
+ return;
+ }
+ ZenPinnedTabsStorage.addTabToGroup(tabPinId, pinId, /* position */ tab._pPos);
+ }
+
+ async #onTabUngrouped(event) {
+ const tab = event.detail;
+ const group = tab.group;
+ if (!group?.isZenFolder) {
+ return;
+ }
+ const tabPinId = tab.getAttribute('zen-pin-id');
+ const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId);
+ if (!tabPin) {
+ return;
+ }
+ ZenPinnedTabsStorage.removeTabFromGroup(tabPinId, /* position */ tab._pPos);
+ }
+
+ async #updateGroupInfo(group, action) {
+ if (!group?.isZenFolder) {
+ return;
+ }
+ const pinId = group.getAttribute('zen-pin-id');
+ const groupPin = this._pinsCache?.find((p) => p.uuid === pinId);
+ if (groupPin) {
+ groupPin.title = group.name;
+ groupPin.folderIcon = group.iconURL;
+ groupPin.isFolderCollapsed = group.collapsed;
+ groupPin.position = group._pPos;
+ groupPin.parentUuid = group.group?.getAttribute('zen-pin-id') || null;
+ groupPin.workspaceUuid = group.getAttribute('zen-workspace-id') || null;
+ await this.savePin(groupPin);
switch (action) {
- case 'TabPinned':
- case 'TabAddedToEssentials':
- tab._zenClickEventListener = this._zenClickEventListener;
- tab.addEventListener('click', tab._zenClickEventListener);
- this._setPinnedAttributes(tab);
- break;
- case 'TabRemovedFromEssentials':
- if (tab.pinned) {
- this.#onTabMove(tab);
- break;
- }
- // [Fall through]
- case 'TabUnpinned':
- this._removePinnedAttributes(tab);
- if (tab._zenClickEventListener) {
- tab.removeEventListener('click', tab._zenClickEventListener);
- delete tab._zenClickEventListener;
- }
- break;
- case 'TabMove':
- this.#onTabMove(tab);
- break;
- case 'TabGroupCreate':
- this.#onTabGroupCreate(event);
- break;
- case 'TabGroupRemoved':
- this.#onTabGroupRemoved(event);
- break;
- case 'TabGroupMoved':
- this.#onTabGroupMoved(event);
- break;
case 'ZenFolderRenamed':
case 'ZenFolderIconChanged':
case 'TabGroupCollapse':
case 'TabGroupExpand':
- case 'ZenFolderChangedWorkspace':
- this.#updateGroupInfo(event.originalTarget, action);
- break;
- case 'TabGrouped':
- this.#onTabGrouped(event);
- break;
- case 'TabUngrouped':
- this.#onTabUngrouped(event);
break;
default:
- console.warn('ZenPinnedTabManager: Unhandled tab event', action);
- break;
+ for (const item of group.allItems) {
+ if (gBrowser.isTabGroup(item)) {
+ await this.#updateGroupInfo(item, action);
+ } else {
+ await this.#onTabMove(item);
+ }
+ }
}
}
+ }
- async #onTabGroupCreate(event) {
- const group = event.originalTarget;
- if (!group.isZenFolder) {
- return;
- }
- if (group.hasAttribute('zen-pin-id')) {
- return; // Group already exists in storage
- }
- const workspaceId = group.getAttribute('zen-workspace-id');
- let id = await ZenPinnedTabsStorage.createGroup(
- group.name,
- group.iconURL,
- group.collapsed,
- workspaceId,
- group.getAttribute('zen-pin-id'),
- group._pPos
- );
- group.setAttribute('zen-pin-id', id);
- for (const tab of group.tabs) {
- // Only add it if the tab is directly under the group
- if (
- tab.pinned &&
- tab.hasAttribute('zen-pin-id') &&
- tab.group === group &&
- this.hasInitializedPins
- ) {
- const tabPinId = tab.getAttribute('zen-pin-id');
- await ZenPinnedTabsStorage.addTabToGroup(tabPinId, id, /* position */ tab._pPos);
- }
- }
- await this.refreshPinnedTabs();
+ async #onTabGroupRemoved(event) {
+ const group = event.originalTarget;
+ if (!group.isZenFolder) {
+ return;
}
+ await ZenPinnedTabsStorage.removePin(group.getAttribute('zen-pin-id'));
+ group.removeAttribute('zen-pin-id');
+ }
- async #onTabGrouped(event) {
- const tab = event.detail;
- const group = tab.group;
- if (!group.isZenFolder) {
- return;
- }
- const pinId = group.getAttribute('zen-pin-id');
- const tabPinId = tab.getAttribute('zen-pin-id');
- const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId);
- if (!tabPin || !tabPin.group) {
- return;
- }
- ZenPinnedTabsStorage.addTabToGroup(tabPinId, pinId, /* position */ tab._pPos);
+ async #onTabGroupMoved(event) {
+ const group = event.originalTarget;
+ if (!group.isZenFolder) {
+ return;
}
-
- async #onTabUngrouped(event) {
- const tab = event.detail;
- const group = tab.group;
- if (!group?.isZenFolder) {
- return;
- }
- const tabPinId = tab.getAttribute('zen-pin-id');
- const tabPin = this._pinsCache?.find((p) => p.uuid === tabPinId);
- if (!tabPin) {
- return;
- }
- ZenPinnedTabsStorage.removeTabFromGroup(tabPinId, /* position */ tab._pPos);
+ const newIndex = group._pPos;
+ const pinId = group.getAttribute('zen-pin-id');
+ if (!pinId) {
+ return;
}
-
- async #updateGroupInfo(group, action) {
- if (!group?.isZenFolder) {
- return;
- }
- const pinId = group.getAttribute('zen-pin-id');
- const groupPin = this._pinsCache?.find((p) => p.uuid === pinId);
- if (groupPin) {
- groupPin.title = group.name;
- groupPin.folderIcon = group.iconURL;
- groupPin.isFolderCollapsed = group.collapsed;
- groupPin.position = group._pPos;
- groupPin.parentUuid = group.group?.getAttribute('zen-pin-id') || null;
- groupPin.workspaceUuid = group.getAttribute('zen-workspace-id') || null;
- await this.savePin(groupPin);
- switch (action) {
- case 'ZenFolderRenamed':
- case 'ZenFolderIconChanged':
- case 'TabGroupCollapse':
- case 'TabGroupExpand':
- break;
- default:
- for (const item of group.allItems) {
- if (gBrowser.isTabGroup(item)) {
- await this.#updateGroupInfo(item, action);
- } else {
- await this.#onTabMove(item);
- }
- }
+ for (const tab of group.allItemsRecursive) {
+ if (tab.pinned && tab.getAttribute('zen-pin-id') === pinId) {
+ const pin = this._pinsCache.find((p) => p.uuid === pinId);
+ if (pin) {
+ pin.position = tab._pPos;
+ pin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null;
+ pin.workspaceUuid = group.getAttribute('zen-workspace-id');
+ await this.savePin(pin, false);
}
+ break;
}
}
+ const groupPin = this._pinsCache?.find((p) => p.uuid === pinId);
+ if (groupPin) {
+ groupPin.position = newIndex;
+ groupPin.parentUuid = group.group?.getAttribute('zen-pin-id');
+ groupPin.workspaceUuid = group.getAttribute('zen-workspace-id');
+ await this.savePin(groupPin);
+ }
+ }
- async #onTabGroupRemoved(event) {
- const group = event.originalTarget;
- if (!group.isZenFolder) {
- return;
- }
- await ZenPinnedTabsStorage.removePin(group.getAttribute('zen-pin-id'));
- group.removeAttribute('zen-pin-id');
+ async #onTabMove(tab) {
+ if (!tab.pinned || !this._pinsCache) {
+ return;
}
- async #onTabGroupMoved(event) {
- const group = event.originalTarget;
- if (!group.isZenFolder) {
- return;
- }
- const newIndex = group._pPos;
- const pinId = group.getAttribute('zen-pin-id');
- if (!pinId) {
- return;
- }
- for (const tab of group.allItemsRecursive) {
- if (tab.pinned && tab.getAttribute('zen-pin-id') === pinId) {
- const pin = this._pinsCache.find((p) => p.uuid === pinId);
- if (pin) {
- pin.position = tab._pPos;
- pin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null;
- pin.workspaceUuid = group.getAttribute('zen-workspace-id');
- await this.savePin(pin, false);
- }
- break;
+ const allTabs = [...gBrowser.tabs, ...gBrowser.tabGroups];
+ for (let i = 0; i < allTabs.length; i++) {
+ const otherTab = allTabs[i];
+ if (
+ otherTab.pinned &&
+ otherTab.getAttribute('zen-pin-id') !== tab.getAttribute('zen-pin-id')
+ ) {
+ const actualPin = this._pinsCache.find(
+ (pin) => pin.uuid === otherTab.getAttribute('zen-pin-id')
+ );
+ if (!actualPin) {
+ continue;
}
- }
- const groupPin = this._pinsCache?.find((p) => p.uuid === pinId);
- if (groupPin) {
- groupPin.position = newIndex;
- groupPin.parentUuid = group.group?.getAttribute('zen-pin-id');
- groupPin.workspaceUuid = group.getAttribute('zen-workspace-id');
- await this.savePin(groupPin);
+ actualPin.position = otherTab._pPos;
+ actualPin.workspaceUuid = otherTab.getAttribute('zen-workspace-id');
+ actualPin.parentUuid = otherTab.group?.getAttribute('zen-pin-id') || null;
+ await this.savePin(actualPin, false);
}
}
- async #onTabMove(tab) {
- if (!tab.pinned || !this._pinsCache) {
- return;
- }
-
- const allTabs = [...gBrowser.tabs, ...gBrowser.tabGroups];
- for (let i = 0; i < allTabs.length; i++) {
- const otherTab = allTabs[i];
- if (
- otherTab.pinned &&
- otherTab.getAttribute('zen-pin-id') !== tab.getAttribute('zen-pin-id')
- ) {
- const actualPin = this._pinsCache.find(
- (pin) => pin.uuid === otherTab.getAttribute('zen-pin-id')
- );
- if (!actualPin) {
- continue;
- }
- actualPin.position = otherTab._pPos;
- actualPin.workspaceUuid = otherTab.getAttribute('zen-workspace-id');
- actualPin.parentUuid = otherTab.group?.getAttribute('zen-pin-id') || null;
- await this.savePin(actualPin, false);
- }
- }
+ const actualPin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
- const actualPin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
+ if (!actualPin) {
+ return;
+ }
+ actualPin.position = tab._pPos;
+ actualPin.isEssential = tab.hasAttribute('zen-essential');
+ actualPin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null;
+ actualPin.workspaceUuid = tab.getAttribute('zen-workspace-id') || null;
+
+ // There was a bug where the title and hasStaticLabel attribute were not being set
+ // This is a workaround to fix that
+ if (tab.hasAttribute('zen-has-static-label')) {
+ actualPin.editedTitle = true;
+ actualPin.title = tab.label;
+ }
+ await this.savePin(actualPin);
+ tab.dispatchEvent(
+ new CustomEvent('ZenPinnedTabMoved', {
+ detail: { tab },
+ })
+ );
+ }
- if (!actualPin) {
- return;
- }
- actualPin.position = tab._pPos;
- actualPin.isEssential = tab.hasAttribute('zen-essential');
- actualPin.parentUuid = tab.group?.getAttribute('zen-pin-id') || null;
- actualPin.workspaceUuid = tab.getAttribute('zen-workspace-id') || null;
-
- // There was a bug where the title and hasStaticLabel attribute were not being set
- // This is a workaround to fix that
- if (tab.hasAttribute('zen-has-static-label')) {
- actualPin.editedTitle = true;
- actualPin.title = tab.label;
- }
- await this.savePin(actualPin);
- tab.dispatchEvent(
- new CustomEvent('ZenPinnedTabMoved', {
- detail: { tab },
- })
- );
+ async _onTabClick(e) {
+ const tab = e.target?.closest('tab');
+ if (e.button === 1 && tab) {
+ await this.onCloseTabShortcut(e, tab, {
+ closeIfPending: Services.prefs.getBoolPref('zen.pinned-tab-manager.wheel-close-if-pending'),
+ });
}
+ }
- async _onTabClick(e) {
- const tab = e.target?.closest('tab');
- if (e.button === 1 && tab) {
- await this.onCloseTabShortcut(e, tab, {
- closeIfPending: Services.prefs.getBoolPref(
- 'zen.pinned-tab-manager.wheel-close-if-pending'
- ),
- });
- }
+ async resetPinnedTab(tab) {
+ if (!tab) {
+ tab = TabContextMenu.contextTab;
}
- async resetPinnedTab(tab) {
- if (!tab) {
- tab = TabContextMenu.contextTab;
- }
+ if (!tab || !tab.pinned) {
+ return;
+ }
- if (!tab || !tab.pinned) {
- return;
- }
+ await this._resetTabToStoredState(tab);
+ }
- await this._resetTabToStoredState(tab);
+ async replacePinnedUrlWithCurrent(tab = undefined) {
+ tab ??= TabContextMenu.contextTab;
+ if (!tab || !tab.pinned || !tab.getAttribute('zen-pin-id')) {
+ return;
}
- async replacePinnedUrlWithCurrent(tab = undefined) {
- tab ??= TabContextMenu.contextTab;
- if (!tab || !tab.pinned || !tab.getAttribute('zen-pin-id')) {
- return;
- }
+ const browser = tab.linkedBrowser;
- const browser = tab.linkedBrowser;
+ const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
- const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
+ if (!pin) {
+ return;
+ }
- if (!pin) {
- return;
- }
+ const userContextId = tab.getAttribute('usercontextid');
- const userContextId = tab.getAttribute('usercontextid');
+ pin.title = tab.label || browser.contentTitle;
+ pin.url = browser.currentURI.spec;
+ pin.workspaceUuid = tab.getAttribute('zen-workspace-id');
+ pin.userContextId = userContextId ? parseInt(userContextId, 10) : 0;
- pin.title = tab.label || browser.contentTitle;
- pin.url = browser.currentURI.spec;
- pin.workspaceUuid = tab.getAttribute('zen-workspace-id');
- pin.userContextId = userContextId ? parseInt(userContextId, 10) : 0;
+ await this.savePin(pin);
+ this.resetPinChangedUrl(tab);
+ await this.refreshPinnedTabs();
+ gZenUIManager.showToast('zen-pinned-tab-replaced');
+ }
- await this.savePin(pin);
- this.resetPinChangedUrl(tab);
- await this.refreshPinnedTabs();
- gZenUIManager.showToast('zen-pinned-tab-replaced');
+ async _setPinnedAttributes(tab) {
+ if (
+ tab.hasAttribute('zen-pin-id') ||
+ !this._hasFinishedLoading ||
+ tab.hasAttribute('zen-empty-tab')
+ ) {
+ return;
}
- async _setPinnedAttributes(tab) {
- if (
- tab.hasAttribute('zen-pin-id') ||
- !this._hasFinishedLoading ||
- tab.hasAttribute('zen-empty-tab')
- ) {
- return;
- }
-
- this.log(`Setting pinned attributes for tab ${tab.linkedBrowser.currentURI.spec}`);
- const browser = tab.linkedBrowser;
-
- const uuid = gZenUIManager.generateUuidv4();
- const userContextId = tab.getAttribute('usercontextid');
-
- let entry = null;
+ this.log(`Setting pinned attributes for tab ${tab.linkedBrowser.currentURI.spec}`);
+ const browser = tab.linkedBrowser;
- if (tab.getAttribute('zen-pinned-entry')) {
- entry = JSON.parse(tab.getAttribute('zen-pinned-entry'));
- }
+ const uuid = gZenUIManager.generateUuidv4();
+ const userContextId = tab.getAttribute('usercontextid');
- await this.savePin({
- uuid,
- title: entry?.title || tab.label || browser.contentTitle,
- url: entry?.url || browser.currentURI.spec,
- containerTabId: userContextId ? parseInt(userContextId, 10) : 0,
- workspaceUuid: tab.getAttribute('zen-workspace-id'),
- isEssential: tab.getAttribute('zen-essential') === 'true',
- parentUuid: tab.group?.getAttribute('zen-pin-id') || null,
- position: tab._pPos,
- });
+ let entry = null;
- tab.setAttribute('zen-pin-id', uuid);
- tab.dispatchEvent(
- new CustomEvent('ZenPinnedTabCreated', {
- detail: { tab },
- })
- );
+ if (tab.getAttribute('zen-pinned-entry')) {
+ entry = JSON.parse(tab.getAttribute('zen-pinned-entry'));
+ }
- // This is used while migrating old pins to new system - we don't want to refresh when migrating
- if (tab.getAttribute('zen-pinned-entry')) {
- tab.removeAttribute('zen-pinned-entry');
- return;
- }
- this.onLocationChange(browser);
- await this.refreshPinnedTabs();
+ await this.savePin({
+ uuid,
+ title: entry?.title || tab.label || browser.contentTitle,
+ url: entry?.url || browser.currentURI.spec,
+ containerTabId: userContextId ? parseInt(userContextId, 10) : 0,
+ workspaceUuid: tab.getAttribute('zen-workspace-id'),
+ isEssential: tab.getAttribute('zen-essential') === 'true',
+ parentUuid: tab.group?.getAttribute('zen-pin-id') || null,
+ position: tab._pPos,
+ });
+
+ tab.setAttribute('zen-pin-id', uuid);
+ tab.dispatchEvent(
+ new CustomEvent('ZenPinnedTabCreated', {
+ detail: { tab },
+ })
+ );
+
+ // This is used while migrating old pins to new system - we don't want to refresh when migrating
+ if (tab.getAttribute('zen-pinned-entry')) {
+ tab.removeAttribute('zen-pinned-entry');
+ return;
}
+ this.onLocationChange(browser);
+ await this.refreshPinnedTabs();
+ }
- async _removePinnedAttributes(tab, isClosing = false) {
- tab.removeAttribute('zen-has-static-label');
- if (!tab.getAttribute('zen-pin-id') || this._temporarilyUnpiningEssential) {
- return;
- }
+ async _removePinnedAttributes(tab, isClosing = false) {
+ tab.removeAttribute('zen-has-static-label');
+ if (!tab.getAttribute('zen-pin-id') || this._temporarilyUnpiningEssential) {
+ return;
+ }
- if (Services.startup.shuttingDown || window.skipNextCanClose) {
- return;
- }
+ if (Services.startup.shuttingDown || window.skipNextCanClose) {
+ return;
+ }
- this.log(`Removing pinned attributes for tab ${tab.getAttribute('zen-pin-id')}`);
- await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
- this.resetPinChangedUrl(tab);
+ this.log(`Removing pinned attributes for tab ${tab.getAttribute('zen-pin-id')}`);
+ await ZenPinnedTabsStorage.removePin(tab.getAttribute('zen-pin-id'));
+ this.resetPinChangedUrl(tab);
- if (!isClosing) {
- tab.removeAttribute('zen-pin-id');
- tab.removeAttribute('zen-essential'); // Just in case
+ if (!isClosing) {
+ tab.removeAttribute('zen-pin-id');
+ tab.removeAttribute('zen-essential'); // Just in case
- if (!tab.hasAttribute('zen-workspace-id') && gZenWorkspaces.workspaceEnabled) {
- const workspace = await gZenWorkspaces.getActiveWorkspace();
- tab.setAttribute('zen-workspace-id', workspace.uuid);
- }
+ if (!tab.hasAttribute('zen-workspace-id') && gZenWorkspaces.workspaceEnabled) {
+ const workspace = await gZenWorkspaces.getActiveWorkspace();
+ tab.setAttribute('zen-workspace-id', workspace.uuid);
}
- await this.refreshPinnedTabs();
- tab.dispatchEvent(
- new CustomEvent('ZenPinnedTabRemoved', {
- detail: { tab },
- })
- );
}
+ await this.refreshPinnedTabs();
+ tab.dispatchEvent(
+ new CustomEvent('ZenPinnedTabRemoved', {
+ detail: { tab },
+ })
+ );
+ }
- _initClosePinnedTabShortcut() {
- let cmdClose = document.getElementById('cmd_close');
+ _initClosePinnedTabShortcut() {
+ let cmdClose = document.getElementById('cmd_close');
- if (cmdClose) {
- cmdClose.addEventListener('command', this.onCloseTabShortcut.bind(this));
- }
+ if (cmdClose) {
+ cmdClose.addEventListener('command', this.onCloseTabShortcut.bind(this));
+ }
+ }
+
+ async savePin(pin, notifyObservers = true) {
+ if (!this.hasInitializedPins && !gZenUIManager.testingEnabled) {
+ return;
+ }
+ const existingPin = this._pinsCache.find((p) => p.uuid === pin.uuid);
+ if (existingPin) {
+ Object.assign(existingPin, pin);
+ } else {
+ // We shouldn't need it, but just in case there's
+ // a race condition while making new pinned tabs.
+ this._pinsCache.push(pin);
}
+ await ZenPinnedTabsStorage.savePin(pin, notifyObservers);
+ }
+
+ async onCloseTabShortcut(
+ event,
+ selectedTab = gBrowser.selectedTab,
+ {
+ behavior = lazy.zenPinnedTabCloseShortcutBehavior,
+ noClose = false,
+ closeIfPending = false,
+ alwaysUnload = false,
+ folderToUnload = null,
+ } = {}
+ ) {
+ try {
+ const tabs = Array.isArray(selectedTab) ? selectedTab : [selectedTab];
+ const pinnedTabs = [
+ ...new Set(
+ tabs
+ .flatMap((tab) => {
+ if (tab.group?.hasAttribute('split-view-group')) {
+ return tab.group.tabs;
+ }
+ return tab;
+ })
+ .filter((tab) => tab?.pinned)
+ ),
+ ];
- async savePin(pin, notifyObservers = true) {
- if (!this.hasInitializedPins && !gZenUIManager.testingEnabled) {
+ if (!pinnedTabs.length) {
return;
}
- const existingPin = this._pinsCache.find((p) => p.uuid === pin.uuid);
- if (existingPin) {
- Object.assign(existingPin, pin);
- } else {
- // We shouldn't need it, but just in case there's
- // a race condition while making new pinned tabs.
- this._pinsCache.push(pin);
- }
- await ZenPinnedTabsStorage.savePin(pin, notifyObservers);
- }
-
- async onCloseTabShortcut(
- event,
- selectedTab = gBrowser.selectedTab,
- {
- behavior = lazy.zenPinnedTabCloseShortcutBehavior,
- noClose = false,
- closeIfPending = false,
- alwaysUnload = false,
- folderToUnload = null,
- } = {}
- ) {
- try {
- const tabs = Array.isArray(selectedTab) ? selectedTab : [selectedTab];
- const pinnedTabs = [
- ...new Set(
- tabs
- .flatMap((tab) => {
- if (tab.group?.hasAttribute('split-view-group')) {
- return tab.group.tabs;
- }
- return tab;
- })
- .filter((tab) => tab?.pinned)
- ),
- ];
-
- if (!pinnedTabs.length) {
- return;
- }
- const selectedTabs = pinnedTabs.filter((tab) => tab.selected);
+ const selectedTabs = pinnedTabs.filter((tab) => tab.selected);
- event.stopPropagation();
- event.preventDefault();
+ event.stopPropagation();
+ event.preventDefault();
- if (noClose && behavior === 'close') {
- behavior = 'unload-switch';
- }
+ if (noClose && behavior === 'close') {
+ behavior = 'unload-switch';
+ }
- if (alwaysUnload && ['close', 'reset', 'switch', 'reset-switch'].includes(behavior)) {
- behavior = behavior.contains('reset') ? 'reset-unload-switch' : 'unload-switch';
- }
+ if (alwaysUnload && ['close', 'reset', 'switch', 'reset-switch'].includes(behavior)) {
+ behavior = behavior.contains('reset') ? 'reset-unload-switch' : 'unload-switch';
+ }
- switch (behavior) {
- case 'close': {
- for (const tab of pinnedTabs) {
- this._removePinnedAttributes(tab, true);
- gBrowser.removeTab(tab, { animate: true });
- }
- break;
+ switch (behavior) {
+ case 'close': {
+ for (const tab of pinnedTabs) {
+ this._removePinnedAttributes(tab, true);
+ gBrowser.removeTab(tab, { animate: true });
}
- case 'reset-unload-switch':
- case 'unload-switch':
- case 'reset-switch':
- case 'switch':
- if (behavior.includes('unload')) {
- for (const tab of pinnedTabs) {
- if (tab.hasAttribute('glance-id')) {
- // We have a glance tab inside the tab we are trying to unload,
- // before we used to just ignore it but now we need to fully close
- // it as well.
- gZenGlanceManager.manageTabClose(tab.glanceTab);
- await new Promise((resolve) => {
- let hasRan = false;
- const onGlanceClose = () => {
- hasRan = true;
+ break;
+ }
+ case 'reset-unload-switch':
+ case 'unload-switch':
+ case 'reset-switch':
+ case 'switch':
+ if (behavior.includes('unload')) {
+ for (const tab of pinnedTabs) {
+ if (tab.hasAttribute('glance-id')) {
+ // We have a glance tab inside the tab we are trying to unload,
+ // before we used to just ignore it but now we need to fully close
+ // it as well.
+ gZenGlanceManager.manageTabClose(tab.glanceTab);
+ await new Promise((resolve) => {
+ let hasRan = false;
+ const onGlanceClose = () => {
+ hasRan = true;
+ resolve();
+ };
+ window.addEventListener('GlanceClose', onGlanceClose, { once: true });
+ // Set a timeout to resolve the promise if the event doesn't fire.
+ // We do this to prevent any future issues where glance woudnt close such as
+ // glance requering to ask for permit unload.
+ setTimeout(() => {
+ if (!hasRan) {
+ console.warn('GlanceClose event did not fire within 3 seconds');
resolve();
- };
- window.addEventListener('GlanceClose', onGlanceClose, { once: true });
- // Set a timeout to resolve the promise if the event doesn't fire.
- // We do this to prevent any future issues where glance woudnt close such as
- // glance requering to ask for permit unload.
- setTimeout(() => {
- if (!hasRan) {
- console.warn('GlanceClose event did not fire within 3 seconds');
- resolve();
- }
- }, 3000);
- });
- return;
- }
- const isSpltView = tab.group?.hasAttribute('split-view-group');
- const group = isSpltView ? tab.group.group : tab.group;
- if (!folderToUnload && tab.hasAttribute('folder-active')) {
- await gZenFolders.animateUnload(group, tab);
- }
- }
- if (folderToUnload) {
- await gZenFolders.animateUnloadAll(folderToUnload);
- }
- const allAreUnloaded = pinnedTabs.every(
- (tab) => tab.hasAttribute('pending') && !tab.hasAttribute('zen-essential')
- );
- for (const tab of pinnedTabs) {
- if (allAreUnloaded && closeIfPending) {
- return await this.onCloseTabShortcut(event, tab, { behavior: 'close' });
- }
+ }
+ }, 3000);
+ });
+ return;
}
- await gBrowser.explicitUnloadTabs(pinnedTabs);
- for (const tab of pinnedTabs) {
- tab.removeAttribute('discarded');
+ const isSpltView = tab.group?.hasAttribute('split-view-group');
+ const group = isSpltView ? tab.group.group : tab.group;
+ if (!folderToUnload && tab.hasAttribute('folder-active')) {
+ await gZenFolders.animateUnload(group, tab);
}
}
- if (selectedTabs.length) {
- this._handleTabSwitch(selectedTabs[0]);
+ if (folderToUnload) {
+ await gZenFolders.animateUnloadAll(folderToUnload);
}
- if (behavior.includes('reset')) {
- for (const tab of pinnedTabs) {
- this._resetTabToStoredState(tab);
+ const allAreUnloaded = pinnedTabs.every(
+ (tab) => tab.hasAttribute('pending') && !tab.hasAttribute('zen-essential')
+ );
+ for (const tab of pinnedTabs) {
+ if (allAreUnloaded && closeIfPending) {
+ return await this.onCloseTabShortcut(event, tab, { behavior: 'close' });
}
}
- break;
- case 'reset':
+ await gBrowser.explicitUnloadTabs(pinnedTabs);
+ for (const tab of pinnedTabs) {
+ tab.removeAttribute('discarded');
+ }
+ }
+ if (selectedTabs.length) {
+ this._handleTabSwitch(selectedTabs[0]);
+ }
+ if (behavior.includes('reset')) {
for (const tab of pinnedTabs) {
this._resetTabToStoredState(tab);
}
- break;
- default:
- return;
- }
- } catch (ex) {
- console.error('Error handling close tab shortcut for pinned tab:', ex);
+ }
+ break;
+ case 'reset':
+ for (const tab of pinnedTabs) {
+ this._resetTabToStoredState(tab);
+ }
+ break;
+ default:
+ return;
}
+ } catch (ex) {
+ console.error('Error handling close tab shortcut for pinned tab:', ex);
}
+ }
- _handleTabSwitch(selectedTab) {
- if (selectedTab !== gBrowser.selectedTab) {
- return;
- }
- const findNextTab = (direction) =>
- gBrowser.tabContainer.findNextTab(selectedTab, {
- direction,
- filter: (tab) => !tab.hidden && !tab.pinned,
- });
-
- let nextTab = findNextTab(1) || findNextTab(-1);
+ _handleTabSwitch(selectedTab) {
+ if (selectedTab !== gBrowser.selectedTab) {
+ return;
+ }
+ const findNextTab = (direction) =>
+ gBrowser.tabContainer.findNextTab(selectedTab, {
+ direction,
+ filter: (tab) => !tab.hidden && !tab.pinned,
+ });
- if (!nextTab) {
- gZenWorkspaces.selectEmptyTab();
- return;
- }
+ let nextTab = findNextTab(1) || findNextTab(-1);
- if (nextTab) {
- gBrowser.selectedTab = nextTab;
- }
+ if (!nextTab) {
+ gZenWorkspaces.selectEmptyTab();
+ return;
}
- _resetTabToStoredState(tab) {
- const id = tab.getAttribute('zen-pin-id');
- if (!id) {
- return;
- }
+ if (nextTab) {
+ gBrowser.selectedTab = nextTab;
+ }
+ }
- const pin = this._pinsCache.find((pin) => pin.uuid === id);
- if (!pin) {
- return;
- }
+ _resetTabToStoredState(tab) {
+ const id = tab.getAttribute('zen-pin-id');
+ if (!id) {
+ return;
+ }
- const tabState = SessionStore.getTabState(tab);
- const state = JSON.parse(tabState);
-
- const foundEntryIndex = state.entries?.findIndex((entry) => entry.url === pin.url);
- if (foundEntryIndex === -1) {
- state.entries = [
- {
- url: pin.url,
- title: pin.title,
- triggeringPrincipal_base64: lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
- },
- ];
- } else {
- // Remove everything except the entry we want to keep
- const existingEntry = state.entries[foundEntryIndex];
- existingEntry.title = pin.title;
- state.entries = [existingEntry];
- }
- state.image = pin.iconUrl || state.image;
- state.index = 0;
+ const pin = this._pinsCache.find((pin) => pin.uuid === id);
+ if (!pin) {
+ return;
+ }
- SessionStore.setTabState(tab, state);
- this.resetPinChangedUrl(tab);
+ const tabState = SessionStore.getTabState(tab);
+ const state = JSON.parse(tabState);
+
+ const foundEntryIndex = state.entries?.findIndex((entry) => entry.url === pin.url);
+ if (foundEntryIndex === -1) {
+ state.entries = [
+ {
+ url: pin.url,
+ title: pin.title,
+ triggeringPrincipal_base64: lazy.E10SUtils.SERIALIZED_SYSTEMPRINCIPAL,
+ },
+ ];
+ } else {
+ // Remove everything except the entry we want to keep
+ const existingEntry = state.entries[foundEntryIndex];
+ existingEntry.title = pin.title;
+ state.entries = [existingEntry];
}
+ state.image = pin.iconUrl || state.image;
+ state.index = 0;
- async getFaviconAsBase64(pageUrl) {
- try {
- const faviconData = await PlacesUtils.favicons.getFaviconForPage(pageUrl);
- if (!faviconData) {
- // empty favicon
- return null;
- }
- return faviconData.dataURI;
- } catch (ex) {
- console.error('Failed to get favicon:', ex);
+ SessionStore.setTabState(tab, state);
+ this.resetPinChangedUrl(tab);
+ }
+
+ async getFaviconAsBase64(pageUrl) {
+ try {
+ const faviconData = await PlacesUtils.favicons.getFaviconForPage(pageUrl);
+ if (!faviconData) {
+ // empty favicon
return null;
}
+ return faviconData.dataURI;
+ } catch (ex) {
+ console.error('Failed to get favicon:', ex);
+ return null;
}
+ }
- addToEssentials(tab) {
- const tabs = tab
- ? // if it's already an array, dont make it [tab]
- tab?.length
- ? tab
- : [tab]
- : TabContextMenu.contextTab.multiselected
- ? gBrowser.selectedTabs
- : [TabContextMenu.contextTab];
- let movedAll = true;
- for (let i = 0; i < tabs.length; i++) {
- let tab = tabs[i];
- const section = gZenWorkspaces.getEssentialsSection(tab);
- if (!this.canEssentialBeAdded(tab)) {
- movedAll = false;
- continue;
- }
- if (tab.hasAttribute('zen-essential')) {
- continue;
- }
- tab.setAttribute('zen-essential', 'true');
- if (tab.hasAttribute('zen-workspace-id')) {
- tab.removeAttribute('zen-workspace-id');
+ addToEssentials(tab) {
+ const tabs = tab
+ ? // if it's already an array, dont make it [tab]
+ tab?.length
+ ? tab
+ : [tab]
+ : TabContextMenu.contextTab.multiselected
+ ? gBrowser.selectedTabs
+ : [TabContextMenu.contextTab];
+ let movedAll = true;
+ for (let i = 0; i < tabs.length; i++) {
+ let tab = tabs[i];
+ const section = gZenWorkspaces.getEssentialsSection(tab);
+ if (!this.canEssentialBeAdded(tab)) {
+ movedAll = false;
+ continue;
+ }
+ if (tab.hasAttribute('zen-essential')) {
+ continue;
+ }
+ tab.setAttribute('zen-essential', 'true');
+ if (tab.hasAttribute('zen-workspace-id')) {
+ tab.removeAttribute('zen-workspace-id');
+ }
+ if (tab.pinned && tab.hasAttribute('zen-pin-id')) {
+ const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
+ if (pin) {
+ pin.isEssential = true;
+ pin.workspaceUuid = null;
+ this.savePin(pin);
}
- if (tab.pinned && tab.hasAttribute('zen-pin-id')) {
- const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
- if (pin) {
- pin.isEssential = true;
- pin.workspaceUuid = null;
- this.savePin(pin);
+ gBrowser.zenHandleTabMove(tab, () => {
+ if (tab.ownerGlobal !== window) {
+ tab = gBrowser.adoptTab(tab, {
+ selectTab: tab.selected,
+ });
+ tab.setAttribute('zen-essential', 'true');
+ } else {
+ section.appendChild(tab);
}
- gBrowser.zenHandleTabMove(tab, () => {
- if (tab.ownerGlobal !== window) {
- tab = gBrowser.adoptTab(tab, {
- selectTab: tab.selected,
- });
- tab.setAttribute('zen-essential', 'true');
- } else {
- section.appendChild(tab);
- }
- });
- } else {
- gBrowser.pinTab(tab);
- this._ignoreNextTabPinnedEvent = true;
- }
- tab.setAttribute('zenDefaultUserContextId', true);
- if (tab.selected) {
- gZenWorkspaces.switchTabIfNeeded(tab);
- }
- this.onTabIconChanged(tab);
- // Dispatch the event to update the UI
- const event = new CustomEvent('TabAddedToEssentials', {
- detail: { tab },
- bubbles: true,
- cancelable: false,
});
- tab.dispatchEvent(event);
- }
- gZenUIManager.updateTabsToolbar();
- return movedAll;
+ } else {
+ gBrowser.pinTab(tab);
+ this._ignoreNextTabPinnedEvent = true;
+ }
+ tab.setAttribute('zenDefaultUserContextId', true);
+ if (tab.selected) {
+ gZenWorkspaces.switchTabIfNeeded(tab);
+ }
+ this.onTabIconChanged(tab);
+ // Dispatch the event to update the UI
+ const event = new CustomEvent('TabAddedToEssentials', {
+ detail: { tab },
+ bubbles: true,
+ cancelable: false,
+ });
+ tab.dispatchEvent(event);
}
+ gZenUIManager.updateTabsToolbar();
+ return movedAll;
+ }
- removeEssentials(tab, unpin = true) {
- const tabs = tab
- ? [tab]
- : TabContextMenu.contextTab.multiselected
- ? gBrowser.selectedTabs
- : [TabContextMenu.contextTab];
- for (let i = 0; i < tabs.length; i++) {
- const tab = tabs[i];
- tab.removeAttribute('zen-essential');
- if (gZenWorkspaces.workspaceEnabled && gZenWorkspaces.getActiveWorkspaceFromCache().uuid) {
- tab.setAttribute('zen-workspace-id', gZenWorkspaces.getActiveWorkspaceFromCache().uuid);
- }
- if (unpin) {
- gBrowser.unpinTab(tab);
- } else {
- gBrowser.zenHandleTabMove(tab, () => {
- const pinContainer = gZenWorkspaces.pinnedTabsContainer;
- pinContainer.prepend(tab);
- });
- }
- // Dispatch the event to update the UI
- const event = new CustomEvent('TabRemovedFromEssentials', {
- detail: { tab },
- bubbles: true,
- cancelable: false,
+ removeEssentials(tab, unpin = true) {
+ const tabs = tab
+ ? [tab]
+ : TabContextMenu.contextTab.multiselected
+ ? gBrowser.selectedTabs
+ : [TabContextMenu.contextTab];
+ for (let i = 0; i < tabs.length; i++) {
+ const tab = tabs[i];
+ tab.removeAttribute('zen-essential');
+ if (gZenWorkspaces.workspaceEnabled && gZenWorkspaces.getActiveWorkspaceFromCache().uuid) {
+ tab.setAttribute('zen-workspace-id', gZenWorkspaces.getActiveWorkspaceFromCache().uuid);
+ }
+ if (unpin) {
+ gBrowser.unpinTab(tab);
+ } else {
+ gBrowser.zenHandleTabMove(tab, () => {
+ const pinContainer = gZenWorkspaces.pinnedTabsContainer;
+ pinContainer.prepend(tab);
});
- tab.dispatchEvent(event);
}
- gZenUIManager.updateTabsToolbar();
+ // Dispatch the event to update the UI
+ const event = new CustomEvent('TabRemovedFromEssentials', {
+ detail: { tab },
+ bubbles: true,
+ cancelable: false,
+ });
+ tab.dispatchEvent(event);
}
+ gZenUIManager.updateTabsToolbar();
+ }
- _insertItemsIntoTabContextMenu() {
- if (!this.enabled) {
- return;
- }
- const elements = window.MozXULElement.parseXULToFragment(`
+ _insertItemsIntoTabContextMenu() {
+ if (!this.enabled) {
+ return;
+ }
+ const elements = window.MozXULElement.parseXULToFragment(`
`);
- document.getElementById('tabContextMenu').appendChild(elements);
+ document.getElementById('tabContextMenu').appendChild(elements);
- const element = window.MozXULElement.parseXULToFragment(`
+ const element = window.MozXULElement.parseXULToFragment(`
`);
- document.getElementById('context_pinTab')?.before(element);
+ document.getElementById('context_pinTab')?.before(element);
+ }
+
+ async updatePinnedTabContextMenu(contextTab) {
+ if (!this.enabled) {
+ document.getElementById('context_pinTab').hidden = true;
+ return;
}
+ const isVisible = contextTab.pinned && !contextTab.multiselected;
+ const zenAddEssential = document.getElementById('context_zen-add-essential');
+ document.getElementById('context_zen-reset-pinned-tab').hidden =
+ !isVisible || !contextTab.getAttribute('zen-pin-id');
+ document.getElementById('context_zen-replace-pinned-url-with-current').hidden = !isVisible;
+ zenAddEssential.hidden = contextTab.getAttribute('zen-essential') || !!contextTab.group;
+ zenAddEssential.setAttribute(
+ 'badge',
+ await document.l10n.formatValue('tab-context-zen-add-essential-badge', {
+ num: gBrowser._numZenEssentials,
+ max: this.maxEssentialTabs,
+ })
+ );
+ document
+ .getElementById('cmd_contextZenAddToEssentials')
+ .setAttribute('disabled', !this.canEssentialBeAdded(contextTab));
+ document.getElementById('context_closeTab').hidden = contextTab.hasAttribute('zen-essential');
+ document.getElementById('context_zen-remove-essential').hidden =
+ !contextTab.getAttribute('zen-essential');
+ document.getElementById('zen-context-menu-new-folder').hidden =
+ contextTab.getAttribute('zen-essential');
+ document.getElementById('context_unpinTab').hidden =
+ document.getElementById('context_unpinTab').hidden ||
+ contextTab.getAttribute('zen-essential');
+ document.getElementById('context_unpinSelectedTabs').hidden =
+ document.getElementById('context_unpinSelectedTabs').hidden ||
+ contextTab.getAttribute('zen-essential');
+ document.getElementById('context_zen-pinned-tab-separator').hidden = !isVisible;
+ }
- async updatePinnedTabContextMenu(contextTab) {
- if (!this.enabled) {
- document.getElementById('context_pinTab').hidden = true;
- return;
- }
- const isVisible = contextTab.pinned && !contextTab.multiselected;
- const zenAddEssential = document.getElementById('context_zen-add-essential');
- document.getElementById('context_zen-reset-pinned-tab').hidden =
- !isVisible || !contextTab.getAttribute('zen-pin-id');
- document.getElementById('context_zen-replace-pinned-url-with-current').hidden = !isVisible;
- zenAddEssential.hidden = contextTab.getAttribute('zen-essential') || !!contextTab.group;
- zenAddEssential.setAttribute(
- 'badge',
- await document.l10n.formatValue('tab-context-zen-add-essential-badge', {
- num: gBrowser._numZenEssentials,
- max: this.maxEssentialTabs,
- })
- );
- document
- .getElementById('cmd_contextZenAddToEssentials')
- .setAttribute('disabled', !this.canEssentialBeAdded(contextTab));
- document.getElementById('context_closeTab').hidden = contextTab.hasAttribute('zen-essential');
- document.getElementById('context_zen-remove-essential').hidden =
- !contextTab.getAttribute('zen-essential');
- document.getElementById('zen-context-menu-new-folder').hidden =
- contextTab.getAttribute('zen-essential');
- document.getElementById('context_unpinTab').hidden =
- document.getElementById('context_unpinTab').hidden ||
- contextTab.getAttribute('zen-essential');
- document.getElementById('context_unpinSelectedTabs').hidden =
- document.getElementById('context_unpinSelectedTabs').hidden ||
- contextTab.getAttribute('zen-essential');
- document.getElementById('context_zen-pinned-tab-separator').hidden = !isVisible;
- }
-
- moveToAnotherTabContainerIfNecessary(event, movingTabs) {
- if (!this.enabled) {
- return false;
- }
- movingTabs = [...movingTabs];
- try {
- const pinnedTabsTarget =
- event.target.closest('.zen-current-workspace-indicator') || this._isGoingToPinnedTabs;
- const essentialTabsTarget = event.target.closest('.zen-essentials-container');
- const tabsTarget = !this._isGoingToPinnedTabs;
-
- // TODO: Solve the issue of adding a tab between two groups
- // Remove group labels from the moving tabs and replace it
- // with the sub tabs
- for (let i = 0; i < movingTabs.length; i++) {
- const draggedTab = movingTabs[i];
- if (gBrowser.isTabGroupLabel(draggedTab)) {
- const group = draggedTab.group;
- // remove label and add sub tabs to moving tabs
- if (group) {
- movingTabs.splice(i, 1, ...group.tabs);
- }
+ moveToAnotherTabContainerIfNecessary(event, movingTabs) {
+ if (!this.enabled) {
+ return false;
+ }
+ movingTabs = [...movingTabs];
+ try {
+ const pinnedTabsTarget =
+ event.target.closest('.zen-current-workspace-indicator') || this._isGoingToPinnedTabs;
+ const essentialTabsTarget = event.target.closest('.zen-essentials-container');
+ const tabsTarget = !this._isGoingToPinnedTabs;
+
+ // TODO: Solve the issue of adding a tab between two groups
+ // Remove group labels from the moving tabs and replace it
+ // with the sub tabs
+ for (let i = 0; i < movingTabs.length; i++) {
+ const draggedTab = movingTabs[i];
+ if (gBrowser.isTabGroupLabel(draggedTab)) {
+ const group = draggedTab.group;
+ // remove label and add sub tabs to moving tabs
+ if (group) {
+ movingTabs.splice(i, 1, ...group.tabs);
}
}
+ }
- let isVertical = this.expandedSidebarMode;
- let moved = false;
- let hasActuallyMoved;
- for (const draggedTab of movingTabs) {
- let isRegularTabs = false;
- // Check for essentials container
- if (essentialTabsTarget) {
- if (!draggedTab.hasAttribute('zen-essential') && !draggedTab?.group) {
- moved = true;
- isVertical = false;
- hasActuallyMoved = this.addToEssentials(draggedTab);
- }
+ let isVertical = this.expandedSidebarMode;
+ let moved = false;
+ let hasActuallyMoved;
+ for (const draggedTab of movingTabs) {
+ let isRegularTabs = false;
+ // Check for essentials container
+ if (essentialTabsTarget) {
+ if (!draggedTab.hasAttribute('zen-essential') && !draggedTab?.group) {
+ moved = true;
+ isVertical = false;
+ hasActuallyMoved = this.addToEssentials(draggedTab);
}
- // Check for pinned tabs container
- else if (pinnedTabsTarget) {
- if (!draggedTab.pinned) {
- gBrowser.pinTab(draggedTab);
- } else if (draggedTab.hasAttribute('zen-essential')) {
- this.removeEssentials(draggedTab, false);
- moved = true;
- }
+ }
+ // Check for pinned tabs container
+ else if (pinnedTabsTarget) {
+ if (!draggedTab.pinned) {
+ gBrowser.pinTab(draggedTab);
+ } else if (draggedTab.hasAttribute('zen-essential')) {
+ this.removeEssentials(draggedTab, false);
+ moved = true;
}
- // Check for normal tabs container
- else if (tabsTarget || event.target.id === 'zen-tabs-wrapper') {
- if (draggedTab.pinned && !draggedTab.hasAttribute('zen-essential')) {
- gBrowser.unpinTab(draggedTab);
- isRegularTabs = true;
- } else if (draggedTab.hasAttribute('zen-essential')) {
- this.removeEssentials(draggedTab);
- moved = true;
- isRegularTabs = true;
- }
+ }
+ // Check for normal tabs container
+ else if (tabsTarget || event.target.id === 'zen-tabs-wrapper') {
+ if (draggedTab.pinned && !draggedTab.hasAttribute('zen-essential')) {
+ gBrowser.unpinTab(draggedTab);
+ isRegularTabs = true;
+ } else if (draggedTab.hasAttribute('zen-essential')) {
+ this.removeEssentials(draggedTab);
+ moved = true;
+ isRegularTabs = true;
}
+ }
+
+ if (typeof hasActuallyMoved === 'undefined') {
+ hasActuallyMoved = moved;
+ }
- if (typeof hasActuallyMoved === 'undefined') {
- hasActuallyMoved = moved;
+ // If the tab was moved, adjust its position relative to the target tab
+ if (hasActuallyMoved) {
+ const targetTab = event.target.closest('.tabbrowser-tab');
+ const targetFolder = event.target.closest('zen-folder');
+ let targetElem = targetTab || targetFolder?.labelElement;
+ if (targetElem?.group?.activeGroups?.length > 0) {
+ const activeGroup = targetElem.group.activeGroups.at(-1);
+ targetElem = activeGroup.labelElement;
}
+ if (targetElem) {
+ const rect = targetElem.getBoundingClientRect();
+ let elementIndex = targetElem.elementIndex;
- // If the tab was moved, adjust its position relative to the target tab
- if (hasActuallyMoved) {
- const targetTab = event.target.closest('.tabbrowser-tab');
- const targetFolder = event.target.closest('zen-folder');
- let targetElem = targetTab || targetFolder?.labelElement;
- if (targetElem?.group?.activeGroups?.length > 0) {
- const activeGroup = targetElem.group.activeGroups.at(-1);
- targetElem = activeGroup.labelElement;
- }
- if (targetElem) {
- const rect = targetElem.getBoundingClientRect();
- let elementIndex = targetElem.elementIndex;
-
- if (isVertical || !this.expandedSidebarMode) {
- const middleY = targetElem.screenY + rect.height / 2;
- if (!isRegularTabs && event.screenY > middleY) {
- elementIndex++;
- } else if (isRegularTabs && event.screenY < middleY) {
- elementIndex--;
- }
- } else {
- const middleX = targetElem.screenX + rect.width / 2;
- if (event.screenX > middleX) {
- elementIndex++;
- }
+ if (isVertical || !this.expandedSidebarMode) {
+ const middleY = targetElem.screenY + rect.height / 2;
+ if (!isRegularTabs && event.screenY > middleY) {
+ elementIndex++;
+ } else if (isRegularTabs && event.screenY < middleY) {
+ elementIndex--;
}
- // If it's the last tab, move it to the end
- if (tabsTarget === gBrowser.tabs.at(-1)) {
+ } else {
+ const middleX = targetElem.screenX + rect.width / 2;
+ if (event.screenX > middleX) {
elementIndex++;
}
-
- gBrowser.moveTabTo(draggedTab, {
- elementIndex,
- forceUngrouped: targetElem?.group?.collapsed !== false,
- });
}
+ // If it's the last tab, move it to the end
+ if (tabsTarget === gBrowser.tabs.at(-1)) {
+ elementIndex++;
+ }
+
+ gBrowser.moveTabTo(draggedTab, {
+ elementIndex,
+ forceUngrouped: targetElem?.group?.collapsed !== false,
+ });
}
}
-
- return moved;
- } catch (ex) {
- console.error('Error moving tabs:', ex);
- return false;
}
+
+ return moved;
+ } catch (ex) {
+ console.error('Error moving tabs:', ex);
+ return false;
}
+ }
- async onLocationChange(browser) {
- const tab = gBrowser.getTabForBrowser(browser);
- if (!tab || !tab.pinned || tab.hasAttribute('zen-essential') || !this._pinsCache) {
- return;
- }
- const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
- if (!pin) {
- return;
- }
- // Remove # and ? from the URL
- const pinUrl = pin.url.split('#')[0];
- const currentUrl = browser.currentURI.spec.split('#')[0];
- // Add an indicator that the pin has been changed
- if (pinUrl === currentUrl) {
- this.resetPinChangedUrl(tab);
- return;
- }
- this.pinHasChangedUrl(tab, pin);
+ async onLocationChange(browser) {
+ const tab = gBrowser.getTabForBrowser(browser);
+ if (!tab || !tab.pinned || tab.hasAttribute('zen-essential') || !this._pinsCache) {
+ return;
+ }
+ const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
+ if (!pin) {
+ return;
+ }
+ // Remove # and ? from the URL
+ const pinUrl = pin.url.split('#')[0];
+ const currentUrl = browser.currentURI.spec.split('#')[0];
+ // Add an indicator that the pin has been changed
+ if (pinUrl === currentUrl) {
+ this.resetPinChangedUrl(tab);
+ return;
}
+ this.pinHasChangedUrl(tab, pin);
+ }
- resetPinChangedUrl(tab) {
- if (!tab.hasAttribute('zen-pinned-changed')) {
- return;
- }
- tab.removeAttribute('zen-pinned-changed');
- tab.removeAttribute('had-zen-pinned-changed');
- tab.style.removeProperty('--zen-original-tab-icon');
+ resetPinChangedUrl(tab) {
+ if (!tab.hasAttribute('zen-pinned-changed')) {
+ return;
}
+ tab.removeAttribute('zen-pinned-changed');
+ tab.removeAttribute('had-zen-pinned-changed');
+ tab.style.removeProperty('--zen-original-tab-icon');
+ }
- pinHasChangedUrl(tab, pin) {
- if (tab.hasAttribute('zen-pinned-changed')) {
- return;
- }
- if (tab.group?.hasAttribute('split-view-group')) {
- tab.setAttribute('had-zen-pinned-changed', 'true');
- } else {
- tab.setAttribute('zen-pinned-changed', 'true');
- }
- tab.style.setProperty('--zen-original-tab-icon', `url(${pin.iconUrl?.spec})`);
+ pinHasChangedUrl(tab, pin) {
+ if (tab.hasAttribute('zen-pinned-changed')) {
+ return;
+ }
+ if (tab.group?.hasAttribute('split-view-group')) {
+ tab.setAttribute('had-zen-pinned-changed', 'true');
+ } else {
+ tab.setAttribute('zen-pinned-changed', 'true');
}
+ tab.style.setProperty('--zen-original-tab-icon', `url(${pin.iconUrl?.spec})`);
+ }
- removeTabContainersDragoverClass(hideIndicator = true) {
- if (this._dragIndicator) {
- Services.zen.playHapticFeedback();
- }
- this.dragIndicator.remove();
- this._dragIndicator = null;
- if (hideIndicator) {
- gZenWorkspaces.activeWorkspaceIndicator?.removeAttribute('open');
- }
+ removeTabContainersDragoverClass(hideIndicator = true) {
+ if (this._dragIndicator) {
+ Services.zen.playHapticFeedback();
+ }
+ this.dragIndicator.remove();
+ this._dragIndicator = null;
+ if (hideIndicator) {
+ gZenWorkspaces.activeWorkspaceIndicator?.removeAttribute('open');
}
+ }
- onDragFinish() {
- for (const item of this.dragShiftableItems) {
- item.style.transform = '';
- }
- delete this._topToNormalTabs;
- for (const item of gBrowser.tabContainer.ariaFocusableItems) {
- if (gBrowser.isTab(item)) {
- let isVisible = true;
- let parent = item.group;
- while (parent) {
- if (!parent.visible) {
- isVisible = false;
- break;
- }
- parent = parent.group;
- }
- if (!isVisible) {
- continue;
+ onDragFinish() {
+ for (const item of this.dragShiftableItems) {
+ item.style.transform = '';
+ }
+ delete this._topToNormalTabs;
+ for (const item of gBrowser.tabContainer.ariaFocusableItems) {
+ if (gBrowser.isTab(item)) {
+ let isVisible = true;
+ let parent = item.group;
+ while (parent) {
+ if (!parent.visible) {
+ isVisible = false;
+ break;
}
+ parent = parent.group;
+ }
+ if (!isVisible) {
+ continue;
}
- const itemToAnimate =
- item.group?.hasAttribute('split-view-group') || gBrowser.isTabGroupLabel(item)
- ? item.group
- : item;
- itemToAnimate.style.removeProperty('--zen-folder-indent');
}
- this.removeTabContainersDragoverClass();
+ const itemToAnimate =
+ item.group?.hasAttribute('split-view-group') || gBrowser.isTabGroupLabel(item)
+ ? item.group
+ : item;
+ itemToAnimate.style.removeProperty('--zen-folder-indent');
}
+ this.removeTabContainersDragoverClass();
+ }
- get dragShiftableItems() {
- const separator = gZenWorkspaces.pinnedTabsContainer.querySelector(
- '.pinned-tabs-container-separator'
- );
- // Make sure to always return the separator at the start of the array
- return Services.prefs.getBoolPref('zen.view.show-newtab-button-top')
- ? [separator, gZenWorkspaces.activeWorkspaceElement.newTabButton]
- : [separator];
- }
+ get dragShiftableItems() {
+ const separator = gZenWorkspaces.pinnedTabsContainer.querySelector(
+ '.pinned-tabs-container-separator'
+ );
+ // Make sure to always return the separator at the start of the array
+ return Services.prefs.getBoolPref('zen.view.show-newtab-button-top')
+ ? [separator, gZenWorkspaces.activeWorkspaceElement.newTabButton]
+ : [separator];
+ }
- animateSeparatorMove(movingTabs, dropElement, isPinned) {
- let draggedTab = movingTabs[0];
- if (gBrowser.isTabGroupLabel(draggedTab) && draggedTab.group.isZenFolder) {
- this._isGoingToPinnedTabs = true;
- return;
- }
- if (draggedTab?.group?.hasAttribute('split-view-group')) {
- draggedTab = draggedTab.group;
- }
- const itemsToCheck = this.dragShiftableItems;
- let translate = movingTabs[isPinned ? movingTabs.length - 1 : 0].getBoundingClientRect().top;
- if (isPinned) {
- const rect = draggedTab.getBoundingClientRect();
- translate += rect.height;
- }
- const draggingTabHeight = movingTabs.reduce((acc, item) => {
- return acc + window.windowUtils.getBoundsWithoutFlushing(item).height;
- }, 0);
- if (typeof this._topToNormalTabs === 'undefined') {
- const rects = itemsToCheck.map((item) => window.windowUtils.getBoundsWithoutFlushing(item));
- this._topToNormalTabs = rects[0].top + rects.at(-1).height / (isPinned ? 2 : 4);
- }
- let topToNormalTabs = this._topToNormalTabs;
- const isGoingToPinnedTabs =
- translate < topToNormalTabs && gBrowser.pinnedTabCount - gBrowser._numZenEssentials > 0;
- const multiplier = isGoingToPinnedTabs !== isPinned ? (isGoingToPinnedTabs ? 1 : -1) : 0;
- this._isGoingToPinnedTabs = isGoingToPinnedTabs;
- if (!dropElement) {
- itemsToCheck.forEach((item) => {
- item.style.transform = `translateY(${draggingTabHeight * multiplier}px)`;
- });
- }
+ animateSeparatorMove(movingTabs, dropElement, isPinned) {
+ let draggedTab = movingTabs[0];
+ if (gBrowser.isTabGroupLabel(draggedTab) && draggedTab.group.isZenFolder) {
+ this._isGoingToPinnedTabs = true;
+ return;
}
-
- getLastTabBound(lastBound, lastTab, isDraggingFolder = false) {
- if (!lastTab.pinned || isDraggingFolder) {
- return lastBound;
- }
- const shiftedItems = this.dragShiftableItems;
- let totalHeight = shiftedItems.reduce((acc, item) => {
- return acc + window.windowUtils.getBoundsWithoutFlushing(item).height;
- }, 0);
- if (shiftedItems.length === 1) {
- // Means the new tab button is not at the top or not visible
- const lastTabRect = window.windowUtils.getBoundsWithoutFlushing(lastTab);
- totalHeight += lastTabRect.height;
- }
- return lastBound + totalHeight + 6;
+ if (draggedTab?.group?.hasAttribute('split-view-group')) {
+ draggedTab = draggedTab.group;
+ }
+ const itemsToCheck = this.dragShiftableItems;
+ let translate = movingTabs[isPinned ? movingTabs.length - 1 : 0].getBoundingClientRect().top;
+ if (isPinned) {
+ const rect = draggedTab.getBoundingClientRect();
+ translate += rect.height;
+ }
+ const draggingTabHeight = movingTabs.reduce((acc, item) => {
+ return acc + window.windowUtils.getBoundsWithoutFlushing(item).height;
+ }, 0);
+ if (typeof this._topToNormalTabs === 'undefined') {
+ const rects = itemsToCheck.map((item) => window.windowUtils.getBoundsWithoutFlushing(item));
+ this._topToNormalTabs = rects[0].top + rects.at(-1).height / (isPinned ? 2 : 4);
+ }
+ let topToNormalTabs = this._topToNormalTabs;
+ const isGoingToPinnedTabs =
+ translate < topToNormalTabs && gBrowser.pinnedTabCount - gBrowser._numZenEssentials > 0;
+ const multiplier = isGoingToPinnedTabs !== isPinned ? (isGoingToPinnedTabs ? 1 : -1) : 0;
+ this._isGoingToPinnedTabs = isGoingToPinnedTabs;
+ if (!dropElement) {
+ itemsToCheck.forEach((item) => {
+ item.style.transform = `translateY(${draggingTabHeight * multiplier}px)`;
+ });
}
+ }
- get dragIndicator() {
- if (!this._dragIndicator) {
- this._dragIndicator = document.createElement('div');
- this._dragIndicator.id = 'zen-drag-indicator';
- gNavToolbox.appendChild(this._dragIndicator);
- }
- return this._dragIndicator;
+ getLastTabBound(lastBound, lastTab, isDraggingFolder = false) {
+ if (!lastTab.pinned || isDraggingFolder) {
+ return lastBound;
+ }
+ const shiftedItems = this.dragShiftableItems;
+ let totalHeight = shiftedItems.reduce((acc, item) => {
+ return acc + window.windowUtils.getBoundsWithoutFlushing(item).height;
+ }, 0);
+ if (shiftedItems.length === 1) {
+ // Means the new tab button is not at the top or not visible
+ const lastTabRect = window.windowUtils.getBoundsWithoutFlushing(lastTab);
+ totalHeight += lastTabRect.height;
}
+ return lastBound + totalHeight + 6;
+ }
- get expandedSidebarMode() {
- return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true';
+ get dragIndicator() {
+ if (!this._dragIndicator) {
+ this._dragIndicator = document.createElement('div');
+ this._dragIndicator.id = 'zen-drag-indicator';
+ gNavToolbox.appendChild(this._dragIndicator);
}
+ return this._dragIndicator;
+ }
- async updatePinTitle(tab, newTitle, isEdited = true, notifyObservers = true) {
- const uuid = tab.getAttribute('zen-pin-id');
- await ZenPinnedTabsStorage.updatePinTitle(uuid, newTitle, isEdited, notifyObservers);
+ get expandedSidebarMode() {
+ return document.documentElement.getAttribute('zen-sidebar-expanded') === 'true';
+ }
- await this.refreshPinnedTabs();
+ async updatePinTitle(tab, newTitle, isEdited = true, notifyObservers = true) {
+ const uuid = tab.getAttribute('zen-pin-id');
+ await ZenPinnedTabsStorage.updatePinTitle(uuid, newTitle, isEdited, notifyObservers);
- const browsers = Services.wm.getEnumerator('navigator:browser');
+ await this.refreshPinnedTabs();
- // update the label for the same pin across all windows
- for (const browser of browsers) {
- const tabs = browser.gBrowser.tabs;
- // Fix pinned cache for the browser
- const browserCache = browser.gZenPinnedTabManager?._pinsCache;
- if (browserCache) {
- const pin = browserCache.find((pin) => pin.uuid === uuid);
- if (pin) {
- pin.title = newTitle;
- pin.editedTitle = isEdited;
- }
+ const browsers = Services.wm.getEnumerator('navigator:browser');
+
+ // update the label for the same pin across all windows
+ for (const browser of browsers) {
+ const tabs = browser.gBrowser.tabs;
+ // Fix pinned cache for the browser
+ const browserCache = browser.gZenPinnedTabManager?._pinsCache;
+ if (browserCache) {
+ const pin = browserCache.find((pin) => pin.uuid === uuid);
+ if (pin) {
+ pin.title = newTitle;
+ pin.editedTitle = isEdited;
}
- for (let i = 0; i < tabs.length; i++) {
- const tabToEdit = tabs[i];
- if (tabToEdit.getAttribute('zen-pin-id') === uuid && tabToEdit !== tab) {
- tabToEdit.removeAttribute('zen-has-static-label');
- if (isEdited) {
- gBrowser._setTabLabel(tabToEdit, newTitle);
- tabToEdit.setAttribute('zen-has-static-label', 'true');
- } else {
- gBrowser.setTabTitle(tabToEdit);
- }
- break;
+ }
+ for (let i = 0; i < tabs.length; i++) {
+ const tabToEdit = tabs[i];
+ if (tabToEdit.getAttribute('zen-pin-id') === uuid && tabToEdit !== tab) {
+ tabToEdit.removeAttribute('zen-has-static-label');
+ if (isEdited) {
+ gBrowser._setTabLabel(tabToEdit, newTitle);
+ tabToEdit.setAttribute('zen-has-static-label', 'true');
+ } else {
+ gBrowser.setTabTitle(tabToEdit);
}
+ break;
}
}
}
+ }
- canEssentialBeAdded(tab) {
- return (
- !(
- (tab.getAttribute('usercontextid') || 0) !=
- gZenWorkspaces.getActiveWorkspaceFromCache().containerTabId &&
- gZenWorkspaces.containerSpecificEssentials
- ) && gBrowser._numZenEssentials < this.maxEssentialTabs
- );
- }
+ canEssentialBeAdded(tab) {
+ return (
+ !(
+ (tab.getAttribute('usercontextid') || 0) !=
+ gZenWorkspaces.getActiveWorkspaceFromCache().containerTabId &&
+ gZenWorkspaces.containerSpecificEssentials
+ ) && gBrowser._numZenEssentials < this.maxEssentialTabs
+ );
+ }
- applyDragoverClass(event, draggedTab) {
- if (!this.enabled) {
- return;
- }
- let isVertical = this.expandedSidebarMode;
- if (
- gBrowser.isTabGroupLabel(draggedTab) &&
- !draggedTab?.group?.hasAttribute('split-view-group')
- ) {
- // If the target is a tab group label, we don't want to apply the dragover class
- this.removeTabContainersDragoverClass();
- return;
- }
- const pinnedTabsTarget = event.target.closest('.zen-workspace-pinned-tabs-section');
- const essentialTabsTarget = event.target.closest('.zen-essentials-container');
- const tabsTarget = event.target.closest('.zen-workspace-normal-tabs-section');
- const folderTarget = event.target.closest('zen-folder');
- let targetTab = event.target.closest('.tabbrowser-tab');
- targetTab = targetTab?.group || targetTab;
- draggedTab = draggedTab?.group?.hasAttribute('split-view-group')
- ? draggedTab.group
- : draggedTab;
- const isHoveringIndicator = !!event.target.closest('.zen-current-workspace-indicator');
- if (isHoveringIndicator) {
- this.removeTabContainersDragoverClass(false);
- gZenWorkspaces.activeWorkspaceIndicator?.setAttribute('open', true);
- } else {
- gZenWorkspaces.activeWorkspaceIndicator?.removeAttribute('open');
- }
+ applyDragoverClass(event, draggedTab) {
+ if (!this.enabled) {
+ return;
+ }
+ let isVertical = this.expandedSidebarMode;
+ if (
+ gBrowser.isTabGroupLabel(draggedTab) &&
+ !draggedTab?.group?.hasAttribute('split-view-group')
+ ) {
+ // If the target is a tab group label, we don't want to apply the dragover class
+ this.removeTabContainersDragoverClass();
+ return;
+ }
+ const pinnedTabsTarget = event.target.closest('.zen-workspace-pinned-tabs-section');
+ const essentialTabsTarget = event.target.closest('.zen-essentials-container');
+ const tabsTarget = event.target.closest('.zen-workspace-normal-tabs-section');
+ const folderTarget = event.target.closest('zen-folder');
+ let targetTab = event.target.closest('.tabbrowser-tab');
+ targetTab = targetTab?.group || targetTab;
+ draggedTab = draggedTab?.group?.hasAttribute('split-view-group')
+ ? draggedTab.group
+ : draggedTab;
+ const isHoveringIndicator = !!event.target.closest('.zen-current-workspace-indicator');
+ if (isHoveringIndicator) {
+ this.removeTabContainersDragoverClass(false);
+ gZenWorkspaces.activeWorkspaceIndicator?.setAttribute('open', true);
+ } else {
+ gZenWorkspaces.activeWorkspaceIndicator?.removeAttribute('open');
+ }
- if (draggedTab?._dragData?.movingTabs) {
- gZenFolders.ungroupTabsFromActiveGroups(draggedTab._dragData.movingTabs);
- }
+ if (draggedTab?._dragData?.movingTabs) {
+ gZenFolders.ungroupTabsFromActiveGroups(draggedTab._dragData.movingTabs);
+ }
- let shouldAddDragOverElement = false;
+ let shouldAddDragOverElement = false;
- // Decide whether we should show a dragover class for the given target
- if (essentialTabsTarget) {
- if (!draggedTab.hasAttribute('zen-essential') && this.canEssentialBeAdded(draggedTab)) {
- shouldAddDragOverElement = true;
- isVertical = false;
- }
- } else if (pinnedTabsTarget) {
- if (draggedTab.hasAttribute('zen-essential')) {
- shouldAddDragOverElement = true;
- }
- } else if (tabsTarget) {
- if (draggedTab.hasAttribute('zen-essential')) {
- shouldAddDragOverElement = true;
- }
+ // Decide whether we should show a dragover class for the given target
+ if (essentialTabsTarget) {
+ if (!draggedTab.hasAttribute('zen-essential') && this.canEssentialBeAdded(draggedTab)) {
+ shouldAddDragOverElement = true;
+ isVertical = false;
}
-
- if (!shouldAddDragOverElement || (!targetTab && !folderTarget) || !targetTab) {
- this.removeTabContainersDragoverClass(!isHoveringIndicator);
- return;
+ } else if (pinnedTabsTarget) {
+ if (draggedTab.hasAttribute('zen-essential')) {
+ shouldAddDragOverElement = true;
}
+ } else if (tabsTarget) {
+ if (draggedTab.hasAttribute('zen-essential')) {
+ shouldAddDragOverElement = true;
+ }
+ }
- // Calculate middle to decide 'before' or 'after'
- const rect = targetTab.getBoundingClientRect();
- let shouldPlayHapticFeedback = false;
- if (isVertical || !this.expandedSidebarMode) {
- const separation = 8;
- const middleY = targetTab.screenY + rect.height / 2;
- const indicator = this.dragIndicator;
- let top = 0;
- if (event.screenY > middleY) {
- top = Math.round(rect.top + rect.height) + 'px';
- } else {
- top = Math.round(rect.top) + 'px';
- }
- if (indicator.style.top !== top) {
- shouldPlayHapticFeedback = true;
- }
- indicator.setAttribute('orientation', 'horizontal');
- indicator.style.setProperty('--indicator-left', rect.left + separation / 2 + 'px');
- indicator.style.setProperty('--indicator-width', rect.width - separation + 'px');
- indicator.style.top = top;
- indicator.style.removeProperty('left');
+ if (!shouldAddDragOverElement || (!targetTab && !folderTarget) || !targetTab) {
+ this.removeTabContainersDragoverClass(!isHoveringIndicator);
+ return;
+ }
+
+ // Calculate middle to decide 'before' or 'after'
+ const rect = targetTab.getBoundingClientRect();
+ let shouldPlayHapticFeedback = false;
+ if (isVertical || !this.expandedSidebarMode) {
+ const separation = 8;
+ const middleY = targetTab.screenY + rect.height / 2;
+ const indicator = this.dragIndicator;
+ let top = 0;
+ if (event.screenY > middleY) {
+ top = Math.round(rect.top + rect.height) + 'px';
} else {
- const separation = 8;
- const middleX = targetTab.screenX + rect.width / 2;
- const indicator = this.dragIndicator;
- let left = 0;
- if (event.screenX > middleX) {
- left = Math.round(rect.left + rect.width + 1) + 'px';
- } else {
- left = Math.round(rect.left - 2) + 'px';
- }
- if (indicator.style.left !== left) {
- shouldPlayHapticFeedback = true;
- }
- indicator.setAttribute('orientation', 'vertical');
- indicator.style.setProperty('--indicator-top', rect.top + separation / 2 + 'px');
- indicator.style.setProperty('--indicator-height', rect.height - separation + 'px');
- indicator.style.left = left;
- indicator.style.removeProperty('top');
+ top = Math.round(rect.top) + 'px';
+ }
+ if (indicator.style.top !== top) {
+ shouldPlayHapticFeedback = true;
+ }
+ indicator.setAttribute('orientation', 'horizontal');
+ indicator.style.setProperty('--indicator-left', rect.left + separation / 2 + 'px');
+ indicator.style.setProperty('--indicator-width', rect.width - separation + 'px');
+ indicator.style.top = top;
+ indicator.style.removeProperty('left');
+ } else {
+ const separation = 8;
+ const middleX = targetTab.screenX + rect.width / 2;
+ const indicator = this.dragIndicator;
+ let left = 0;
+ if (event.screenX > middleX) {
+ left = Math.round(rect.left + rect.width + 1) + 'px';
+ } else {
+ left = Math.round(rect.left - 2) + 'px';
}
- if (shouldPlayHapticFeedback) {
- Services.zen.playHapticFeedback();
+ if (indicator.style.left !== left) {
+ shouldPlayHapticFeedback = true;
}
+ indicator.setAttribute('orientation', 'vertical');
+ indicator.style.setProperty('--indicator-top', rect.top + separation / 2 + 'px');
+ indicator.style.setProperty('--indicator-height', rect.height - separation + 'px');
+ indicator.style.left = left;
+ indicator.style.removeProperty('top');
+ }
+ if (shouldPlayHapticFeedback) {
+ Services.zen.playHapticFeedback();
}
+ }
- async onTabLabelChanged(tab) {
- if (!this._pinsCache) {
- return;
- }
- // If our current pin in the cache point to about:blank, we need to update the entry
- const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
- if (!pin) {
- return;
- }
+ async onTabLabelChanged(tab) {
+ if (!this._pinsCache) {
+ return;
+ }
+ // If our current pin in the cache point to about:blank, we need to update the entry
+ const pin = this._pinsCache.find((pin) => pin.uuid === tab.getAttribute('zen-pin-id'));
+ if (!pin) {
+ return;
+ }
- if (pin.url === 'about:blank' && tab.linkedBrowser.currentURI.spec !== 'about:blank') {
- await this.replacePinnedUrlWithCurrent(tab);
- }
+ if (pin.url === 'about:blank' && tab.linkedBrowser.currentURI.spec !== 'about:blank') {
+ await this.replacePinnedUrlWithCurrent(tab);
}
}
-
- window.gZenPinnedTabManager = new nsZenPinnedTabManager();
}
+
+window.gZenPinnedTabManager = new nsZenPinnedTabManager();
diff --git a/src/zen/tabs/ZenPinnedTabsStorage.mjs b/src/zen/tabs/ZenPinnedTabsStorage.mjs
index bc11213f77..a407dad353 100644
--- a/src/zen/tabs/ZenPinnedTabsStorage.mjs
+++ b/src/zen/tabs/ZenPinnedTabsStorage.mjs
@@ -1,7 +1,8 @@
// 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/.
-var ZenPinnedTabsStorage = {
+
+window.ZenPinnedTabsStorage = {
_saveCache: [],
async init() {
@@ -78,6 +79,7 @@ var ZenPinnedTabsStorage = {
async savePin(pin, notifyObservers = true) {
// If we find the exact same pin in the cache, skip saving
const existingIndex = this._saveCache.findIndex((cachedPin) => cachedPin.uuid === pin.uuid);
+ const copy = { ...pin };
if (existingIndex !== -1) {
const existingPin = this._saveCache[existingIndex];
const isSame = Object.keys(pin).every((key) => pin[key] === existingPin[key]);
@@ -85,11 +87,11 @@ var ZenPinnedTabsStorage = {
return; // No changes, skip saving
} else {
// Update the cached pin
- this._saveCache[existingIndex] = pin;
+ this._saveCache[existingIndex] = { ...copy };
}
} else {
// Add to cache
- this._saveCache.push(pin);
+ this._saveCache.push(copy);
}
const changedUUIDs = new Set();
diff --git a/src/zen/tabs/jar.inc.mn b/src/zen/tabs/jar.inc.mn
new file mode 100644
index 0000000000..2acab4816b
--- /dev/null
+++ b/src/zen/tabs/jar.inc.mn
@@ -0,0 +1,8 @@
+# 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/.
+
+ content/browser/zen-components/ZenPinnedTabsStorage.mjs (../../zen/tabs/ZenPinnedTabsStorage.mjs)
+ content/browser/zen-components/ZenPinnedTabManager.mjs (../../zen/tabs/ZenPinnedTabManager.mjs)
+* content/browser/zen-styles/zen-tabs.css (../../zen/tabs/zen-tabs.css)
+ content/browser/zen-styles/zen-tabs/vertical-tabs.css (../../zen/tabs/zen-tabs/vertical-tabs.css)
\ No newline at end of file
diff --git a/src/zen/tabs/zen-tabs/vertical-tabs.css b/src/zen/tabs/zen-tabs/vertical-tabs.css
index ce6669ef8a..20bcccefcd 100644
--- a/src/zen/tabs/zen-tabs/vertical-tabs.css
+++ b/src/zen/tabs/zen-tabs/vertical-tabs.css
@@ -313,7 +313,7 @@
var(--zen-tabbox-element-indent-transition);
}
- :root[zen-sidebar-expanded='true'] & {
+ :root[zen-sidebar-expanded='true'] &:not([zen-glance-tab]) {
margin-inline-start: var(--zen-folder-indent) !important;
}
@@ -329,13 +329,6 @@
scale: 0.92;
}
- &:not(:hover) {
- .tab-close-button,
- .tab-reset-button {
- display: none !important;
- }
- }
-
& .tab-icon-image {
&:not([src]),
&:-moz-broken {
@@ -656,7 +649,14 @@
}
}
- &:is(:hover, [visuallyselected]) .tab-close-button {
+ &:not(:hover) {
+ .tab-close-button,
+ .tab-reset-button {
+ display: none;
+ }
+ }
+
+ &:is(:hover, [multiselected][selected]) .tab-close-button {
display: block;
--tab-inline-padding: 0;
margin-inline-end: 0;
diff --git a/src/zen/tests/tabs/browser_tabs_cycle_by_attribute.js b/src/zen/tests/tabs/browser_tabs_cycle_by_attribute.js
index 3dba19ffa1..f6dce0faf7 100644
--- a/src/zen/tests/tabs/browser_tabs_cycle_by_attribute.js
+++ b/src/zen/tests/tabs/browser_tabs_cycle_by_attribute.js
@@ -1,3 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ https://creativecommons.org/publicdomain/zero/1.0/ */
+
'use strict';
const URL1 = 'data:text/plain,tab1';
diff --git a/src/zen/vendor/jar.inc.mn b/src/zen/vendor/jar.inc.mn
new file mode 100644
index 0000000000..1f1f91fe31
--- /dev/null
+++ b/src/zen/vendor/jar.inc.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+ content/browser/zen-vendor/tsparticles.confetti.bundle.min.js (../../zen/vendor/tsparticles.confetti.bundle.min.js)
+ content/browser/zen-vendor/motion.min.mjs (../../zen/vendor/motion.min.mjs)
diff --git a/src/zen/welcome/jar.inc.mn b/src/zen/welcome/jar.inc.mn
new file mode 100644
index 0000000000..9e7fc185b3
--- /dev/null
+++ b/src/zen/welcome/jar.inc.mn
@@ -0,0 +1,6 @@
+# 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/.
+
+ content/browser/zen-components/ZenWelcome.mjs (../../zen/welcome/ZenWelcome.mjs)
+ content/browser/zen-styles/zen-welcome.css (../../zen/welcome/zen-welcome.css)
\ No newline at end of file
diff --git a/src/zen/workspaces/ZenGradientGenerator.mjs b/src/zen/workspaces/ZenGradientGenerator.mjs
index 28f79ce529..ba3f1edbe2 100644
--- a/src/zen/workspaces/ZenGradientGenerator.mjs
+++ b/src/zen/workspaces/ZenGradientGenerator.mjs
@@ -2,891 +2,996 @@
// 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/.
-{
- function parseSinePath(pathStr) {
- const points = [];
- const commands = pathStr.match(/[MCL]\s*[\d\s.\-,]+/g);
- if (!commands) return points;
-
- commands.forEach((command) => {
- const type = command.charAt(0);
- const coordsStr = command.slice(1).trim();
- const coords = coordsStr.split(/[\s,]+/).map(Number);
-
- switch (type) {
- case 'M':
- points.push({ x: coords[0], y: coords[1], type: 'M' });
- break;
- case 'C':
- if (coords.length >= 6 && coords.length % 6 === 0) {
- for (let i = 0; i < coords.length; i += 6) {
- points.push({
- x1: coords[i],
- y1: coords[i + 1],
- x2: coords[i + 2],
- y2: coords[i + 3],
- x: coords[i + 4],
- y: coords[i + 5],
- type: 'C',
- });
- }
+import { nsZenMultiWindowFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
+
+function parseSinePath(pathStr) {
+ const points = [];
+ const commands = pathStr.match(/[MCL]\s*[\d\s.\-,]+/g);
+ if (!commands) return points;
+
+ commands.forEach((command) => {
+ const type = command.charAt(0);
+ const coordsStr = command.slice(1).trim();
+ const coords = coordsStr.split(/[\s,]+/).map(Number);
+
+ switch (type) {
+ case 'M':
+ points.push({ x: coords[0], y: coords[1], type: 'M' });
+ break;
+ case 'C':
+ if (coords.length >= 6 && coords.length % 6 === 0) {
+ for (let i = 0; i < coords.length; i += 6) {
+ points.push({
+ x1: coords[i],
+ y1: coords[i + 1],
+ x2: coords[i + 2],
+ y2: coords[i + 3],
+ x: coords[i + 4],
+ y: coords[i + 5],
+ type: 'C',
+ });
}
- break;
- case 'L':
- points.push({ x: coords[0], y: coords[1], type: 'L' });
- break;
- }
- });
- return points;
- }
-
- const MAX_OPACITY = 0.9;
- const MIN_OPACITY = AppConstants.platform === 'macosx' ? 0.25 : 0.35;
-
- const EXPLICIT_LIGHTNESS_TYPE = 'explicit-lightness';
-
- class nsZenThemePicker extends nsZenMultiWindowFeature {
- static MAX_DOTS = 3;
-
- currentOpacity = 0.5;
- dots = [];
- useAlgo = '';
- #currentLightness = 50;
-
- #allowTransparencyOnSidebar = Services.prefs.getBoolPref('zen.theme.acrylic-elements', false);
-
- #linePath = `M 51.373 27.395 L 367.037 27.395`;
- #sinePath = `M 51.373 27.395 C 60.14 -8.503 68.906 -8.503 77.671 27.395 C 86.438 63.293 95.205 63.293 103.971 27.395 C 112.738 -8.503 121.504 -8.503 130.271 27.395 C 139.037 63.293 147.803 63.293 156.57 27.395 C 165.335 -8.503 174.101 -8.503 182.868 27.395 C 191.634 63.293 200.4 63.293 209.167 27.395 C 217.933 -8.503 226.7 -8.503 235.467 27.395 C 244.233 63.293 252.999 63.293 261.765 27.395 C 270.531 -8.503 279.297 -8.503 288.064 27.395 C 296.83 63.293 305.596 63.293 314.363 27.395 C 323.13 -8.503 331.896 -8.503 340.662 27.395 M 314.438 27.395 C 323.204 -8.503 331.97 -8.503 340.737 27.395 C 349.503 63.293 358.27 63.293 367.037 27.395`;
-
- #sinePoints = parseSinePath(this.#sinePath);
-
- #colorPage = 0;
- #gradientsCache = new Map();
-
- constructor() {
- super();
- if (
- !Services.prefs.getBoolPref('zen.theme.gradient', true) ||
- !gZenWorkspaces.shouldHaveWorkspaces ||
- gZenWorkspaces.privateWindowOrDisabled
- ) {
- return;
- }
- this.promiseInitialized = new Promise((resolve) => {
- this._resolveInitialized = resolve;
- });
- this.dragStartPosition = null;
-
- this.isLegacyVersion =
- Services.prefs.getIntPref('zen.theme.gradient-legacy-version', 1) === 0;
+ }
+ break;
+ case 'L':
+ points.push({ x: coords[0], y: coords[1], type: 'L' });
+ break;
+ }
+ });
+ return points;
+}
- ChromeUtils.defineLazyGetter(this, 'panel', () =>
- document.getElementById('PanelUI-zen-gradient-generator')
- );
- ChromeUtils.defineLazyGetter(this, 'toolbox', () => document.getElementById('TabsToolbar'));
- ChromeUtils.defineLazyGetter(this, 'customColorInput', () =>
- document.getElementById('PanelUI-zen-gradient-generator-custom-input')
- );
- ChromeUtils.defineLazyGetter(this, 'customColorList', () =>
- document.getElementById('PanelUI-zen-gradient-generator-custom-list')
- );
+const MAX_OPACITY = 0.9;
+const MIN_OPACITY = AppConstants.platform === 'macosx' ? 0.25 : 0.35;
- ChromeUtils.defineLazyGetter(this, 'sliderWavePath', () =>
- document.getElementById('PanelUI-zen-gradient-slider-wave').querySelector('path')
- );
+const EXPLICIT_LIGHTNESS_TYPE = 'explicit-lightness';
- this.panel.addEventListener('popupshowing', this.handlePanelOpen.bind(this));
- this.panel.addEventListener('popuphidden', this.handlePanelClose.bind(this));
- this.panel.addEventListener('command', this.handlePanelCommand.bind(this));
+export class nsZenThemePicker extends nsZenMultiWindowFeature {
+ static MAX_DOTS = 3;
- document
- .getElementById('PanelUI-zen-gradient-generator-opacity')
- .addEventListener('input', this.onOpacityChange.bind(this));
+ currentOpacity = 0.5;
+ dots = [];
+ useAlgo = '';
+ #currentLightness = 50;
- // Call the rest of the initialization
- this.initContextMenu();
- this.initPredefinedColors();
+ #allowTransparencyOnSidebar = Services.prefs.getBoolPref('zen.theme.acrylic-elements', false);
- this._resolveInitialized();
- delete this._resolveInitialized;
+ #linePath = `M 51.373 27.395 L 367.037 27.395`;
+ #sinePath = `M 51.373 27.395 C 60.14 -8.503 68.906 -8.503 77.671 27.395 C 86.438 63.293 95.205 63.293 103.971 27.395 C 112.738 -8.503 121.504 -8.503 130.271 27.395 C 139.037 63.293 147.803 63.293 156.57 27.395 C 165.335 -8.503 174.101 -8.503 182.868 27.395 C 191.634 63.293 200.4 63.293 209.167 27.395 C 217.933 -8.503 226.7 -8.503 235.467 27.395 C 244.233 63.293 252.999 63.293 261.765 27.395 C 270.531 -8.503 279.297 -8.503 288.064 27.395 C 296.83 63.293 305.596 63.293 314.363 27.395 C 323.13 -8.503 331.896 -8.503 340.662 27.395 M 314.438 27.395 C 323.204 -8.503 331.97 -8.503 340.737 27.395 C 349.503 63.293 358.27 63.293 367.037 27.395`;
- this.initCustomColorInput();
- this.initTextureInput();
- this.initSchemeButtons();
- this.initColorPages();
+ #sinePoints = parseSinePath(this.#sinePath);
- const darkModeChange = this.handleDarkModeChange.bind(this);
- window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', darkModeChange);
+ #colorPage = 0;
+ #gradientsCache = new Map();
- XPCOMUtils.defineLazyPreferenceGetter(
- this,
- 'windowSchemeType',
- 'zen.view.window.scheme',
- 2,
- darkModeChange
- );
-
- XPCOMUtils.defineLazyPreferenceGetter(
- this,
- 'darkModeBias',
- 'zen.theme.dark-mode-bias',
- '0.5'
- );
+ constructor() {
+ super();
+ if (!gZenWorkspaces.shouldHaveWorkspaces || gZenWorkspaces.privateWindowOrDisabled) {
+ return;
}
+ this.promiseInitialized = new Promise((resolve) => {
+ this._resolveInitialized = resolve;
+ });
+ this.dragStartPosition = null;
+
+ this.isLegacyVersion = Services.prefs.getIntPref('zen.theme.gradient-legacy-version', 1) === 0;
+
+ ChromeUtils.defineLazyGetter(this, 'panel', () =>
+ document.getElementById('PanelUI-zen-gradient-generator')
+ );
+ ChromeUtils.defineLazyGetter(this, 'toolbox', () => document.getElementById('TabsToolbar'));
+ ChromeUtils.defineLazyGetter(this, 'customColorInput', () =>
+ document.getElementById('PanelUI-zen-gradient-generator-custom-input')
+ );
+ ChromeUtils.defineLazyGetter(this, 'customColorList', () =>
+ document.getElementById('PanelUI-zen-gradient-generator-custom-list')
+ );
+
+ ChromeUtils.defineLazyGetter(this, 'sliderWavePath', () =>
+ document.getElementById('PanelUI-zen-gradient-slider-wave').querySelector('path')
+ );
+
+ this.panel.addEventListener('popupshowing', this.handlePanelOpen.bind(this));
+ this.panel.addEventListener('popuphidden', this.handlePanelClose.bind(this));
+ this.panel.addEventListener('command', this.handlePanelCommand.bind(this));
+
+ document
+ .getElementById('PanelUI-zen-gradient-generator-opacity')
+ .addEventListener('input', this.onOpacityChange.bind(this));
+
+ // Call the rest of the initialization
+ this.initContextMenu();
+ this.initPredefinedColors();
+
+ this._resolveInitialized();
+ delete this._resolveInitialized;
+
+ this.initCustomColorInput();
+ this.initTextureInput();
+ this.initSchemeButtons();
+ this.initColorPages();
+
+ const darkModeChange = this.handleDarkModeChange.bind(this);
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', darkModeChange);
+
+ XPCOMUtils.defineLazyPreferenceGetter(
+ this,
+ 'windowSchemeType',
+ 'zen.view.window.scheme',
+ 2,
+ darkModeChange
+ );
+
+ XPCOMUtils.defineLazyPreferenceGetter(this, 'darkModeBias', 'zen.theme.dark-mode-bias', '0.5');
+ }
- handleDarkModeChange() {
- this.updateCurrentWorkspace();
- }
+ handleDarkModeChange() {
+ this.updateCurrentWorkspace();
+ }
- get isDarkMode() {
- if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ get isDarkMode() {
+ if (PrivateBrowsingUtils.isWindowPrivate(window)) {
+ return true;
+ }
+ switch (this.windowSchemeType) {
+ case 0:
return true;
- }
- switch (this.windowSchemeType) {
- case 0:
- return true;
- case 1:
- return false;
- default:
- }
- return window.matchMedia('(prefers-color-scheme: dark)').matches;
+ case 1:
+ return false;
+ default:
}
+ return window.matchMedia('(prefers-color-scheme: dark)').matches;
+ }
- get colorHarmonies() {
- return [
- { type: 'complementary', angles: [180] },
- { type: 'splitComplementary', angles: [150, 210] },
- { type: 'analogous', angles: [50, 310] },
- { type: 'triadic', angles: [120, 240] },
- { type: 'floating', angles: [] },
- ];
- }
+ get colorHarmonies() {
+ return [
+ { type: 'complementary', angles: [180] },
+ { type: 'splitComplementary', angles: [150, 210] },
+ { type: 'analogous', angles: [50, 310] },
+ { type: 'triadic', angles: [120, 240] },
+ { type: 'floating', angles: [] },
+ ];
+ }
- initContextMenu() {
- const menu = window.MozXULElement.parseXULToFragment(`
+ initContextMenu() {
+ const menu = window.MozXULElement.parseXULToFragment(`
`);
- document.getElementById('toolbar-context-customize').before(menu);
- }
-
- openThemePicker(event) {
- const fromForm = event.explicitOriginalTarget?.classList?.contains(
- 'zen-workspace-creation-edit-theme-button'
- );
- PanelMultiView.openPopup(this.panel, this.toolbox, {
- position: 'topright topleft',
- triggerEvent: event,
- y: fromForm ? -160 : 0,
- });
- }
-
- initCustomColorInput() {
- this.customColorInput.addEventListener('change', (event) => {
- // Prevent the popup from closing when the input is focused
- this.openThemePicker(event);
- });
- }
+ document.getElementById('toolbar-context-customize').before(menu);
+ }
- initPredefinedColors() {
- document
- .getElementById('PanelUI-zen-gradient-generator-color-pages')
- .addEventListener('click', async (event) => {
- const target = event.target;
- const rawPosition = target.getAttribute('data-position');
- if (!rawPosition) {
- return;
- }
- const algo = target.getAttribute('data-algo');
- const lightness = target.getAttribute('data-lightness');
- const numDots = parseInt(target.getAttribute('data-num-dots'));
- if (numDots < this.dots.length) {
- for (let i = numDots; i < this.dots.length; i++) {
- this.dots[i].element.remove();
- }
- this.dots = this.dots.slice(0, numDots);
- }
- // Generate new gradient from the single color given
- const [x, y] = rawPosition.split(',').map((pos) => parseInt(pos));
- let dots = [
- {
- ID: 0,
- position: { x, y },
- isPrimary: true,
- type: EXPLICIT_LIGHTNESS_TYPE,
- },
- ];
- for (let i = 1; i < numDots; i++) {
- dots.push({
- ID: i,
- position: { x: 0, y: 0 },
- type: EXPLICIT_LIGHTNESS_TYPE,
- });
- }
- this.useAlgo = algo;
- this.#currentLightness = lightness;
- dots = this.calculateCompliments(dots, 'update', this.useAlgo);
- this.handleColorPositions(dots, true);
- this.updateCurrentWorkspace();
- });
- }
+ openThemePicker(event) {
+ const fromForm = event.explicitOriginalTarget?.classList?.contains(
+ 'zen-workspace-creation-edit-theme-button'
+ );
+ PanelMultiView.openPopup(this.panel, this.toolbox, {
+ position: 'topright topleft',
+ triggerEvent: event,
+ y: fromForm ? -160 : 0,
+ });
+ }
- initColorPages() {
- const leftButton = document.getElementById('PanelUI-zen-gradient-generator-color-page-left');
- const rightButton = document.getElementById(
- 'PanelUI-zen-gradient-generator-color-page-right'
- );
- const pagesWrapper = document.getElementById('PanelUI-zen-gradient-generator-color-pages');
- const pages = pagesWrapper.children;
- pagesWrapper.addEventListener('wheel', (event) => {
- event.preventDefault();
- event.stopPropagation();
- });
- leftButton.addEventListener('command', () => {
- this.#colorPage = (this.#colorPage - 1 + pages.length) % pages.length;
- // Scroll to the next page, by using scrollLeft
- pagesWrapper.scrollLeft = (this.#colorPage * pagesWrapper.scrollWidth) / pages.length;
- rightButton.disabled = false;
- leftButton.disabled = this.#colorPage === 0;
- });
- rightButton.addEventListener('command', () => {
- this.#colorPage = (this.#colorPage + 1) % pages.length;
- // Scroll to the next page, by using scrollLeft
- pagesWrapper.scrollLeft = (this.#colorPage * pagesWrapper.scrollWidth) / pages.length;
- leftButton.disabled = false;
- rightButton.disabled = this.#colorPage === pages.length - 1;
- });
- }
+ initCustomColorInput() {
+ this.customColorInput.addEventListener('change', (event) => {
+ // Prevent the popup from closing when the input is focused
+ this.openThemePicker(event);
+ });
+ }
- initSchemeButtons() {
- const buttons = document.getElementById('PanelUI-zen-gradient-generator-scheme');
- buttons.addEventListener('click', (event) => {
- const target = event.target.closest('.subviewbutton');
- if (!target) {
+ initPredefinedColors() {
+ document
+ .getElementById('PanelUI-zen-gradient-generator-color-pages')
+ .addEventListener('click', async (event) => {
+ const target = event.target;
+ const rawPosition = target.getAttribute('data-position');
+ if (!rawPosition) {
return;
}
- event.preventDefault();
- event.stopPropagation();
- const scheme = target.id.replace('PanelUI-zen-gradient-generator-scheme-', '');
- if (!scheme) {
- return;
+ const algo = target.getAttribute('data-algo');
+ const lightness = target.getAttribute('data-lightness');
+ const numDots = parseInt(target.getAttribute('data-num-dots'));
+ if (numDots < this.dots.length) {
+ for (let i = numDots; i < this.dots.length; i++) {
+ this.dots[i].element.remove();
+ }
+ this.dots = this.dots.slice(0, numDots);
}
- const themeInt = {
- auto: 2,
- light: 1,
- dark: 0,
- }[scheme];
- if (themeInt === undefined) {
- return;
+ // Generate new gradient from the single color given
+ const [x, y] = rawPosition.split(',').map((pos) => parseInt(pos));
+ let dots = [
+ {
+ ID: 0,
+ position: { x, y },
+ isPrimary: true,
+ type: EXPLICIT_LIGHTNESS_TYPE,
+ },
+ ];
+ for (let i = 1; i < numDots; i++) {
+ dots.push({
+ ID: i,
+ position: { x: 0, y: 0 },
+ type: EXPLICIT_LIGHTNESS_TYPE,
+ });
}
- Services.prefs.setIntPref('zen.view.window.scheme', themeInt);
+ this.useAlgo = algo;
+ this.#currentLightness = lightness;
+ dots = this.calculateCompliments(dots, 'update', this.useAlgo);
+ this.handleColorPositions(dots, true);
+ this.updateCurrentWorkspace();
});
- }
-
- initTextureInput() {
- const wrapper = document.getElementById('PanelUI-zen-gradient-generator-texture-wrapper');
- const wrapperWidth = wrapper.getBoundingClientRect().width;
- // Add elements in a circular pattern, where the center is the center of the wrapper
- for (let i = 0; i < 16; i++) {
- const dot = document.createElement('div');
- dot.classList.add('zen-theme-picker-texture-dot');
- const position = (i / 16) * Math.PI * 2 + wrapperWidth;
- dot.style.left = `${Math.cos(position) * 50 + 50}%`;
- dot.style.top = `${Math.sin(position) * 50 + 50}%`;
- wrapper.appendChild(dot);
- }
- this._textureHandler = document.createElement('div');
- this._textureHandler.id = 'PanelUI-zen-gradient-generator-texture-handler';
- this._textureHandler.addEventListener('mousedown', this.onTextureHandlerMouseDown.bind(this));
- wrapper.appendChild(this._textureHandler);
- }
+ }
- onTextureHandlerMouseDown(event) {
+ initColorPages() {
+ const leftButton = document.getElementById('PanelUI-zen-gradient-generator-color-page-left');
+ const rightButton = document.getElementById('PanelUI-zen-gradient-generator-color-page-right');
+ const pagesWrapper = document.getElementById('PanelUI-zen-gradient-generator-color-pages');
+ const pages = pagesWrapper.children;
+ pagesWrapper.addEventListener('wheel', (event) => {
event.preventDefault();
- this._onTextureMouseMove = this.onTextureMouseMove.bind(this);
- this._onTextureMouseUp = this.onTextureMouseUp.bind(this);
- document.addEventListener('mousemove', this._onTextureMouseMove);
- document.addEventListener('mouseup', this._onTextureMouseUp);
- }
+ event.stopPropagation();
+ });
+ leftButton.addEventListener('command', () => {
+ this.#colorPage = (this.#colorPage - 1 + pages.length) % pages.length;
+ // Scroll to the next page, by using scrollLeft
+ pagesWrapper.scrollLeft = (this.#colorPage * pagesWrapper.scrollWidth) / pages.length;
+ rightButton.disabled = false;
+ leftButton.disabled = this.#colorPage === 0;
+ });
+ rightButton.addEventListener('command', () => {
+ this.#colorPage = (this.#colorPage + 1) % pages.length;
+ // Scroll to the next page, by using scrollLeft
+ pagesWrapper.scrollLeft = (this.#colorPage * pagesWrapper.scrollWidth) / pages.length;
+ leftButton.disabled = false;
+ rightButton.disabled = this.#colorPage === pages.length - 1;
+ });
+ }
- onTextureMouseMove(event) {
- event.preventDefault();
- const wrapper = document.getElementById('PanelUI-zen-gradient-generator-texture-wrapper');
- const wrapperRect = wrapper.getBoundingClientRect();
- // Determine how much rotation there is based on the mouse position and the center of the wrapper
- const rotation = Math.atan2(
- event.clientY - wrapperRect.top - wrapperRect.height / 2,
- event.clientX - wrapperRect.left - wrapperRect.width / 2
- );
- const previousTexture = this.currentTexture;
- this.currentTexture = (rotation * 180) / Math.PI + 90;
- // if it's negative, add 360 to make it positive
- if (this.currentTexture < 0) {
- this.currentTexture += 360;
+ initSchemeButtons() {
+ const buttons = document.getElementById('PanelUI-zen-gradient-generator-scheme');
+ buttons.addEventListener('click', (event) => {
+ const target = event.target.closest('.subviewbutton');
+ if (!target) {
+ return;
}
- // make it go from 1 to 0 instead of being in degrees
- this.currentTexture /= 360;
- // We clip it to the closest button out of 16 possible buttons
- this.currentTexture = Math.round(this.currentTexture * 16) / 16;
- if (this.currentTexture === 1) {
- this.currentTexture = 0;
+ event.preventDefault();
+ event.stopPropagation();
+ const scheme = target.id.replace('PanelUI-zen-gradient-generator-scheme-', '');
+ if (!scheme) {
+ return;
}
- if (previousTexture !== this.currentTexture) {
- this.updateCurrentWorkspace();
- Services.zen.playHapticFeedback();
+ const themeInt = {
+ auto: 2,
+ light: 1,
+ dark: 0,
+ }[scheme];
+ if (themeInt === undefined) {
+ return;
}
- }
+ Services.prefs.setIntPref('zen.view.window.scheme', themeInt);
+ });
+ }
- onTextureMouseUp(event) {
- event.preventDefault();
- document.removeEventListener('mousemove', this._onTextureMouseMove);
- document.removeEventListener('mouseup', this._onTextureMouseUp);
- this._onTextureMouseMove = null;
- this._onTextureMouseUp = null;
- }
-
- initThemePicker() {
- const themePicker = this.panel.querySelector('.zen-theme-picker-gradient');
- this._onDotMouseMove = this.onDotMouseMove.bind(this);
- this._onDotMouseUp = this.onDotMouseUp.bind(this);
- this._onDotMouseDown = this.onDotMouseDown.bind(this);
- this._onThemePickerClick = this.onThemePickerClick.bind(this);
- document.addEventListener('mousemove', this._onDotMouseMove);
- document.addEventListener('mouseup', this._onDotMouseUp);
- themePicker.addEventListener('mousedown', this._onDotMouseDown);
- themePicker.addEventListener('click', this._onThemePickerClick);
- }
-
- uninitThemePicker() {
- const themePicker = this.panel.querySelector('.zen-theme-picker-gradient');
- document.removeEventListener('mousemove', this._onDotMouseMove);
- document.removeEventListener('mouseup', this._onDotMouseUp);
- themePicker.removeEventListener('mousedown', this._onDotMouseDown);
- themePicker.removeEventListener('click', this._onThemePickerClick);
- this._onDotMouseMove = null;
- this._onDotMouseUp = null;
- this._onDotMouseDown = null;
- this._onThemePickerClick = null;
- }
-
- /**
- * Converts an HSL color value to RGB. Conversion formula
- * adapted from https://en.wikipedia.org/wiki/HSL_color_space.
- * Assumes h, s, and l are contained in the set [0, 1] and
- * returns r, g, and b in the set [0, 255].
- *
- * @param {number} h The hue
- * @param {number} s The saturation
- * @param {number} l The lightness
- * @return {Array} The RGB representation
- */
- hslToRgb(h, s, l) {
- const { round } = Math;
- let r, g, b;
-
- if (s === 0) {
- r = g = b = l; // achromatic
- } else {
- const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
- const p = 2 * l - q;
- r = this.hueToRgb(p, q, h + 1 / 3);
- g = this.hueToRgb(p, q, h);
- b = this.hueToRgb(p, q, h - 1 / 3);
- }
+ initTextureInput() {
+ const wrapper = document.getElementById('PanelUI-zen-gradient-generator-texture-wrapper');
+ const wrapperWidth = wrapper.getBoundingClientRect().width;
+ // Add elements in a circular pattern, where the center is the center of the wrapper
+ for (let i = 0; i < 16; i++) {
+ const dot = document.createElement('div');
+ dot.classList.add('zen-theme-picker-texture-dot');
+ const position = (i / 16) * Math.PI * 2 + wrapperWidth;
+ dot.style.left = `${Math.cos(position) * 50 + 50}%`;
+ dot.style.top = `${Math.sin(position) * 50 + 50}%`;
+ wrapper.appendChild(dot);
+ }
+ this._textureHandler = document.createElement('div');
+ this._textureHandler.id = 'PanelUI-zen-gradient-generator-texture-handler';
+ this._textureHandler.addEventListener('mousedown', this.onTextureHandlerMouseDown.bind(this));
+ wrapper.appendChild(this._textureHandler);
+ }
- return [round(r * 255), round(g * 255), round(b * 255)];
- }
-
- rgbToHsl(r, g, b) {
- r /= 255;
- g /= 255;
- b /= 255;
- let max = Math.max(r, g, b);
- let min = Math.min(r, g, b);
- let d = max - min;
- let h;
- if (d === 0) h = 0;
- else if (max === r) h = ((g - b) / d) % 6;
- else if (max === g) h = (b - r) / d + 2;
- else if (max === b) h = (r - g) / d + 4;
- let l = (min + max) / 2;
- let s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
- return [h * 60, s, l];
- }
-
- hueToRgb(p, q, t) {
- if (t < 0) t += 1;
- if (t > 1) t -= 1;
- if (t < 1 / 6) return p + (q - p) * 6 * t;
- if (t < 1 / 2) return q;
- if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
- return p;
- }
-
- calculateInitialPosition([r, g, b]) {
- // This function is called before the picker is even rendered, so we hard code the dimensions
- // important: If any sort of sizing is changed, make sure changes are reflected here
- const padding = 30;
- const rect = {
- width: 338,
- height: 338,
- };
- const centerX = rect.width / 2;
- const centerY = rect.height / 2;
- const radius = (rect.width - padding) / 2;
- const [hue, saturation] = this.rgbToHsl(r, g, b);
- const angle = (hue / 360) * 2 * Math.PI; // Convert to radians
- const normalizedSaturation = saturation / 100; // Convert to [0, 1]
- const x = centerX + radius * normalizedSaturation * Math.cos(angle) - padding;
- const y = centerY + radius * normalizedSaturation * Math.sin(angle) - padding;
- return { x, y };
- }
-
- getColorFromPosition(x, y, type = undefined) {
- // Return a color as hsl based on the position in the gradient
- const gradient = this.panel.querySelector('.zen-theme-picker-gradient');
- const rect = gradient.getBoundingClientRect();
- const padding = 30; // each side
- const dotHalfSize = 36 / 2; // half the size of the dot
- x += dotHalfSize;
- y += dotHalfSize;
- rect.width += padding * 2; // Adjust width and height for padding
- rect.height += padding * 2; // Adjust width and height for padding
- const centerX = rect.width / 2;
- const centerY = rect.height / 2;
- const radius = (rect.width - padding) / 2;
- const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
- let angle = Math.atan2(y - centerY, x - centerX);
- angle = (angle * 180) / Math.PI; // Convert to degrees
- if (angle < 0) {
- angle += 360; // Normalize to [0, 360)
- }
- const normalizedDistance = 1 - Math.min(distance / radius, 1); // Normalize distance to [0, 1]
- const hue = (angle / 360) * 360; // Normalize angle to [0, 360)
- let saturation = normalizedDistance * 100; // stays high even in center
- if (type !== EXPLICIT_LIGHTNESS_TYPE) {
- saturation = 80 + (1 - normalizedDistance) * 20;
- // Set the current lightness to how far we are from the center of the circle
- // For example, moving the dot outside will have higher lightness, while moving it inside will have lower lightness
- this.#currentLightness = Math.round((1 - normalizedDistance) * 100);
- }
- const lightness = this.#currentLightness; // Fixed lightness for simplicity
- const [r, g, b] = this.hslToRgb(hue / 360, saturation / 100, lightness / 100);
- return [
- Math.min(255, Math.max(0, r)),
- Math.min(255, Math.max(0, g)),
- Math.min(255, Math.max(0, b)),
- ];
- }
+ onTextureHandlerMouseDown(event) {
+ event.preventDefault();
+ this._onTextureMouseMove = this.onTextureMouseMove.bind(this);
+ this._onTextureMouseUp = this.onTextureMouseUp.bind(this);
+ document.addEventListener('mousemove', this._onTextureMouseMove);
+ document.addEventListener('mouseup', this._onTextureMouseUp);
+ }
- getJSONPos(x, y) {
- // Return a JSON string with the position
- return JSON.stringify({ x: Math.round(x), y: Math.round(y) });
+ onTextureMouseMove(event) {
+ event.preventDefault();
+ const wrapper = document.getElementById('PanelUI-zen-gradient-generator-texture-wrapper');
+ const wrapperRect = wrapper.getBoundingClientRect();
+ // Determine how much rotation there is based on the mouse position and the center of the wrapper
+ const rotation = Math.atan2(
+ event.clientY - wrapperRect.top - wrapperRect.height / 2,
+ event.clientX - wrapperRect.left - wrapperRect.width / 2
+ );
+ const previousTexture = this.currentTexture;
+ this.currentTexture = (rotation * 180) / Math.PI + 90;
+ // if it's negative, add 360 to make it positive
+ if (this.currentTexture < 0) {
+ this.currentTexture += 360;
+ }
+ // make it go from 1 to 0 instead of being in degrees
+ this.currentTexture /= 360;
+ // We clip it to the closest button out of 16 possible buttons
+ this.currentTexture = Math.round(this.currentTexture * 16) / 16;
+ if (this.currentTexture === 1) {
+ this.currentTexture = 0;
+ }
+ if (previousTexture !== this.currentTexture) {
+ this.updateCurrentWorkspace();
+ Services.zen.playHapticFeedback();
}
+ }
- createDot(color, fromWorkspace = false) {
- const [r, g, b] = color.c;
- const dot = document.createElement('div');
- if (color.isPrimary) {
- dot.classList.add('primary');
- }
- if (color.isCustom) {
- if (!color.c) {
- return;
- }
- dot.classList.add('custom');
- dot.style.opacity = 0;
- dot.style.setProperty('--zen-theme-picker-dot-color', color.c);
- } else {
- const { x, y } = color.position || this.calculateInitialPosition([r, g, b]);
- const dotPad = this.panel.querySelector('.zen-theme-picker-gradient');
+ onTextureMouseUp(event) {
+ event.preventDefault();
+ document.removeEventListener('mousemove', this._onTextureMouseMove);
+ document.removeEventListener('mouseup', this._onTextureMouseUp);
+ this._onTextureMouseMove = null;
+ this._onTextureMouseUp = null;
+ }
- dot.classList.add('zen-theme-picker-dot');
+ initThemePicker() {
+ const themePicker = this.panel.querySelector('.zen-theme-picker-gradient');
+ this._onDotMouseMove = this.onDotMouseMove.bind(this);
+ this._onDotMouseUp = this.onDotMouseUp.bind(this);
+ this._onDotMouseDown = this.onDotMouseDown.bind(this);
+ this._onThemePickerClick = this.onThemePickerClick.bind(this);
+ document.addEventListener('mousemove', this._onDotMouseMove);
+ document.addEventListener('mouseup', this._onDotMouseUp);
+ themePicker.addEventListener('mousedown', this._onDotMouseDown);
+ themePicker.addEventListener('click', this._onThemePickerClick);
+ }
- dot.style.left = `${x}px`;
- dot.style.top = `${y}px`;
+ uninitThemePicker() {
+ const themePicker = this.panel.querySelector('.zen-theme-picker-gradient');
+ document.removeEventListener('mousemove', this._onDotMouseMove);
+ document.removeEventListener('mouseup', this._onDotMouseUp);
+ themePicker.removeEventListener('mousedown', this._onDotMouseDown);
+ themePicker.removeEventListener('click', this._onThemePickerClick);
+ this._onDotMouseMove = null;
+ this._onDotMouseUp = null;
+ this._onDotMouseDown = null;
+ this._onThemePickerClick = null;
+ }
- if (this.dots.length < 1) {
- dot.classList.add('primary');
- }
+ /**
+ * Converts an HSL color value to RGB. Conversion formula
+ * adapted from https://en.wikipedia.org/wiki/HSL_color_space.
+ * Assumes h, s, and l are contained in the set [0, 1] and
+ * returns r, g, and b in the set [0, 255].
+ *
+ * @param {number} h The hue
+ * @param {number} s The saturation
+ * @param {number} l The lightness
+ * @return {Array} The RGB representation
+ */
+ hslToRgb(h, s, l) {
+ const { round } = Math;
+ let r, g, b;
+
+ if (s === 0) {
+ r = g = b = l; // achromatic
+ } else {
+ const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
+ const p = 2 * l - q;
+ r = this.hueToRgb(p, q, h + 1 / 3);
+ g = this.hueToRgb(p, q, h);
+ b = this.hueToRgb(p, q, h - 1 / 3);
+ }
+
+ return [round(r * 255), round(g * 255), round(b * 255)];
+ }
- dotPad.appendChild(dot);
- let id = this.dots.length;
+ rgbToHsl(r, g, b) {
+ r /= 255;
+ g /= 255;
+ b /= 255;
+ let max = Math.max(r, g, b);
+ let min = Math.min(r, g, b);
+ let d = max - min;
+ let h;
+ if (d === 0) h = 0;
+ else if (max === r) h = ((g - b) / d) % 6;
+ else if (max === g) h = (b - r) / d + 2;
+ else if (max === b) h = (r - g) / d + 4;
+ let l = (min + max) / 2;
+ let s = d === 0 ? 0 : d / (1 - Math.abs(2 * l - 1));
+ return [h * 60, s, l];
+ }
- dot.style.setProperty('--zen-theme-picker-dot-color', `rgb(${r}, ${g}, ${b})`);
- dot.setAttribute('data-position', this.getJSONPos(x, y));
- dot.setAttribute('data-type', color.type);
+ hueToRgb(p, q, t) {
+ if (t < 0) t += 1;
+ if (t > 1) t -= 1;
+ if (t < 1 / 6) return p + (q - p) * 6 * t;
+ if (t < 1 / 2) return q;
+ if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
+ return p;
+ }
- this.dots.push({
- ID: id,
- element: dot,
- position: { x, y },
- type: color.type,
- lightness: color.lightness,
- });
+ calculateInitialPosition([r, g, b]) {
+ // This function is called before the picker is even rendered, so we hard code the dimensions
+ // important: If any sort of sizing is changed, make sure changes are reflected here
+ const padding = 30;
+ const rect = {
+ width: 338,
+ height: 338,
+ };
+ const centerX = rect.width / 2;
+ const centerY = rect.height / 2;
+ const radius = (rect.width - padding) / 2;
+ const [hue, saturation] = this.rgbToHsl(r, g, b);
+ const angle = (hue / 360) * 2 * Math.PI; // Convert to radians
+ const normalizedSaturation = saturation / 100; // Convert to [0, 1]
+ const x = centerX + radius * normalizedSaturation * Math.cos(angle) - padding;
+ const y = centerY + radius * normalizedSaturation * Math.sin(angle) - padding;
+ return { x, y };
+ }
+
+ getColorFromPosition(x, y, type = undefined) {
+ // Return a color as hsl based on the position in the gradient
+ const gradient = this.panel.querySelector('.zen-theme-picker-gradient');
+ const rect = gradient.getBoundingClientRect();
+ const padding = 30; // each side
+ const dotHalfSize = 36 / 2; // half the size of the dot
+ x += dotHalfSize;
+ y += dotHalfSize;
+ rect.width += padding * 2; // Adjust width and height for padding
+ rect.height += padding * 2; // Adjust width and height for padding
+ const centerX = rect.width / 2;
+ const centerY = rect.height / 2;
+ const radius = (rect.width - padding) / 2;
+ const distance = Math.sqrt((x - centerX) ** 2 + (y - centerY) ** 2);
+ let angle = Math.atan2(y - centerY, x - centerX);
+ angle = (angle * 180) / Math.PI; // Convert to degrees
+ if (angle < 0) {
+ angle += 360; // Normalize to [0, 360)
+ }
+ const normalizedDistance = 1 - Math.min(distance / radius, 1); // Normalize distance to [0, 1]
+ const hue = (angle / 360) * 360; // Normalize angle to [0, 360)
+ let saturation = normalizedDistance * 100; // stays high even in center
+ if (type !== EXPLICIT_LIGHTNESS_TYPE) {
+ saturation = 80 + (1 - normalizedDistance) * 20;
+ // Set the current lightness to how far we are from the center of the circle
+ // For example, moving the dot outside will have higher lightness, while moving it inside will have lower lightness
+ this.#currentLightness = Math.round((1 - normalizedDistance) * 100);
+ }
+ const lightness = this.#currentLightness; // Fixed lightness for simplicity
+ const [r, g, b] = this.hslToRgb(hue / 360, saturation / 100, lightness / 100);
+ return [
+ Math.min(255, Math.max(0, r)),
+ Math.min(255, Math.max(0, g)),
+ Math.min(255, Math.max(0, b)),
+ ];
+ }
+
+ getJSONPos(x, y) {
+ // Return a JSON string with the position
+ return JSON.stringify({ x: Math.round(x), y: Math.round(y) });
+ }
+
+ createDot(color, fromWorkspace = false) {
+ const [r, g, b] = color.c;
+ const dot = document.createElement('div');
+ if (color.isPrimary) {
+ dot.classList.add('primary');
+ }
+ if (color.isCustom) {
+ if (!color.c) {
+ return;
}
- if (!fromWorkspace) {
- this.updateCurrentWorkspace(true);
+ dot.classList.add('custom');
+ dot.style.opacity = 0;
+ dot.style.setProperty('--zen-theme-picker-dot-color', color.c);
+ } else {
+ const { x, y } = color.position || this.calculateInitialPosition([r, g, b]);
+ const dotPad = this.panel.querySelector('.zen-theme-picker-gradient');
+
+ dot.classList.add('zen-theme-picker-dot');
+
+ dot.style.left = `${x}px`;
+ dot.style.top = `${y}px`;
+
+ if (this.dots.length < 1) {
+ dot.classList.add('primary');
}
+
+ dotPad.appendChild(dot);
+ let id = this.dots.length;
+
+ dot.style.setProperty('--zen-theme-picker-dot-color', `rgb(${r}, ${g}, ${b})`);
+ dot.setAttribute('data-position', this.getJSONPos(x, y));
+ dot.setAttribute('data-type', color.type);
+
+ this.dots.push({
+ ID: id,
+ element: dot,
+ position: { x, y },
+ type: color.type,
+ lightness: color.lightness,
+ });
+ }
+ if (!fromWorkspace) {
+ this.updateCurrentWorkspace(true);
}
+ }
- addColorToCustomList(color) {
- const listItems = window.MozXULElement.parseXULToFragment(`
+ addColorToCustomList(color) {
+ const listItems = window.MozXULElement.parseXULToFragment(`
`);
- listItems
- .querySelector('.zen-theme-picker-custom-list-item')
- .setAttribute('data-color', color);
- listItems
- .querySelector('.zen-theme-picker-dot')
- .style.setProperty('--zen-theme-picker-dot-color', color);
- listItems.querySelector('.zen-theme-picker-custom-list-item-label').textContent = color;
- listItems
- .querySelector('.zen-theme-picker-custom-list-item-remove')
- .addEventListener('command', this.removeCustomColor.bind(this));
-
- this.customColorList.appendChild(listItems);
- }
-
- async addCustomColor() {
- let color = this.customColorInput.value;
-
- if (!color) {
- return;
- }
+ listItems.querySelector('.zen-theme-picker-custom-list-item').setAttribute('data-color', color);
+ listItems
+ .querySelector('.zen-theme-picker-dot')
+ .style.setProperty('--zen-theme-picker-dot-color', color);
+ listItems.querySelector('.zen-theme-picker-custom-list-item-label').textContent = color;
+ listItems
+ .querySelector('.zen-theme-picker-custom-list-item-remove')
+ .addEventListener('command', this.removeCustomColor.bind(this));
+
+ this.customColorList.appendChild(listItems);
+ }
- let colorOpacity =
- document.getElementById('PanelUI-zen-gradient-generator-custom-opacity')?.value ?? 1;
- // Convert the opacity into a hex value if it's not already
- if (colorOpacity < 1) {
- // e.g. if opacity is 1, we add to the color FF, if it's 0.5 we add 80, etc.
- const hexOpacity = Math.round(colorOpacity * 255)
- .toString(16)
- .padStart(2, '0')
- .toUpperCase();
- // If the color is in hex format
- if (color.startsWith('#')) {
- // If the color is already in hex format, we just append the opacity
- if (color.length === 7) {
- color += hexOpacity;
- }
+ async addCustomColor() {
+ let color = this.customColorInput.value;
+
+ if (!color) {
+ return;
+ }
+
+ let colorOpacity =
+ document.getElementById('PanelUI-zen-gradient-generator-custom-opacity')?.value ?? 1;
+ // Convert the opacity into a hex value if it's not already
+ if (colorOpacity < 1) {
+ // e.g. if opacity is 1, we add to the color FF, if it's 0.5 we add 80, etc.
+ const hexOpacity = Math.round(colorOpacity * 255)
+ .toString(16)
+ .padStart(2, '0')
+ .toUpperCase();
+ // If the color is in hex format
+ if (color.startsWith('#')) {
+ // If the color is already in hex format, we just append the opacity
+ if (color.length === 7) {
+ color += hexOpacity;
}
}
+ }
- // Add '#' prefix if it's missing and the input appears to be a hex color
- if (!color.startsWith('#') && /^[0-9A-Fa-f]{3,6}$/.test(color)) {
- color = '#' + color;
- }
-
- // can be any color format, we just add it to the list as a dot, but hidden
- const dot = document.createElement('div');
- dot.classList.add('zen-theme-picker-dot', 'hidden', 'custom');
- dot.style.opacity = 0;
- dot.style.setProperty('--zen-theme-picker-dot-color', color);
- this.panel.querySelector('#PanelUI-zen-gradient-generator-custom-list').prepend(dot);
- this.customColorInput.value = '';
- document.getElementById('PanelUI-zen-gradient-generator-custom-opacity').value = 1;
- await this.updateCurrentWorkspace();
+ // Add '#' prefix if it's missing and the input appears to be a hex color
+ if (!color.startsWith('#') && /^[0-9A-Fa-f]{3,6}$/.test(color)) {
+ color = '#' + color;
}
- handlePanelCommand(event) {
- const target = event.target.closest('toolbarbutton');
- if (!target) {
- return;
- }
- switch (target.id) {
- case 'PanelUI-zen-gradient-generator-color-custom-add':
- this.addCustomColor();
- break;
- }
+ // can be any color format, we just add it to the list as a dot, but hidden
+ const dot = document.createElement('div');
+ dot.classList.add('zen-theme-picker-dot', 'hidden', 'custom');
+ dot.style.opacity = 0;
+ dot.style.setProperty('--zen-theme-picker-dot-color', color);
+ this.panel.querySelector('#PanelUI-zen-gradient-generator-custom-list').prepend(dot);
+ this.customColorInput.value = '';
+ document.getElementById('PanelUI-zen-gradient-generator-custom-opacity').value = 1;
+ await this.updateCurrentWorkspace();
+ }
+
+ handlePanelCommand(event) {
+ const target = event.target.closest('toolbarbutton');
+ if (!target) {
+ return;
}
+ switch (target.id) {
+ case 'PanelUI-zen-gradient-generator-color-custom-add':
+ this.addCustomColor();
+ break;
+ }
+ }
- spawnDot(dotData, primary = false) {
- const dotPad = this.panel.querySelector('.zen-theme-picker-gradient');
- const relativePosition = {
- x: dotData.x,
- y: dotData.y,
- };
+ spawnDot(dotData, primary = false) {
+ const dotPad = this.panel.querySelector('.zen-theme-picker-gradient');
+ const relativePosition = {
+ x: dotData.x,
+ y: dotData.y,
+ };
- const dot = document.createElement('div');
- dot.classList.add('zen-theme-picker-dot');
+ const dot = document.createElement('div');
+ dot.classList.add('zen-theme-picker-dot');
- dot.style.left = `${dotData.x}px`;
- dot.style.top = `${dotData.y}px`;
+ dot.style.left = `${dotData.x}px`;
+ dot.style.top = `${dotData.y}px`;
- dotPad.appendChild(dot);
+ dotPad.appendChild(dot);
- let id = this.dots.length;
+ let id = this.dots.length;
- if (primary) {
- id = 0;
- dot.classList.add('primary');
+ if (primary) {
+ id = 0;
+ dot.classList.add('primary');
- const existingPrimaryDot = this.dots.find((d) => d.ID === 0);
- if (existingPrimaryDot) {
- existingPrimaryDot.ID = this.dots.length;
- existingPrimaryDot.element.classList.remove('primary');
- }
- }
+ const existingPrimaryDot = this.dots.find((d) => d.ID === 0);
+ if (existingPrimaryDot) {
+ existingPrimaryDot.ID = this.dots.length;
+ existingPrimaryDot.element.classList.remove('primary');
+ }
+ }
+
+ const colorFromPos = this.getColorFromPosition(
+ relativePosition.x,
+ relativePosition.y,
+ dotData.type
+ );
+ dot.style.setProperty(
+ '--zen-theme-picker-dot-color',
+ `rgb(${colorFromPos[0]}, ${colorFromPos[1]}, ${colorFromPos[2]})`
+ );
+ dot.setAttribute('data-position', this.getJSONPos(relativePosition.x, relativePosition.y));
+ dot.setAttribute('data-type', dotData.type);
+
+ this.dots.push({
+ ID: id,
+ element: dot,
+ position: { x: relativePosition.x, y: relativePosition.y },
+ lightness: this.#currentLightness,
+ type: dotData.type,
+ });
+ }
- const colorFromPos = this.getColorFromPosition(
- relativePosition.x,
- relativePosition.y,
- dotData.type
- );
- dot.style.setProperty(
- '--zen-theme-picker-dot-color',
- `rgb(${colorFromPos[0]}, ${colorFromPos[1]}, ${colorFromPos[2]})`
- );
- dot.setAttribute('data-position', this.getJSONPos(relativePosition.x, relativePosition.y));
- dot.setAttribute('data-type', dotData.type);
+ calculateCompliments(dots, action = 'update', useHarmony = '') {
+ const colorHarmonies = this.colorHarmonies;
- this.dots.push({
- ID: id,
- element: dot,
- position: { x: relativePosition.x, y: relativePosition.y },
- lightness: this.#currentLightness,
- type: dotData.type,
- });
+ if (dots.length === 0) {
+ return [];
}
- calculateCompliments(dots, action = 'update', useHarmony = '') {
- const colorHarmonies = this.colorHarmonies;
-
- if (dots.length === 0) {
- return [];
- }
+ function getColorHarmonyType(numDots, dots) {
+ if (useHarmony !== '') {
+ const selectedHarmony = colorHarmonies.find((harmony) => harmony.type === useHarmony);
- function getColorHarmonyType(numDots, dots) {
- if (useHarmony !== '') {
- const selectedHarmony = colorHarmonies.find((harmony) => harmony.type === useHarmony);
-
- if (selectedHarmony) {
- if (action === 'remove') {
- if (dots.length !== 0) {
- return colorHarmonies.find(
- (harmony) => harmony.angles.length === selectedHarmony.angles.length - 1
- );
- } else {
- return { type: 'floating', angles: [] };
- }
- }
- if (action === 'add') {
+ if (selectedHarmony) {
+ if (action === 'remove') {
+ if (dots.length !== 0) {
return colorHarmonies.find(
- (harmony) => harmony.angles.length === selectedHarmony.angles.length + 1
+ (harmony) => harmony.angles.length === selectedHarmony.angles.length - 1
);
- }
- if (action === 'update') {
- return selectedHarmony;
+ } else {
+ return { type: 'floating', angles: [] };
}
}
- }
-
- if (action === 'remove') {
- return colorHarmonies.find((harmony) => harmony.angles.length === numDots);
- }
- if (action === 'add') {
- return colorHarmonies.find((harmony) => harmony.angles.length + 1 === numDots);
- }
- if (action === 'update') {
- return colorHarmonies.find((harmony) => harmony.angles.length + 1 === numDots);
+ if (action === 'add') {
+ return colorHarmonies.find(
+ (harmony) => harmony.angles.length === selectedHarmony.angles.length + 1
+ );
+ }
+ if (action === 'update') {
+ return selectedHarmony;
+ }
}
}
- function getAngleFromPosition(position, centerPosition) {
- let deltaX = position.x - centerPosition.x;
- let deltaY = position.y - centerPosition.y;
- let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
- return (angle + 360) % 360;
+ if (action === 'remove') {
+ return colorHarmonies.find((harmony) => harmony.angles.length === numDots);
}
-
- function getDistanceFromCenter(position, centerPosition) {
- const deltaX = position.x - centerPosition.x;
- const deltaY = position.y - centerPosition.y;
- return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
+ if (action === 'add') {
+ return colorHarmonies.find((harmony) => harmony.angles.length + 1 === numDots);
}
+ if (action === 'update') {
+ return colorHarmonies.find((harmony) => harmony.angles.length + 1 === numDots);
+ }
+ }
- const dotPad = this.panel.querySelector('.zen-theme-picker-gradient');
- const rect = dotPad.getBoundingClientRect();
- const padding = 30;
-
- let updatedDots = [...dots];
- const centerPosition = { x: rect.width / 2, y: rect.height / 2 };
+ function getAngleFromPosition(position, centerPosition) {
+ let deltaX = position.x - centerPosition.x;
+ let deltaY = position.y - centerPosition.y;
+ let angle = Math.atan2(deltaY, deltaX) * (180 / Math.PI);
+ return (angle + 360) % 360;
+ }
- const harmonyAngles = getColorHarmonyType(
- dots.length + (action === 'add' ? 1 : action === 'remove' ? -1 : 0),
- this.dots
- );
- this.useAlgo = harmonyAngles.type;
- if (!harmonyAngles || harmonyAngles.angles.length === 0) return dots;
+ function getDistanceFromCenter(position, centerPosition) {
+ const deltaX = position.x - centerPosition.x;
+ const deltaY = position.y - centerPosition.y;
+ return Math.sqrt(deltaX * deltaX + deltaY * deltaY);
+ }
- let primaryDot = dots.find((dot) => dot.ID === 0);
- if (!primaryDot) return [];
+ const dotPad = this.panel.querySelector('.zen-theme-picker-gradient');
+ const rect = dotPad.getBoundingClientRect();
+ const padding = 30;
- if (action === 'add' && this.dots.length) {
- updatedDots.push({ ID: this.dots.length, position: centerPosition });
- }
- const baseAngle = getAngleFromPosition(primaryDot.position, centerPosition);
- let distance = getDistanceFromCenter(primaryDot.position, centerPosition);
- const radius = (rect.width - padding) / 2;
- if (distance > radius) distance = radius;
- if (this.dots.length > 0) {
- updatedDots = [
- {
- ID: 0,
- position: primaryDot.position,
- type: primaryDot.type,
- },
- ];
- }
+ let updatedDots = [...dots];
+ const centerPosition = { x: rect.width / 2, y: rect.height / 2 };
- harmonyAngles.angles.forEach((angleOffset, index) => {
- let newAngle = (baseAngle + angleOffset) % 360;
- let radian = (newAngle * Math.PI) / 180;
+ const harmonyAngles = getColorHarmonyType(
+ dots.length + (action === 'add' ? 1 : action === 'remove' ? -1 : 0),
+ this.dots
+ );
+ this.useAlgo = harmonyAngles.type;
+ if (!harmonyAngles || harmonyAngles.angles.length === 0) return dots;
- let newPosition = {
- x: centerPosition.x + distance * Math.cos(radian),
- y: centerPosition.y + distance * Math.sin(radian),
- };
+ let primaryDot = dots.find((dot) => dot.ID === 0);
+ if (!primaryDot) return [];
- updatedDots.push({
- ID: index + 1,
- position: newPosition,
+ if (action === 'add' && this.dots.length) {
+ updatedDots.push({ ID: this.dots.length, position: centerPosition });
+ }
+ const baseAngle = getAngleFromPosition(primaryDot.position, centerPosition);
+ let distance = getDistanceFromCenter(primaryDot.position, centerPosition);
+ const radius = (rect.width - padding) / 2;
+ if (distance > radius) distance = radius;
+ if (this.dots.length > 0) {
+ updatedDots = [
+ {
+ ID: 0,
+ position: primaryDot.position,
type: primaryDot.type,
- });
+ },
+ ];
+ }
+
+ harmonyAngles.angles.forEach((angleOffset, index) => {
+ let newAngle = (baseAngle + angleOffset) % 360;
+ let radian = (newAngle * Math.PI) / 180;
+
+ let newPosition = {
+ x: centerPosition.x + distance * Math.cos(radian),
+ y: centerPosition.y + distance * Math.sin(radian),
+ };
+
+ updatedDots.push({
+ ID: index + 1,
+ position: newPosition,
+ type: primaryDot.type,
});
+ });
+
+ return updatedDots;
+ }
- return updatedDots;
+ handleColorPositions(colorPositions, ignoreLegacy = false) {
+ colorPositions.sort((a, b) => a.ID - b.ID);
+
+ if (this.isLegacyVersion && !ignoreLegacy) {
+ this.isLegacyVersion = false;
+ Services.prefs.setIntPref('zen.theme.gradient-legacy-version', 1);
}
- handleColorPositions(colorPositions, ignoreLegacy = false) {
- colorPositions.sort((a, b) => a.ID - b.ID);
+ colorPositions.forEach((dotPosition) => {
+ const existingDot = this.dots.find((dot) => dot.ID === dotPosition.ID);
+
+ if (existingDot) {
+ existingDot.type = dotPosition.type;
+ existingDot.position = dotPosition.position;
+ const colorFromPos = this.getColorFromPosition(
+ dotPosition.position.x,
+ dotPosition.position.y,
+ dotPosition.type
+ );
+ existingDot.lightness = this.#currentLightness;
+ existingDot.element.style.setProperty(
+ '--zen-theme-picker-dot-color',
+ `rgb(${colorFromPos[0]}, ${colorFromPos[1]}, ${colorFromPos[2]})`
+ );
+ existingDot.element.setAttribute(
+ 'data-position',
+ this.getJSONPos(dotPosition.position.x, dotPosition.position.y)
+ );
+ existingDot.element.setAttribute('data-type', dotPosition.type);
- if (this.isLegacyVersion && !ignoreLegacy) {
- this.isLegacyVersion = false;
- Services.prefs.setIntPref('zen.theme.gradient-legacy-version', 1);
+ if (!this.dragging) {
+ gZenUIManager.motion.animate(
+ existingDot.element,
+ {
+ left: `${dotPosition.position.x}px`,
+ top: `${dotPosition.position.y}px`,
+ },
+ {
+ duration: 0.4,
+ type: 'spring',
+ bounce: 0.3,
+ }
+ );
+ } else {
+ existingDot.element.style.left = `${dotPosition.position.x}px`;
+ existingDot.element.style.top = `${dotPosition.position.y}px`;
+ }
+ } else {
+ this.spawnDot({
+ type: dotPosition.type,
+ ...dotPosition.position,
+ });
}
+ });
+ }
- colorPositions.forEach((dotPosition) => {
- const existingDot = this.dots.find((dot) => dot.ID === dotPosition.ID);
+ onThemePickerClick(event) {
+ if (this._rotating) {
+ return;
+ }
+ if (event.target.closest('#PanelUI-zen-gradient-generator-scheme')) {
+ return;
+ }
+ event.preventDefault();
+ const target = event.target;
+ if (target.id === 'PanelUI-zen-gradient-generator-color-add') {
+ if (this.dots.length >= nsZenThemePicker.MAX_DOTS) return;
+ let colorPositions = this.calculateCompliments(this.dots, 'add', this.useAlgo);
- if (existingDot) {
- existingDot.type = dotPosition.type;
- existingDot.position = dotPosition.position;
- const colorFromPos = this.getColorFromPosition(
- dotPosition.position.x,
- dotPosition.position.y,
- dotPosition.type
- );
- existingDot.lightness = this.#currentLightness;
- existingDot.element.style.setProperty(
- '--zen-theme-picker-dot-color',
- `rgb(${colorFromPos[0]}, ${colorFromPos[1]}, ${colorFromPos[2]})`
- );
- existingDot.element.setAttribute(
- 'data-position',
- this.getJSONPos(dotPosition.position.x, dotPosition.position.y)
- );
- existingDot.element.setAttribute('data-type', dotPosition.type);
-
- if (!this.dragging) {
- gZenUIManager.motion.animate(
- existingDot.element,
- {
- left: `${dotPosition.position.x}px`,
- top: `${dotPosition.position.y}px`,
- },
- {
- duration: 0.4,
- type: 'spring',
- bounce: 0.3,
- }
- );
- } else {
- existingDot.element.style.left = `${dotPosition.position.x}px`;
- existingDot.element.style.top = `${dotPosition.position.y}px`;
- }
+ this.handleColorPositions(colorPositions);
+ this.updateCurrentWorkspace();
+ return;
+ } else if (target.id === 'PanelUI-zen-gradient-generator-color-remove') {
+ this.dots.sort((a, b) => a.ID - b.ID);
+ if (this.dots.length === 0) return;
+
+ const lastDot = this.dots.pop();
+ lastDot.element.remove();
+
+ this.dots.forEach((dot, index) => {
+ dot.ID = index;
+ if (index === 0) {
+ dot.element.classList.add('primary');
} else {
- this.spawnDot({
- type: dotPosition.type,
- ...dotPosition.position,
- });
+ dot.element.classList.remove('primary');
}
});
+
+ let colorPositions = this.calculateCompliments(this.dots, 'remove');
+ this.handleColorPositions(colorPositions);
+ this.updateCurrentWorkspace();
+ return;
}
- onThemePickerClick(event) {
- if (this._rotating) {
- return;
- }
- if (event.target.closest('#PanelUI-zen-gradient-generator-scheme')) {
- return;
- }
+ if (event.button !== 0 || this.dragging || this.recentlyDragged) return;
+
+ const gradient = this.panel.querySelector('.zen-theme-picker-gradient');
+ const rect = gradient.getBoundingClientRect();
+ const padding = 30;
+
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+ const radius = (rect.width - padding) / 2;
+ let pixelX = event.clientX;
+ let pixelY = event.clientY;
+
+ const clickedElement = event.target;
+ let clickedDot = null;
+ const existingPrimaryDot = this.dots.find((d) => d.ID === 0);
+
+ clickedDot = this.dots.find((dot) => dot.element === clickedElement);
+
+ if (clickedDot) {
+ // TODO: this doesnt work and needs to be fixed
+ existingPrimaryDot.ID = clickedDot.ID;
+ clickedDot.ID = 0;
+ clickedDot.element.style.zIndex = 999;
+
+ let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo);
+ this.handleColorPositions(colorPositions);
+ return;
+ }
+
+ const distance = Math.sqrt((pixelX - centerX) ** 2 + (pixelY - centerY) ** 2);
+ if (distance > radius) {
+ const angle = Math.atan2(pixelY - centerY, pixelX - centerX);
+ pixelX = centerX + Math.cos(angle) * radius;
+ pixelY = centerY + Math.sin(angle) * radius;
+ }
+
+ const relativeX = pixelX - rect.left;
+ const relativeY = pixelY - rect.top;
+
+ if (!clickedDot && this.dots.length < 1) {
+ this.spawnDot({ x: relativeX, y: relativeY }, this.dots.length === 0);
+
+ this.updateCurrentWorkspace(true);
+ } else if (!clickedDot && existingPrimaryDot) {
+ existingPrimaryDot.position = {
+ x: relativeX,
+ y: relativeY,
+ };
+
+ let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo);
+ this.handleColorPositions(colorPositions);
+ this.updateCurrentWorkspace(true);
+
+ gZenUIManager.motion.animate(
+ existingPrimaryDot.element,
+ {
+ left: `${existingPrimaryDot.position.x}px`,
+ top: `${existingPrimaryDot.position.y}px`,
+ },
+ {
+ duration: 0.4,
+ type: 'spring',
+ bounce: 0.3,
+ }
+ );
+ }
+ }
+
+ onDotMouseDown(event) {
+ if (event.button === 2) {
+ return;
+ }
+ const draggedDot = this.dots.find((dot) => dot.element === event.target);
+ if (draggedDot) {
event.preventDefault();
- const target = event.target;
- if (target.id === 'PanelUI-zen-gradient-generator-color-add') {
- if (this.dots.length >= nsZenThemePicker.MAX_DOTS) return;
- let colorPositions = this.calculateCompliments(this.dots, 'add', this.useAlgo);
+ this.dragging = true;
+ this.draggedDot = event.target;
+ this.draggedDot.classList.add('dragging');
+ }
- this.handleColorPositions(colorPositions);
- this.updateCurrentWorkspace();
- return;
- } else if (target.id === 'PanelUI-zen-gradient-generator-color-remove') {
- this.dots.sort((a, b) => a.ID - b.ID);
- if (this.dots.length === 0) return;
-
- const lastDot = this.dots.pop();
- lastDot.element.remove();
-
- this.dots.forEach((dot, index) => {
- dot.ID = index;
- if (index === 0) {
- dot.element.classList.add('primary');
- } else {
- dot.element.classList.remove('primary');
- }
- });
+ // Store the starting position of the drag
+ this.dragStartPosition = {
+ x: event.clientX,
+ y: event.clientY,
+ };
+ }
- let colorPositions = this.calculateCompliments(this.dots, 'remove');
- this.handleColorPositions(colorPositions);
- this.updateCurrentWorkspace();
+ onDotMouseUp(event) {
+ if (this._rotating) {
+ return;
+ }
+ if (event.button === 2) {
+ if (!event.target.classList.contains('zen-theme-picker-dot')) {
return;
}
+ this.dots = this.dots.filter((dot) => dot.element !== event.target);
+ event.target.remove();
+
+ this.dots.sort((a, b) => a.ID - b.ID);
+
+ // Reassign the IDs after sorting
+ this.dots.forEach((dot, index) => {
+ dot.ID = index;
+ if (index === 0) {
+ dot.element.classList.add('primary');
+ } else {
+ dot.element.classList.remove('primary');
+ }
+ });
+
+ let colorPositions = this.calculateCompliments(this.dots, 'remove');
+ this.handleColorPositions(colorPositions);
+
+ this.updateCurrentWorkspace();
+ return;
+ }
- if (event.button !== 0 || this.dragging || this.recentlyDragged) return;
+ if (this.dragging) {
+ event.preventDefault();
+ event.stopPropagation();
+ this.dragging = false;
+ this.draggedDot.classList.remove('dragging');
+ this.draggedDot = null;
+ this.dragStartPosition = null; // Reset the drag start position
+
+ this.recentlyDragged = true;
+ setTimeout(() => {
+ this.recentlyDragged = false;
+ }, 100);
+ return;
+ }
+ }
- const gradient = this.panel.querySelector('.zen-theme-picker-gradient');
- const rect = gradient.getBoundingClientRect();
- const padding = 30;
+ onDotMouseMove(event) {
+ if (this.dragging) {
+ event.preventDefault();
+ const rect = this.panel.querySelector('.zen-theme-picker-gradient').getBoundingClientRect();
+ const padding = 30; // each side
+ // do NOT let the ball be draged outside of an imaginary circle. You can drag it anywhere inside the circle
+ // if the distance between the center of the circle and the dragged ball is bigger than the radius, then the ball
+ // should be placed on the edge of the circle. If it's inside the circle, then the ball just follows the mouse
const centerX = rect.left + rect.width / 2;
const centerY = rect.top + rect.height / 2;
const radius = (rect.width - padding) / 2;
let pixelX = event.clientX;
let pixelY = event.clientY;
-
- const clickedElement = event.target;
- let clickedDot = null;
- const existingPrimaryDot = this.dots.find((d) => d.ID === 0);
-
- clickedDot = this.dots.find((dot) => dot.element === clickedElement);
-
- if (clickedDot) {
- // TODO: this doesnt work and needs to be fixed
- existingPrimaryDot.ID = clickedDot.ID;
- clickedDot.ID = 0;
- clickedDot.element.style.zIndex = 999;
-
- let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo);
- this.handleColorPositions(colorPositions);
- return;
- }
-
const distance = Math.sqrt((pixelX - centerX) ** 2 + (pixelY - centerY) ** 2);
if (distance > radius) {
const angle = Math.atan2(pixelY - centerY, pixelX - centerX);
@@ -894,818 +999,692 @@
pixelY = centerY + Math.sin(angle) * radius;
}
+ // set the location of the dot in pixels
const relativeX = pixelX - rect.left;
const relativeY = pixelY - rect.top;
- if (!clickedDot && this.dots.length < 1) {
- this.spawnDot({ x: relativeX, y: relativeY }, this.dots.length === 0);
-
- this.updateCurrentWorkspace(true);
- } else if (!clickedDot && existingPrimaryDot) {
- existingPrimaryDot.position = {
- x: relativeX,
- y: relativeY,
- };
-
- let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo);
- this.handleColorPositions(colorPositions);
- this.updateCurrentWorkspace(true);
-
- gZenUIManager.motion.animate(
- existingPrimaryDot.element,
- {
- left: `${existingPrimaryDot.position.x}px`,
- top: `${existingPrimaryDot.position.y}px`,
- },
- {
- duration: 0.4,
- type: 'spring',
- bounce: 0.3,
- }
- );
- }
- }
-
- onDotMouseDown(event) {
- if (event.button === 2) {
- return;
- }
- const draggedDot = this.dots.find((dot) => dot.element === event.target);
- if (draggedDot) {
- event.preventDefault();
- this.dragging = true;
- this.draggedDot = event.target;
- this.draggedDot.classList.add('dragging');
- }
-
- // Store the starting position of the drag
- this.dragStartPosition = {
- x: event.clientX,
- y: event.clientY,
+ const draggedDot = this.dots.find((dot) => dot.element === this.draggedDot);
+ draggedDot.element.style.left = `${relativeX}px`;
+ draggedDot.element.style.top = `${relativeY}px`;
+ draggedDot.position = {
+ x: relativeX,
+ y: relativeY,
};
- }
+ let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo);
+ this.handleColorPositions(colorPositions);
- onDotMouseUp(event) {
- if (this._rotating) {
- return;
- }
- if (event.button === 2) {
- if (!event.target.classList.contains('zen-theme-picker-dot')) {
- return;
- }
- this.dots = this.dots.filter((dot) => dot.element !== event.target);
- event.target.remove();
-
- this.dots.sort((a, b) => a.ID - b.ID);
-
- // Reassign the IDs after sorting
- this.dots.forEach((dot, index) => {
- dot.ID = index;
- if (index === 0) {
- dot.element.classList.add('primary');
- } else {
- dot.element.classList.remove('primary');
- }
- });
-
- let colorPositions = this.calculateCompliments(this.dots, 'remove');
- this.handleColorPositions(colorPositions);
+ this.updateCurrentWorkspace();
+ }
+ }
- this.updateCurrentWorkspace();
- return;
- }
+ themedColors(colors) {
+ // For non-Mica themes, we return the colors as they are
+ return [...colors];
+ }
- if (this.dragging) {
- event.preventDefault();
- event.stopPropagation();
- this.dragging = false;
- this.draggedDot.classList.remove('dragging');
- this.draggedDot = null;
- this.dragStartPosition = null; // Reset the drag start position
-
- this.recentlyDragged = true;
- setTimeout(() => {
- this.recentlyDragged = false;
- }, 100);
- return;
- }
+ onOpacityChange(event) {
+ this.currentOpacity = parseFloat(event.target.value);
+ // If we reached a whole number (e.g., 0.1, 0.2, etc.), send a haptic feedback.
+ if (Math.round(this.currentOpacity * 10) !== this._lastHapticFeedback) {
+ Services.zen.playHapticFeedback();
+ this._lastHapticFeedback = Math.round(this.currentOpacity * 10);
}
+ this.updateCurrentWorkspace();
+ }
- onDotMouseMove(event) {
- if (this.dragging) {
- event.preventDefault();
- const rect = this.panel.querySelector('.zen-theme-picker-gradient').getBoundingClientRect();
- const padding = 30; // each side
- // do NOT let the ball be draged outside of an imaginary circle. You can drag it anywhere inside the circle
- // if the distance between the center of the circle and the dragged ball is bigger than the radius, then the ball
- // should be placed on the edge of the circle. If it's inside the circle, then the ball just follows the mouse
-
- const centerX = rect.left + rect.width / 2;
- const centerY = rect.top + rect.height / 2;
- const radius = (rect.width - padding) / 2;
- let pixelX = event.clientX;
- let pixelY = event.clientY;
- const distance = Math.sqrt((pixelX - centerX) ** 2 + (pixelY - centerY) ** 2);
- if (distance > radius) {
- const angle = Math.atan2(pixelY - centerY, pixelX - centerX);
- pixelX = centerX + Math.cos(angle) * radius;
- pixelY = centerY + Math.sin(angle) * radius;
- }
+ getToolbarModifiedBaseRaw() {
+ const opacity = this.#allowTransparencyOnSidebar ? 0.6 : 1;
+ return this.isDarkMode ? [23, 23, 26, opacity] : [240, 240, 244, opacity];
+ }
- // set the location of the dot in pixels
- const relativeX = pixelX - rect.left;
- const relativeY = pixelY - rect.top;
+ getToolbarModifiedBase() {
+ const baseColor = this.getToolbarModifiedBaseRaw();
+ return `rgba(${baseColor[0]}, ${baseColor[1]}, ${baseColor[2]}, ${baseColor[3]})`;
+ }
- const draggedDot = this.dots.find((dot) => dot.element === this.draggedDot);
- draggedDot.element.style.left = `${relativeX}px`;
- draggedDot.element.style.top = `${relativeY}px`;
- draggedDot.position = {
- x: relativeX,
- y: relativeY,
- };
- let colorPositions = this.calculateCompliments(this.dots, 'update', this.useAlgo);
- this.handleColorPositions(colorPositions);
+ get isMica() {
+ return window.matchMedia('(-moz-windows-mica)').matches;
+ }
- this.updateCurrentWorkspace();
- }
- }
+ get canBeTransparent() {
+ return (
+ this.isMica ||
+ window.matchMedia(
+ '(-moz-platform: macos) or ((-moz-platform: linux) and -moz-pref("zen.widget.linux.transparency"))'
+ ).matches
+ );
+ }
- themedColors(colors) {
- // For non-Mica themes, we return the colors as they are
- return [...colors];
+ blendWithWhiteOverlay(baseColor, opacity) {
+ let colorToBlend;
+ let colorToBlendOpacity;
+ if (this.isMica) {
+ colorToBlend = !this.isDarkMode ? [0, 0, 0] : [255, 255, 255];
+ colorToBlendOpacity = 0.35;
+ } else if (AppConstants.platform === 'macosx') {
+ colorToBlend = [255, 255, 255];
+ colorToBlendOpacity = 0.3;
+ }
+ if (colorToBlend) {
+ const blendedAlpha = Math.min(
+ 1,
+ opacity + MIN_OPACITY + colorToBlendOpacity * (1 - (opacity + MIN_OPACITY))
+ );
+ baseColor = this.blendColors(baseColor, colorToBlend, blendedAlpha * 100);
+ opacity += colorToBlendOpacity * (1 - opacity);
}
+ return `rgba(${baseColor[0]}, ${baseColor[1]}, ${baseColor[2]}, ${opacity})`;
+ }
- onOpacityChange(event) {
- this.currentOpacity = parseFloat(event.target.value);
- // If we reached a whole number (e.g., 0.1, 0.2, etc.), send a haptic feedback.
- if (Math.round(this.currentOpacity * 10) !== this._lastHapticFeedback) {
- Services.zen.playHapticFeedback();
- this._lastHapticFeedback = Math.round(this.currentOpacity * 10);
- }
- this.updateCurrentWorkspace();
+ getSingleRGBColor(color, forToolbar = false) {
+ if (color.isCustom) {
+ return color.c;
+ }
+ let opacity = this.currentOpacity;
+ if (
+ (forToolbar && !this.#allowTransparencyOnSidebar) ||
+ (!forToolbar && !this.canBeTransparent)
+ ) {
+ color = this.blendColors(
+ color.c,
+ this.getToolbarModifiedBaseRaw().slice(0, 3),
+ this.canBeTransparent ? 90 : opacity * 100
+ );
+ opacity = 1; // Toolbar colors should always be fully opaque
+ } else {
+ color = color.c;
}
-
- getToolbarModifiedBaseRaw() {
- const opacity = this.#allowTransparencyOnSidebar ? 0.6 : 1;
- return this.isDarkMode ? [23, 23, 26, opacity] : [240, 240, 244, opacity];
+ if (this.isLegacyVersion && this.isDarkMode) {
+ // In legacy version, we blend with white overlay or black overlay based on if we are in dark mode
+ color = this.blendColors(color, [0, 0, 0], 30);
}
+ return this.blendWithWhiteOverlay(color, opacity);
+ }
- getToolbarModifiedBase() {
- const baseColor = this.getToolbarModifiedBaseRaw();
- return `rgba(${baseColor[0]}, ${baseColor[1]}, ${baseColor[2]}, ${baseColor[3]})`;
- }
+ luminance([r, g, b]) {
+ // These magic numbers are extracted from the wikipedia article on relative luminance
+ // https://en.wikipedia.org/wiki/Relative_luminance
+ var a = [r, g, b].map((v) => {
+ v /= 255;
+ return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
+ });
+ return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
+ }
- get isMica() {
- return window.matchMedia('(-moz-windows-mica)').matches;
- }
+ contrastRatio(rgb1, rgb2) {
+ const lum1 = this.luminance(rgb1);
+ const lum2 = this.luminance(rgb2);
+ const brightest = Math.max(lum1, lum2);
+ const darkest = Math.min(lum1, lum2);
+ return (brightest + 0.05) / (darkest + 0.05);
+ }
- get canBeTransparent() {
- return (
- this.isMica ||
- window.matchMedia(
- '(-moz-platform: macos) or ((-moz-platform: linux) and -moz-pref("zen.widget.linux.transparency"))'
- ).matches
- );
- }
+ blendColors(rgb1, rgb2, percentage) {
+ const p = percentage / 100;
+ return [
+ Math.round(rgb1[0] * p + rgb2[0] * (1 - p)),
+ Math.round(rgb1[1] * p + rgb2[1] * (1 - p)),
+ Math.round(rgb1[2] * p + rgb2[2] * (1 - p)),
+ ];
+ }
- blendWithWhiteOverlay(baseColor, opacity) {
- let colorToBlend;
- let colorToBlendOpacity;
- if (this.isMica) {
- colorToBlend = !this.isDarkMode ? [0, 0, 0] : [255, 255, 255];
- colorToBlendOpacity = 0.35;
- } else if (AppConstants.platform === 'macosx') {
- colorToBlend = [255, 255, 255];
- colorToBlendOpacity = 0.3;
- }
- if (colorToBlend) {
- const blendedAlpha = Math.min(
- 1,
- opacity + MIN_OPACITY + colorToBlendOpacity * (1 - (opacity + MIN_OPACITY))
- );
- baseColor = this.blendColors(baseColor, colorToBlend, blendedAlpha * 100);
- opacity += colorToBlendOpacity * (1 - opacity);
- }
- return `rgba(${baseColor[0]}, ${baseColor[1]}, ${baseColor[2]}, ${opacity})`;
- }
+ getGradient(colors, forToolbar = false) {
+ const themedColors = this.themedColors(colors);
+ this.useAlgo = themedColors[0]?.algorithm ?? '';
+ this.#currentLightness = themedColors[0]?.lightness ?? 50;
- getSingleRGBColor(color, forToolbar = false) {
- if (color.isCustom) {
- return color.c;
- }
- let opacity = this.currentOpacity;
- if (
- (forToolbar && !this.#allowTransparencyOnSidebar) ||
- (!forToolbar && !this.canBeTransparent)
- ) {
- color = this.blendColors(
- color.c,
- this.getToolbarModifiedBaseRaw().slice(0, 3),
- this.canBeTransparent ? 90 : opacity * 100
+ const rotation = -45; // TODO: Detect rotation based on the accent color
+ if (themedColors.length === 0) {
+ const getBrowserBg = () => {
+ if (this.canBeTransparent) {
+ return this.isDarkMode ? 'rgba(0, 0, 0, 0.4)' : 'transparent';
+ }
+ return this.isDarkMode ? '#131313' : '#e9e9e9';
+ };
+ return forToolbar ? this.getToolbarModifiedBase() : getBrowserBg();
+ } else if (themedColors.length === 1) {
+ return this.getSingleRGBColor(themedColors[0], forToolbar);
+ } else {
+ // If there are custom colors, we just return a linear gradient with all colors
+ if (themedColors.find((color) => color.isCustom)) {
+ // Just return a linear gradient with all colors
+ const gradientColors = themedColors.map((color) =>
+ this.getSingleRGBColor(color, forToolbar)
);
- opacity = 1; // Toolbar colors should always be fully opaque
- } else {
- color = color.c;
- }
- if (this.isLegacyVersion && this.isDarkMode) {
- // In legacy version, we blend with white overlay or black overlay based on if we are in dark mode
- color = this.blendColors(color, [0, 0, 0], 30);
+ // Divide all colors evenly in the gradient
+ const colorStops = gradientColors
+ .map((color, index) => {
+ const position = (index / (gradientColors.length - 1)) * 100;
+ return `${color} ${position}%`;
+ })
+ .join(', ');
+ return `linear-gradient(${rotation}deg, ${colorStops})`;
+ }
+ if (themedColors.length === 2) {
+ if (!forToolbar) {
+ return [
+ `linear-gradient(${rotation}deg, ${this.getSingleRGBColor(themedColors[1], forToolbar)} 0%, transparent 100%)`,
+ `linear-gradient(${rotation + 180}deg, ${this.getSingleRGBColor(themedColors[0], forToolbar)} 0%, transparent 80%)`,
+ ]
+ .reverse()
+ .join(', ');
+ }
+ return `linear-gradient(${rotation}deg, ${this.getSingleRGBColor(themedColors[1], forToolbar)} 0%, ${this.getSingleRGBColor(themedColors[0], forToolbar)} 100%)`;
+ } else if (themedColors.length === 3) {
+ let color1 = this.getSingleRGBColor(themedColors[2], forToolbar);
+ let color2 = this.getSingleRGBColor(themedColors[0], forToolbar);
+ let color3 = this.getSingleRGBColor(themedColors[1], forToolbar);
+ return [
+ `linear-gradient(to top, ${color1} -50%, transparent 125%)`,
+ `radial-gradient(circle at 0% 0%, ${color2} 10%, transparent 80%)`,
+ `radial-gradient(circle at 100% -100%, ${color3} -100%, transparent 400%)`,
+ ].join(', ');
}
- return this.blendWithWhiteOverlay(color, opacity);
}
+ }
- luminance([r, g, b]) {
- // These magic numbers are extracted from the wikipedia article on relative luminance
- // https://en.wikipedia.org/wiki/Relative_luminance
- var a = [r, g, b].map((v) => {
- v /= 255;
- return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4);
- });
- return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722;
+ shouldBeDarkMode(accentColor) {
+ if (Services.prefs.getBoolPref('zen.theme.use-system-colors')) {
+ return this.isDarkMode;
}
- contrastRatio(rgb1, rgb2) {
- const lum1 = this.luminance(rgb1);
- const lum2 = this.luminance(rgb2);
- const brightest = Math.max(lum1, lum2);
- const darkest = Math.min(lum1, lum2);
- return (brightest + 0.05) / (darkest + 0.05);
+ if (!this.canBeTransparent) {
+ const toolbarBg = this.getToolbarModifiedBaseRaw();
+ accentColor = this.blendColors(
+ toolbarBg.slice(0, 3),
+ accentColor,
+ (1 - this.currentOpacity) * 100
+ );
}
- blendColors(rgb1, rgb2, percentage) {
- const p = percentage / 100;
- return [
- Math.round(rgb1[0] * p + rgb2[0] * (1 - p)),
- Math.round(rgb1[1] * p + rgb2[1] * (1 - p)),
- Math.round(rgb1[2] * p + rgb2[2] * (1 - p)),
- ];
- }
+ const bg = accentColor;
- getGradient(colors, forToolbar = false) {
- const themedColors = this.themedColors(colors);
- this.useAlgo = themedColors[0]?.algorithm ?? '';
- this.#currentLightness = themedColors[0]?.lightness ?? 50;
+ // Get text colors (with alpha)
+ let darkText = this.getToolbarColor(true); // e.g. [r, g, b, a]
+ let lightText = this.getToolbarColor(false); // e.g. [r, g, b, a]
- const rotation = -45; // TODO: Detect rotation based on the accent color
- if (themedColors.length === 0) {
- const getBrowserBg = () => {
- if (this.canBeTransparent) {
- return this.isDarkMode ? 'rgba(0, 0, 0, 0.4)' : 'transparent';
- }
- return this.isDarkMode ? '#131313' : '#e9e9e9';
- };
- return forToolbar ? this.getToolbarModifiedBase() : getBrowserBg();
- } else if (themedColors.length === 1) {
- return this.getSingleRGBColor(themedColors[0], forToolbar);
- } else {
- // If there are custom colors, we just return a linear gradient with all colors
- if (themedColors.find((color) => color.isCustom)) {
- // Just return a linear gradient with all colors
- const gradientColors = themedColors.map((color) =>
- this.getSingleRGBColor(color, forToolbar)
- );
- // Divide all colors evenly in the gradient
- const colorStops = gradientColors
- .map((color, index) => {
- const position = (index / (gradientColors.length - 1)) * 100;
- return `${color} ${position}%`;
- })
- .join(', ');
- return `linear-gradient(${rotation}deg, ${colorStops})`;
- }
- if (themedColors.length === 2) {
- if (!forToolbar) {
- return [
- `linear-gradient(${rotation}deg, ${this.getSingleRGBColor(themedColors[1], forToolbar)} 0%, transparent 100%)`,
- `linear-gradient(${rotation + 180}deg, ${this.getSingleRGBColor(themedColors[0], forToolbar)} 0%, transparent 80%)`,
- ]
- .reverse()
- .join(', ');
- }
- return `linear-gradient(${rotation}deg, ${this.getSingleRGBColor(themedColors[1], forToolbar)} 0%, ${this.getSingleRGBColor(themedColors[0], forToolbar)} 100%)`;
- } else if (themedColors.length === 3) {
- let color1 = this.getSingleRGBColor(themedColors[2], forToolbar);
- let color2 = this.getSingleRGBColor(themedColors[0], forToolbar);
- let color3 = this.getSingleRGBColor(themedColors[1], forToolbar);
- return [
- `linear-gradient(to top, ${color1} -50%, transparent 125%)`,
- `radial-gradient(circle at 0% 0%, ${color2} 10%, transparent 80%)`,
- `radial-gradient(circle at 100% -100%, ${color3} -100%, transparent 400%)`,
- ].join(', ');
- }
- }
+ if (this.canBeTransparent) {
+ lightText[3] -= parseFloat(this.darkModeBias); // Reduce alpha for light text
}
- shouldBeDarkMode(accentColor) {
- if (Services.prefs.getBoolPref('zen.theme.use-system-colors')) {
- return this.isDarkMode;
- }
+ // Composite text color over background
+ darkText = this.blendColors(bg, darkText.slice(0, 3), (1 - darkText[3]) * 100);
+ lightText = this.blendColors(bg, lightText.slice(0, 3), (1 - lightText[3]) * 100);
- if (!this.canBeTransparent) {
- const toolbarBg = this.getToolbarModifiedBaseRaw();
- accentColor = this.blendColors(
- toolbarBg.slice(0, 3),
- accentColor,
- (1 - this.currentOpacity) * 100
- );
- }
+ const darkContrast = this.contrastRatio(bg, darkText);
+ const lightContrast = this.contrastRatio(bg, lightText);
- const bg = accentColor;
+ return darkContrast > lightContrast;
+ }
- // Get text colors (with alpha)
- let darkText = this.getToolbarColor(true); // e.g. [r, g, b, a]
- let lightText = this.getToolbarColor(false); // e.g. [r, g, b, a]
+ static getTheme(colors = [], opacity = 0.5, texture = 0) {
+ return {
+ type: 'gradient',
+ gradientColors: colors ? colors.filter((color) => color) : [], // remove undefined
+ opacity,
+ texture,
+ };
+ }
- if (this.canBeTransparent) {
- lightText[3] -= parseFloat(this.darkModeBias); // Reduce alpha for light text
- }
+ updateNoise(texture) {
+ document.documentElement.style.setProperty('--zen-grainy-background-opacity', texture);
+ document.documentElement.setAttribute(
+ 'zen-show-grainy-background',
+ texture > 0 ? 'true' : 'false'
+ );
+ }
- // Composite text color over background
- darkText = this.blendColors(bg, darkText.slice(0, 3), (1 - darkText[3]) * 100);
- lightText = this.blendColors(bg, lightText.slice(0, 3), (1 - lightText[3]) * 100);
+ hexToRgb(hex) {
+ if (hex.startsWith('#')) {
+ hex = hex.substring(1);
+ }
+ if (hex.length === 3) {
+ hex = hex
+ .split('')
+ .map((char) => char + char)
+ .join('');
+ }
+ return [
+ parseInt(hex.substring(0, 2), 16),
+ parseInt(hex.substring(2, 4), 16),
+ parseInt(hex.substring(4, 6), 16),
+ ];
+ }
- const darkContrast = this.contrastRatio(bg, darkText);
- const lightContrast = this.contrastRatio(bg, lightText);
+ /**
+ * Get the primary color from a list of colors.
+ * @returns {string} The primary color in hex format.
+ */
+ getAccentColorForUI(accentColor) {
+ return `rgb(${accentColor[0]}, ${accentColor[1]}, ${accentColor[2]})`;
+ }
- return darkContrast > lightContrast;
+ getMostDominantColor(allColors) {
+ const color = this.getPrimaryColor(allColors);
+ if (typeof color === 'string') {
+ // We found a custom color, we should rather return the native accent color
+ return this.getNativeAccentColor();
}
+ return color;
+ }
- static getTheme(colors = [], opacity = 0.5, texture = 0) {
- return {
- type: 'gradient',
- gradientColors: colors ? colors.filter((color) => color) : [], // remove undefined
- opacity,
- texture,
- };
- }
+ getToolbarColor(isDarkMode = false) {
+ return isDarkMode ? [255, 255, 255, 0.8] : [0, 0, 0, 0.8]; // Default toolbar
+ }
- updateNoise(texture) {
- document.documentElement.style.setProperty('--zen-grainy-background-opacity', texture);
- document.documentElement.setAttribute(
- 'zen-show-grainy-background',
- texture > 0 ? 'true' : 'false'
- );
- }
+ async onWorkspaceChange(workspace, skipUpdate = false, theme = null) {
+ const uuid = workspace.uuid;
+ // Use theme from workspace object or passed theme
+ let workspaceTheme = theme || workspace.theme;
- hexToRgb(hex) {
- if (hex.startsWith('#')) {
- hex = hex.substring(1);
- }
- if (hex.length === 3) {
- hex = hex
- .split('')
- .map((char) => char + char)
- .join('');
+ await this.foreachWindowAsActive(async (browser) => {
+ if (!browser.gZenThemePicker?.promiseInitialized) {
+ return;
}
- return [
- parseInt(hex.substring(0, 2), 16),
- parseInt(hex.substring(2, 4), 16),
- parseInt(hex.substring(4, 6), 16),
- ];
- }
-
- /**
- * Get the primary color from a list of colors.
- * @returns {string} The primary color in hex format.
- */
- getAccentColorForUI(accentColor) {
- return `rgb(${accentColor[0]}, ${accentColor[1]}, ${accentColor[2]})`;
- }
- getMostDominantColor(allColors) {
- const color = this.getPrimaryColor(allColors);
- if (typeof color === 'string') {
- // We found a custom color, we should rather return the native accent color
- return this.getNativeAccentColor();
+ if (browser.closing || (await browser.gZenThemePicker?.promiseInitialized)) {
+ return;
}
- return color;
- }
-
- getToolbarColor(isDarkMode = false) {
- return isDarkMode ? [255, 255, 255, 0.8] : [0, 0, 0, 0.8]; // Default toolbar
- }
-
- async onWorkspaceChange(workspace, skipUpdate = false, theme = null) {
- const uuid = workspace.uuid;
- // Use theme from workspace object or passed theme
- let workspaceTheme = theme || workspace.theme;
-
- await this.foreachWindowAsActive(async (browser) => {
- if (!browser.gZenThemePicker?.promiseInitialized) {
- return;
- }
-
- if (browser.closing || (await browser.gZenThemePicker?.promiseInitialized)) {
- return;
- }
- if (theme === null) {
- browser.gZenThemePicker.invalidateGradientCache();
- }
+ if (theme === null) {
+ browser.gZenThemePicker.invalidateGradientCache();
+ }
- // Do not rebuild if the workspace is not the same as the current one
- const windowWorkspace = await browser.gZenWorkspaces.getActiveWorkspace();
- if (windowWorkspace.uuid !== uuid) {
- return;
- }
+ // Do not rebuild if the workspace is not the same as the current one
+ const windowWorkspace = await browser.gZenWorkspaces.getActiveWorkspace();
+ if (windowWorkspace.uuid !== uuid) {
+ return;
+ }
- // get the theme from the window
- workspaceTheme = this.fixTheme(theme || windowWorkspace.theme);
- const docElement = browser.document.documentElement;
+ // get the theme from the window
+ workspaceTheme = this.fixTheme(theme || windowWorkspace.theme);
+ const docElement = browser.document.documentElement;
- if (!skipUpdate) {
- for (const dot of browser.gZenThemePicker.panel.querySelectorAll(
- '.zen-theme-picker-dot'
- )) {
- dot.remove();
- }
+ if (!skipUpdate) {
+ for (const dot of browser.gZenThemePicker.panel.querySelectorAll('.zen-theme-picker-dot')) {
+ dot.remove();
}
+ }
- if (theme) {
- const workspaceElement = browser.gZenWorkspaces.workspaceElement(windowWorkspace.uuid);
- if (workspaceElement) {
- workspaceElement.clearThemeStyles();
- }
+ if (theme) {
+ const workspaceElement = browser.gZenWorkspaces.workspaceElement(windowWorkspace.uuid);
+ if (workspaceElement) {
+ workspaceElement.clearThemeStyles();
}
+ }
- if (!skipUpdate) {
- docElement.style.setProperty(
- '--zen-main-browser-background-old',
- docElement.style.getPropertyValue('--zen-main-browser-background')
- );
- docElement.style.setProperty(
- '--zen-main-browser-background-toolbar-old',
- docElement.style.getPropertyValue('--zen-main-browser-background-toolbar')
- );
- docElement.style.setProperty(
- '--zen-background-opacity',
- browser.gZenThemePicker.previousBackgroundOpacity ?? 1
- );
- if (browser.gZenThemePicker.previousBackgroundResolve) {
- browser.gZenThemePicker.previousBackgroundResolve();
- }
- delete browser.gZenThemePicker.previousBackgroundOpacity;
+ if (!skipUpdate) {
+ docElement.style.setProperty(
+ '--zen-main-browser-background-old',
+ docElement.style.getPropertyValue('--zen-main-browser-background')
+ );
+ docElement.style.setProperty(
+ '--zen-main-browser-background-toolbar-old',
+ docElement.style.getPropertyValue('--zen-main-browser-background-toolbar')
+ );
+ docElement.style.setProperty(
+ '--zen-background-opacity',
+ browser.gZenThemePicker.previousBackgroundOpacity ?? 1
+ );
+ if (browser.gZenThemePicker.previousBackgroundResolve) {
+ browser.gZenThemePicker.previousBackgroundResolve();
}
+ delete browser.gZenThemePicker.previousBackgroundOpacity;
+ }
- browser.gZenThemePicker.resetCustomColorList();
-
- browser.gZenThemePicker.currentOpacity = workspaceTheme.opacity ?? 0.5;
- browser.gZenThemePicker.currentTexture = workspaceTheme.texture ?? 0;
+ browser.gZenThemePicker.resetCustomColorList();
- let dominantColor = this.getMostDominantColor(workspaceTheme.gradientColors);
- const isDefaultTheme = !dominantColor;
- if (isDefaultTheme) {
- dominantColor = this.getNativeAccentColor();
- }
+ browser.gZenThemePicker.currentOpacity = workspaceTheme.opacity ?? 0.5;
+ browser.gZenThemePicker.currentTexture = workspaceTheme.texture ?? 0;
- const opacitySlider = browser.document.getElementById(
- 'PanelUI-zen-gradient-generator-opacity'
- );
+ let dominantColor = this.getMostDominantColor(workspaceTheme.gradientColors);
+ const isDefaultTheme = !dominantColor;
+ if (isDefaultTheme) {
+ dominantColor = this.getNativeAccentColor();
+ }
- {
- let opacity = browser.gZenThemePicker.currentOpacity;
- const svg = browser.gZenThemePicker.sliderWavePath;
- /* eslint-disable no-unused-vars */
- const [_, secondStop, thirdStop] = document.querySelectorAll(
- '#PanelUI-zen-gradient-generator-slider-wave-gradient stop'
- );
- // Opacity can only be between MIN_OPACITY to MAX_OPACITY. Make opacity relative to that range
- if (opacity < MIN_OPACITY) {
- opacity = 0;
- } else if (opacity > MAX_OPACITY) {
- opacity = 1;
- } else {
- opacity = (opacity - MIN_OPACITY) / (MAX_OPACITY - MIN_OPACITY);
- }
- if (isDefaultTheme) {
- opacity = 1; // If it's the default theme, we want the wave to be
- }
- // Since it's sine waves, we can't just set the offset to the opacity, we need to calculate it
- // The offset is the percentage of the wave that is visible, so we need to multiply
- // the opacity by 100 to get the percentage.
- // Set the offset of the stops
- secondStop.setAttribute('offset', `${opacity * 100}%`);
- thirdStop.setAttribute('offset', `${opacity * 100}%`);
- const interpolatedPath = this.#interpolateWavePath(opacity);
- svg.setAttribute('d', interpolatedPath);
- opacitySlider.style.setProperty('--zen-thumb-height', `${40 + opacity * 15}px`);
- opacitySlider.style.setProperty('--zen-thumb-width', `${10 + opacity * 15}px`);
- svg.style.stroke =
- interpolatedPath === this.#linePath
- ? thirdStop.getAttribute('stop-color')
- : 'url(#PanelUI-zen-gradient-generator-slider-wave-gradient)';
- }
+ const opacitySlider = browser.document.getElementById(
+ 'PanelUI-zen-gradient-generator-opacity'
+ );
- for (const button of browser.document.querySelectorAll(
- '#PanelUI-zen-gradient-generator-color-actions button'
- )) {
- // disable if there are no buttons
- button.disabled =
- workspaceTheme.gradientColors.length === 0 ||
- (button.id === 'PanelUI-zen-gradient-generator-color-add'
- ? workspaceTheme.gradientColors.length >= nsZenThemePicker.MAX_DOTS
- : false);
- }
- const clickToAdd = browser.document.getElementById(
- 'PanelUI-zen-gradient-generator-color-click-to-add'
+ {
+ let opacity = browser.gZenThemePicker.currentOpacity;
+ const svg = browser.gZenThemePicker.sliderWavePath;
+ /* eslint-disable no-unused-vars */
+ const [_, secondStop, thirdStop] = document.querySelectorAll(
+ '#PanelUI-zen-gradient-generator-slider-wave-gradient stop'
);
- if (workspaceTheme.gradientColors.length > 0) {
- clickToAdd.setAttribute('hidden', 'true');
+ // Opacity can only be between MIN_OPACITY to MAX_OPACITY. Make opacity relative to that range
+ if (opacity < MIN_OPACITY) {
+ opacity = 0;
+ } else if (opacity > MAX_OPACITY) {
+ opacity = 1;
} else {
- clickToAdd.removeAttribute('hidden');
+ opacity = (opacity - MIN_OPACITY) / (MAX_OPACITY - MIN_OPACITY);
+ }
+ if (isDefaultTheme) {
+ opacity = 1; // If it's the default theme, we want the wave to be
}
+ // Since it's sine waves, we can't just set the offset to the opacity, we need to calculate it
+ // The offset is the percentage of the wave that is visible, so we need to multiply
+ // the opacity by 100 to get the percentage.
+ // Set the offset of the stops
+ secondStop.setAttribute('offset', `${opacity * 100}%`);
+ thirdStop.setAttribute('offset', `${opacity * 100}%`);
+ const interpolatedPath = this.#interpolateWavePath(opacity);
+ svg.setAttribute('d', interpolatedPath);
+ opacitySlider.style.setProperty('--zen-thumb-height', `${40 + opacity * 15}px`);
+ opacitySlider.style.setProperty('--zen-thumb-width', `${10 + opacity * 15}px`);
+ svg.style.stroke =
+ interpolatedPath === this.#linePath
+ ? thirdStop.getAttribute('stop-color')
+ : 'url(#PanelUI-zen-gradient-generator-slider-wave-gradient)';
+ }
+
+ for (const button of browser.document.querySelectorAll(
+ '#PanelUI-zen-gradient-generator-color-actions button'
+ )) {
+ // disable if there are no buttons
+ button.disabled =
+ workspaceTheme.gradientColors.length === 0 ||
+ (button.id === 'PanelUI-zen-gradient-generator-color-add'
+ ? workspaceTheme.gradientColors.length >= nsZenThemePicker.MAX_DOTS
+ : false);
+ }
+ const clickToAdd = browser.document.getElementById(
+ 'PanelUI-zen-gradient-generator-color-click-to-add'
+ );
+ if (workspaceTheme.gradientColors.length > 0) {
+ clickToAdd.setAttribute('hidden', 'true');
+ } else {
+ clickToAdd.removeAttribute('hidden');
+ }
- opacitySlider.value = browser.gZenThemePicker.currentOpacity;
- const textureSelectWrapper = browser.document.getElementById(
- 'PanelUI-zen-gradient-generator-texture-wrapper'
- );
- const textureWrapperWidth = textureSelectWrapper.getBoundingClientRect().width;
- // Dont show when hidden
- if (textureWrapperWidth) {
- // rotate and trasnform relative to the wrapper width depending on the texture value
- const textureValue = this.currentTexture;
- const textureHandler = browser.gZenThemePicker._textureHandler;
- const rotation = textureValue * 360 - 90;
- textureHandler.style.transform = `rotate(${rotation + 90}deg)`;
- // add top and left to center the texture handler in relation with textureWrapperWidth
- // based on the rotation
- const top = Math.sin((rotation * Math.PI) / 180) * (textureWrapperWidth / 2) - 6;
- const left = Math.cos((rotation * Math.PI) / 180) * (textureWrapperWidth / 2) - 3;
- textureHandler.style.top = `${textureWrapperWidth / 2 + top}px`;
- textureHandler.style.left = `${textureWrapperWidth / 2 + left}px`;
- // Highlight the 16 buttons based on the texture value
- const buttons = browser.document.querySelectorAll('.zen-theme-picker-texture-dot');
- let i = 4;
- for (const button of buttons) {
- button.classList.toggle('active', i / 16 <= textureValue);
- i++;
- // We start at point 4 because that's the first point that is not in the middle of the texture
- if (i === 16) {
- i = 0;
- }
+ opacitySlider.value = browser.gZenThemePicker.currentOpacity;
+ const textureSelectWrapper = browser.document.getElementById(
+ 'PanelUI-zen-gradient-generator-texture-wrapper'
+ );
+ const textureWrapperWidth = textureSelectWrapper.getBoundingClientRect().width;
+ // Dont show when hidden
+ if (textureWrapperWidth) {
+ // rotate and trasnform relative to the wrapper width depending on the texture value
+ const textureValue = this.currentTexture;
+ const textureHandler = browser.gZenThemePicker._textureHandler;
+ const rotation = textureValue * 360 - 90;
+ textureHandler.style.transform = `rotate(${rotation + 90}deg)`;
+ // add top and left to center the texture handler in relation with textureWrapperWidth
+ // based on the rotation
+ const top = Math.sin((rotation * Math.PI) / 180) * (textureWrapperWidth / 2) - 6;
+ const left = Math.cos((rotation * Math.PI) / 180) * (textureWrapperWidth / 2) - 3;
+ textureHandler.style.top = `${textureWrapperWidth / 2 + top}px`;
+ textureHandler.style.left = `${textureWrapperWidth / 2 + left}px`;
+ // Highlight the 16 buttons based on the texture value
+ const buttons = browser.document.querySelectorAll('.zen-theme-picker-texture-dot');
+ let i = 4;
+ for (const button of buttons) {
+ button.classList.toggle('active', i / 16 <= textureValue);
+ i++;
+ // We start at point 4 because that's the first point that is not in the middle of the texture
+ if (i === 16) {
+ i = 0;
}
}
+ }
- const gradient = browser.gZenThemePicker.getGradient(workspaceTheme.gradientColors);
- const gradientToolbar = browser.gZenThemePicker.getGradient(
- workspaceTheme.gradientColors,
- true
- );
- browser.gZenThemePicker.updateNoise(workspaceTheme.texture);
+ const gradient = browser.gZenThemePicker.getGradient(workspaceTheme.gradientColors);
+ const gradientToolbar = browser.gZenThemePicker.getGradient(
+ workspaceTheme.gradientColors,
+ true
+ );
+ browser.gZenThemePicker.updateNoise(workspaceTheme.texture);
- browser.gZenThemePicker.customColorList.innerHTML = '';
- for (const dot of workspaceTheme.gradientColors) {
- if (dot.isCustom) {
- browser.gZenThemePicker.addColorToCustomList(dot.c);
- }
+ browser.gZenThemePicker.customColorList.innerHTML = '';
+ for (const dot of workspaceTheme.gradientColors) {
+ if (dot.isCustom) {
+ browser.gZenThemePicker.addColorToCustomList(dot.c);
}
+ }
- docElement.style.setProperty('--zen-main-browser-background-toolbar', gradientToolbar);
- docElement.style.setProperty('--zen-main-browser-background', gradient);
- const isDarkModeWindow = browser.gZenThemePicker.isDarkMode;
- if (isDefaultTheme) {
- docElement.setAttribute('zen-default-theme', 'true');
+ docElement.style.setProperty('--zen-main-browser-background-toolbar', gradientToolbar);
+ docElement.style.setProperty('--zen-main-browser-background', gradient);
+ const isDarkModeWindow = browser.gZenThemePicker.isDarkMode;
+ if (isDefaultTheme) {
+ docElement.setAttribute('zen-default-theme', 'true');
+ } else {
+ docElement.removeAttribute('zen-default-theme');
+ }
+ if (dominantColor) {
+ const primaryColor = this.getAccentColorForUI(dominantColor);
+ docElement.style.setProperty('--zen-primary-color', primaryColor);
+
+ // Should be set to `this.isLegacyVersion` but for some reason it is set to undefined if we open a private window,
+ // so instead get the pref value directly.
+ browser.gZenThemePicker.isLegacyVersion =
+ Services.prefs.getIntPref('zen.theme.gradient-legacy-version', 1) === 0;
+
+ let isDarkMode = isDarkModeWindow;
+ if (!isDefaultTheme && !this.isLegacyVersion) {
+ // Check for the primary color
+ isDarkMode = browser.gZenThemePicker.shouldBeDarkMode(dominantColor);
+ docElement.setAttribute('zen-should-be-dark-mode', isDarkMode);
+ browser.gZenThemePicker.panel.removeAttribute('invalidate-controls');
} else {
- docElement.removeAttribute('zen-default-theme');
- }
- if (dominantColor) {
- const primaryColor = this.getAccentColorForUI(dominantColor);
- docElement.style.setProperty('--zen-primary-color', primaryColor);
-
- // Should be set to `this.isLegacyVersion` but for some reason it is set to undefined if we open a private window,
- // so instead get the pref value directly.
- browser.gZenThemePicker.isLegacyVersion =
- Services.prefs.getIntPref('zen.theme.gradient-legacy-version', 1) === 0;
-
- let isDarkMode = isDarkModeWindow;
- if (!isDefaultTheme && !this.isLegacyVersion) {
- // Check for the primary color
- isDarkMode = browser.gZenThemePicker.shouldBeDarkMode(dominantColor);
- docElement.setAttribute('zen-should-be-dark-mode', isDarkMode);
- browser.gZenThemePicker.panel.removeAttribute('invalidate-controls');
- } else {
- docElement.removeAttribute('zen-should-be-dark-mode');
- if (!this.isLegacyVersion) {
- browser.gZenThemePicker.panel.setAttribute('invalidate-controls', 'true');
- }
+ docElement.removeAttribute('zen-should-be-dark-mode');
+ if (!this.isLegacyVersion) {
+ browser.gZenThemePicker.panel.setAttribute('invalidate-controls', 'true');
}
- // Set `--toolbox-textcolor` to have a contrast with the primary color
- const textColor = this.getToolbarColor(isDarkMode);
- docElement.style.setProperty(
- '--toolbox-textcolor',
- `rgba(${textColor[0]}, ${textColor[1]}, ${textColor[2]}, ${textColor[3]})`
- );
- }
-
- if (!skipUpdate) {
- browser.gZenThemePicker.dots = [];
- browser.gZenThemePicker.recalculateDots(workspaceTheme.gradientColors);
}
- });
- }
-
- fixTheme(theme) {
- // add a primary color if there isn't one
- if (
- !theme.gradientColors.find((color) => color.isPrimary) &&
- theme.gradientColors.length > 0
- ) {
- theme.gradientColors[0].isPrimary = true;
- }
- return theme;
- }
-
- getNativeAccentColor() {
- let accentColor = Services.prefs.getStringPref('zen.theme.accent-color');
- let rgb;
- if (accentColor === 'AccentColor') {
- const rawRgb = window.getComputedStyle(document.getElementById('zen-browser-background'))[
- 'color'
- ];
- rgb = rawRgb.match(/\d+/g).map(Number);
- // Match our theme a bit more, since we can't always expect the OS
- // to give us a color matching our theme scheme
- rgb = this.blendColors(
- rgb,
- this.getToolbarModifiedBaseRaw().slice(0, 3),
- this.isDarkMode ? 80 : 50
+ // Set `--toolbox-textcolor` to have a contrast with the primary color
+ const textColor = this.getToolbarColor(isDarkMode);
+ docElement.style.setProperty(
+ '--toolbox-textcolor',
+ `rgba(${textColor[0]}, ${textColor[1]}, ${textColor[2]}, ${textColor[3]})`
);
- } else {
- rgb = this.hexToRgb(accentColor);
- }
- if (this.isDarkMode) {
- // If the theme is dark, we want to use a lighter color
- return this.blendColors(rgb, [0, 0, 0], 40);
}
- return rgb;
- }
-
- resetCustomColorList() {
- this.customColorList.innerHTML = '';
- }
- removeCustomColor(event) {
- const target = event.target.closest('.zen-theme-picker-custom-list-item');
- const color = target.getAttribute('data-color');
- const dots = this.panel.querySelectorAll('.zen-theme-picker-dot');
- for (const dot of dots) {
- if (dot.style.getPropertyValue('--zen-theme-picker-dot-color') === color) {
- dot.remove();
- break;
- }
+ if (!skipUpdate) {
+ browser.gZenThemePicker.dots = [];
+ browser.gZenThemePicker.recalculateDots(workspaceTheme.gradientColors);
}
- target.remove();
- this.updateCurrentWorkspace();
- }
+ });
+ }
- getPrimaryColor(colors) {
- const primaryColor = colors.find((color) => color.isPrimary);
- if (primaryColor) {
- return primaryColor.c;
- }
- if (colors.length === 0) {
- return undefined;
- }
- // Get the middle color
- return colors[Math.floor(colors.length / 2)].c;
+ fixTheme(theme) {
+ // add a primary color if there isn't one
+ if (!theme.gradientColors.find((color) => color.isPrimary) && theme.gradientColors.length > 0) {
+ theme.gradientColors[0].isPrimary = true;
}
+ return theme;
+ }
- recalculateDots(colors) {
- for (const color of colors) {
- this.createDot(color, true);
- }
+ getNativeAccentColor() {
+ let accentColor = Services.prefs.getStringPref('zen.theme.accent-color');
+ let rgb;
+ if (accentColor === 'AccentColor') {
+ const rawRgb = window.getComputedStyle(document.getElementById('zen-browser-background'))[
+ 'color'
+ ];
+ rgb = rawRgb.match(/\d+/g).map(Number);
+ // Match our theme a bit more, since we can't always expect the OS
+ // to give us a color matching our theme scheme
+ rgb = this.blendColors(
+ rgb,
+ this.getToolbarModifiedBaseRaw().slice(0, 3),
+ this.isDarkMode ? 80 : 50
+ );
+ } else {
+ rgb = this.hexToRgb(accentColor);
}
+ if (this.isDarkMode) {
+ // If the theme is dark, we want to use a lighter color
+ return this.blendColors(rgb, [0, 0, 0], 40);
+ }
+ return rgb;
+ }
- async updateCurrentWorkspace(skipSave = true) {
- this.updated = skipSave;
- const dots = this.panel.querySelectorAll('.zen-theme-picker-dot');
- const colors = Array.from(dots)
- .sort((a, b) => a.getAttribute('data-index') - b.getAttribute('data-index'))
- .map((dot) => {
- const color = dot.style.getPropertyValue('--zen-theme-picker-dot-color');
- const isPrimary = dot.classList.contains('primary');
+ resetCustomColorList() {
+ this.customColorList.innerHTML = '';
+ }
- if (color === 'undefined') {
- return;
- }
- const isCustom = dot.classList.contains('custom');
- const algorithm = this.useAlgo;
- const position =
- dot.getAttribute('data-position') && JSON.parse(dot.getAttribute('data-position'));
- const type = dot.getAttribute('data-type');
- return {
- c: isCustom ? color : color.match(/\d+/g).map(Number),
- isCustom,
- algorithm,
- isPrimary,
- lightness: this.#currentLightness,
- position,
- type,
- };
- });
- const gradient = nsZenThemePicker.getTheme(colors, this.currentOpacity, this.currentTexture);
- let currentWorkspace = await gZenWorkspaces.getActiveWorkspace();
-
- if (!skipSave) {
- await ZenWorkspacesStorage.saveWorkspaceTheme(currentWorkspace.uuid, gradient);
- await gZenWorkspaces._propagateWorkspaceData();
- gZenUIManager.showToast('zen-panel-ui-gradient-generator-saved-message');
- currentWorkspace = await gZenWorkspaces.getActiveWorkspace();
+ removeCustomColor(event) {
+ const target = event.target.closest('.zen-theme-picker-custom-list-item');
+ const color = target.getAttribute('data-color');
+ const dots = this.panel.querySelectorAll('.zen-theme-picker-dot');
+ for (const dot of dots) {
+ if (dot.style.getPropertyValue('--zen-theme-picker-dot-color') === color) {
+ dot.remove();
+ break;
}
-
- await this.onWorkspaceChange(currentWorkspace, skipSave, skipSave ? gradient : null);
}
+ target.remove();
+ this.updateCurrentWorkspace();
+ }
- async handlePanelClose() {
- if (this.updated) {
- await this.updateCurrentWorkspace(false);
- }
- this.uninitThemePicker();
+ getPrimaryColor(colors) {
+ const primaryColor = colors.find((color) => color.isPrimary);
+ if (primaryColor) {
+ return primaryColor.c;
}
+ if (colors.length === 0) {
+ return undefined;
+ }
+ // Get the middle color
+ return colors[Math.floor(colors.length / 2)].c;
+ }
- handlePanelOpen() {
- this.initThemePicker();
- setTimeout(() => {
- this.updateCurrentWorkspace();
- }, 200);
+ recalculateDots(colors) {
+ for (const color of colors) {
+ this.createDot(color, true);
}
+ }
- #interpolateWavePath(progress) {
- const linePath = this.#linePath;
- const sinePath = this.#sinePath;
- const referenceY = 27.3;
- if (this.#sinePoints.length === 0) {
- return progress < 0.5 ? linePath : sinePath;
- }
- if (progress <= 0.001) return linePath;
- if (progress >= 0.999) return sinePath;
- const t = progress;
- let newPathData = '';
- this.#sinePoints.forEach((p) => {
- switch (p.type) {
- case 'M': {
- const interpolatedY = referenceY + (p.y - referenceY) * t;
- newPathData += `M ${p.x} ${interpolatedY} `;
- break;
- }
- case 'C': {
- const y1 = referenceY + (p.y1 - referenceY) * t;
- const y2 = referenceY + (p.y2 - referenceY) * t;
- const y = referenceY + (p.y - referenceY) * t;
- newPathData += `C ${p.x1} ${y1} ${p.x2} ${y2} ${p.x} ${y} `;
- break;
- }
- case 'L':
- newPathData += `L ${p.x} ${p.y} `;
- break;
+ async updateCurrentWorkspace(skipSave = true) {
+ this.updated = skipSave;
+ const dots = this.panel.querySelectorAll('.zen-theme-picker-dot');
+ const colors = Array.from(dots)
+ .sort((a, b) => a.getAttribute('data-index') - b.getAttribute('data-index'))
+ .map((dot) => {
+ const color = dot.style.getPropertyValue('--zen-theme-picker-dot-color');
+ const isPrimary = dot.classList.contains('primary');
+
+ if (color === 'undefined') {
+ return;
}
+ const isCustom = dot.classList.contains('custom');
+ const algorithm = this.useAlgo;
+ const position =
+ dot.getAttribute('data-position') && JSON.parse(dot.getAttribute('data-position'));
+ const type = dot.getAttribute('data-type');
+ return {
+ c: isCustom ? color : color.match(/\d+/g).map(Number),
+ isCustom,
+ algorithm,
+ isPrimary,
+ lightness: this.#currentLightness,
+ position,
+ type,
+ };
});
- return newPathData.trim();
+ const gradient = nsZenThemePicker.getTheme(colors, this.currentOpacity, this.currentTexture);
+ let currentWorkspace = await gZenWorkspaces.getActiveWorkspace();
+
+ if (!skipSave) {
+ await ZenWorkspacesStorage.saveWorkspaceTheme(currentWorkspace.uuid, gradient);
+ await gZenWorkspaces._propagateWorkspaceData();
+ gZenUIManager.showToast('zen-panel-ui-gradient-generator-saved-message');
+ currentWorkspace = await gZenWorkspaces.getActiveWorkspace();
}
- invalidateGradientCache() {
- this.#gradientsCache = {};
- window.dispatchEvent(new Event('ZenGradientCacheChanged'));
+ await this.onWorkspaceChange(currentWorkspace, skipSave, skipSave ? gradient : null);
+ }
+
+ async handlePanelClose() {
+ if (this.updated) {
+ await this.updateCurrentWorkspace(false);
}
+ this.uninitThemePicker();
+ }
- getGradientForWorkspace(workspace) {
- const uuid = workspace.uuid;
- if (this.#gradientsCache[uuid]) {
- return this.#gradientsCache[uuid];
- }
- const previousOpacity = this.currentOpacity;
- const previousLightness = this.#currentLightness;
- const theme = workspace.theme;
- this.currentOpacity = theme.opacity ?? 0.5;
- this.#currentLightness = theme.lightness ?? 50;
- const gradient = this.getGradient(theme.gradientColors);
- const toolbarGradient = this.getGradient(theme.gradientColors, true);
- let dominantColor = this.getMostDominantColor(theme.gradientColors);
- const isDefaultTheme = !dominantColor;
- if (isDefaultTheme) {
- dominantColor = this.getNativeAccentColor();
- }
- let isDarkMode = this.isDarkMode;
- let isExplicitMode = false;
- if (!isDefaultTheme && !this.isLegacyVersion) {
- // Check for the primary color
- isDarkMode = this.shouldBeDarkMode(dominantColor);
- isExplicitMode = true;
+ handlePanelOpen() {
+ this.initThemePicker();
+ setTimeout(() => {
+ this.updateCurrentWorkspace();
+ }, 200);
+ }
+
+ #interpolateWavePath(progress) {
+ const linePath = this.#linePath;
+ const sinePath = this.#sinePath;
+ const referenceY = 27.3;
+ if (this.#sinePoints.length === 0) {
+ return progress < 0.5 ? linePath : sinePath;
+ }
+ if (progress <= 0.001) return linePath;
+ if (progress >= 0.999) return sinePath;
+ const t = progress;
+ let newPathData = '';
+ this.#sinePoints.forEach((p) => {
+ switch (p.type) {
+ case 'M': {
+ const interpolatedY = referenceY + (p.y - referenceY) * t;
+ newPathData += `M ${p.x} ${interpolatedY} `;
+ break;
+ }
+ case 'C': {
+ const y1 = referenceY + (p.y1 - referenceY) * t;
+ const y2 = referenceY + (p.y2 - referenceY) * t;
+ const y = referenceY + (p.y - referenceY) * t;
+ newPathData += `C ${p.x1} ${y1} ${p.x2} ${y2} ${p.x} ${y} `;
+ break;
+ }
+ case 'L':
+ newPathData += `L ${p.x} ${p.y} `;
+ break;
}
- this.#gradientsCache[uuid] = {
- gradient,
- toolbarGradient,
- grain: theme.texture ?? 0,
- isDarkMode,
- isExplicitMode,
- toolbarColor: this.getToolbarColor(isDarkMode),
- primaryColor: this.getAccentColorForUI(dominantColor),
- };
- this.currentOpacity = previousOpacity;
- this.#currentLightness = previousLightness;
+ });
+ return newPathData.trim();
+ }
+
+ invalidateGradientCache() {
+ this.#gradientsCache = {};
+ window.dispatchEvent(new Event('ZenGradientCacheChanged'));
+ }
+
+ getGradientForWorkspace(workspace) {
+ const uuid = workspace.uuid;
+ if (this.#gradientsCache[uuid]) {
return this.#gradientsCache[uuid];
}
+ const previousOpacity = this.currentOpacity;
+ const previousLightness = this.#currentLightness;
+ const theme = workspace.theme;
+ this.currentOpacity = theme.opacity ?? 0.5;
+ this.#currentLightness = theme.lightness ?? 50;
+ const gradient = this.getGradient(theme.gradientColors);
+ const toolbarGradient = this.getGradient(theme.gradientColors, true);
+ let dominantColor = this.getMostDominantColor(theme.gradientColors);
+ const isDefaultTheme = !dominantColor;
+ if (isDefaultTheme) {
+ dominantColor = this.getNativeAccentColor();
+ }
+ let isDarkMode = this.isDarkMode;
+ let isExplicitMode = false;
+ if (!isDefaultTheme && !this.isLegacyVersion) {
+ // Check for the primary color
+ isDarkMode = this.shouldBeDarkMode(dominantColor);
+ isExplicitMode = true;
+ }
+ this.#gradientsCache[uuid] = {
+ gradient,
+ toolbarGradient,
+ grain: theme.texture ?? 0,
+ isDarkMode,
+ isExplicitMode,
+ toolbarColor: this.getToolbarColor(isDarkMode),
+ primaryColor: this.getAccentColorForUI(dominantColor),
+ };
+ this.currentOpacity = previousOpacity;
+ this.#currentLightness = previousLightness;
+ return this.#gradientsCache[uuid];
}
-
- window.nsZenThemePicker = nsZenThemePicker;
}
diff --git a/src/zen/workspaces/ZenWorkspace.mjs b/src/zen/workspaces/ZenWorkspace.mjs
index aa92b13f7b..ce6baa122b 100644
--- a/src/zen/workspaces/ZenWorkspace.mjs
+++ b/src/zen/workspaces/ZenWorkspace.mjs
@@ -1,10 +1,10 @@
// 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 nsZenWorkspace extends MozXULElement {
- static get markup() {
- return `
+
+class nsZenWorkspace extends MozXULElement {
+ static get markup() {
+ return `
@@ -35,230 +35,227 @@
`;
- }
-
- static get inheritedAttributes() {
- return {
- '.zen-workspace-tabs-section': 'zen-workspace-id=id',
- };
- }
-
- constructor() {
- super();
- }
-
- connectedCallback() {
- if (this.delayConnectedCallback() || this._hasConnected) {
- // If we are not ready yet, or if we have already connected, we
- // don't need to do anything.
- return;
- }
-
- this._hasConnected = true;
- this.appendChild(this.constructor.fragment);
-
- this.tabsContainer = this.querySelector('.zen-workspace-normal-tabs-section');
- this.indicator = this.querySelector('.zen-current-workspace-indicator');
- this.pinnedTabsContainer = this.querySelector('.zen-workspace-pinned-tabs-section');
- this.initializeAttributeInheritance();
-
- this.scrollbox = this.querySelector('arrowscrollbox');
- this.scrollbox.smoothScroll = Services.prefs.getBoolPref(
- 'zen.startup.smooth-scroll-in-tabs',
- false
- );
-
- this.scrollbox.addEventListener('wheel', this, true);
- this.scrollbox.addEventListener('underflow', this);
- this.scrollbox.addEventListener('overflow', this);
-
- this.indicator.querySelector('.zen-current-workspace-indicator-name').onRenameFinished =
- this.onIndicatorRenameFinished.bind(this);
+ }
- this.pinnedTabsContainer.scrollbox = this.scrollbox;
+ static get inheritedAttributes() {
+ return {
+ '.zen-workspace-tabs-section': 'zen-workspace-id=id',
+ };
+ }
- this.indicator
- .querySelector('.zen-workspaces-actions')
- .addEventListener('click', this.onActionsCommand.bind(this));
+ constructor() {
+ super();
+ }
- this.indicator
- .querySelector('.zen-current-workspace-indicator-icon')
- .addEventListener('dblclick', (event) => {
- event.stopPropagation();
- gZenWorkspaces.changeWorkspaceIcon();
- });
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this._hasConnected) {
+ // If we are not ready yet, or if we have already connected, we
+ // don't need to do anything.
+ return;
+ }
- this.scrollbox._getScrollableElements = () => {
- const children = [...this.pinnedTabsContainer.children, ...this.tabsContainer.children];
- if (Services.prefs.getBoolPref('zen.view.show-newtab-button-top', false)) {
- // Move the perifery to the first non-pinned tab
- const periphery = this.tabsContainer.querySelector(
- '#tabbrowser-arrowscrollbox-periphery'
+ this._hasConnected = true;
+ this.appendChild(this.constructor.fragment);
+
+ this.tabsContainer = this.querySelector('.zen-workspace-normal-tabs-section');
+ this.indicator = this.querySelector('.zen-current-workspace-indicator');
+ this.pinnedTabsContainer = this.querySelector('.zen-workspace-pinned-tabs-section');
+ this.initializeAttributeInheritance();
+
+ this.scrollbox = this.querySelector('arrowscrollbox');
+ this.scrollbox.smoothScroll = Services.prefs.getBoolPref(
+ 'zen.startup.smooth-scroll-in-tabs',
+ false
+ );
+
+ this.scrollbox.addEventListener('wheel', this, true);
+ this.scrollbox.addEventListener('underflow', this);
+ this.scrollbox.addEventListener('overflow', this);
+
+ this.indicator.querySelector('.zen-current-workspace-indicator-name').onRenameFinished =
+ this.onIndicatorRenameFinished.bind(this);
+
+ this.pinnedTabsContainer.scrollbox = this.scrollbox;
+
+ this.indicator
+ .querySelector('.zen-workspaces-actions')
+ .addEventListener('click', this.onActionsCommand.bind(this));
+
+ this.indicator
+ .querySelector('.zen-current-workspace-indicator-icon')
+ .addEventListener('dblclick', (event) => {
+ event.stopPropagation();
+ gZenWorkspaces.changeWorkspaceIcon();
+ });
+
+ this.scrollbox._getScrollableElements = () => {
+ const children = [...this.pinnedTabsContainer.children, ...this.tabsContainer.children];
+ if (Services.prefs.getBoolPref('zen.view.show-newtab-button-top', false)) {
+ // Move the perifery to the first non-pinned tab
+ const periphery = this.tabsContainer.querySelector('#tabbrowser-arrowscrollbox-periphery');
+ if (periphery) {
+ const firstNonPinnedTabIndex = children.findIndex(
+ (child) => gBrowser.isTab(child) && !child.pinned
);
- if (periphery) {
- const firstNonPinnedTabIndex = children.findIndex(
- (child) => gBrowser.isTab(child) && !child.pinned
- );
- if (firstNonPinnedTabIndex > -1) {
- // Change to new location and remove from the old one on the list
- const peripheryIndex = children.indexOf(periphery);
- if (peripheryIndex > -1) {
- children.splice(peripheryIndex, 1);
- }
- children.splice(firstNonPinnedTabIndex, 0, periphery);
+ if (firstNonPinnedTabIndex > -1) {
+ // Change to new location and remove from the old one on the list
+ const peripheryIndex = children.indexOf(periphery);
+ if (peripheryIndex > -1) {
+ children.splice(peripheryIndex, 1);
}
+ children.splice(firstNonPinnedTabIndex, 0, periphery);
}
}
- return Array.prototype.filter.call(
- children,
- this.scrollbox._canScrollToElement,
- this.scrollbox
+ }
+ return Array.prototype.filter.call(
+ children,
+ this.scrollbox._canScrollToElement,
+ this.scrollbox
+ );
+ };
+
+ this.scrollbox._canScrollToElement = (element) => {
+ if (gBrowser.isTab(element)) {
+ return (
+ !element.hasAttribute('zen-essential') &&
+ !this.hasAttribute('positionpinnedtabs') &&
+ !element.hasAttribute('zen-empty-tab')
);
- };
-
- this.scrollbox._canScrollToElement = (element) => {
- if (gBrowser.isTab(element)) {
- return (
- !element.hasAttribute('zen-essential') &&
- !this.hasAttribute('positionpinnedtabs') &&
- !element.hasAttribute('zen-empty-tab')
- );
- }
- return true;
- };
-
- // Override for performance reasons. This is the size of a single element
- // that can be scrolled when using mouse wheel scrolling. If we don't do
- // this then arrowscrollbox computes this value by calling
- // _getScrollableElements and dividing the box size by that number.
- // However in the tabstrip case we already know the answer to this as,
- // when we're overflowing, it is always the same as the tab min width or
- // height. For tab group labels, the number won't exactly match, but
- // that shouldn't be a problem in practice since the arrowscrollbox
- // stops at element bounds when finishing scrolling.
- try {
- Object.defineProperty(this.scrollbox, 'lineScrollAmount', {
- get: () => 36,
- });
- } catch (e) {
- console.warn('Failed to set lineScrollAmount', e);
}
+ return true;
+ };
+
+ // Override for performance reasons. This is the size of a single element
+ // that can be scrolled when using mouse wheel scrolling. If we don't do
+ // this then arrowscrollbox computes this value by calling
+ // _getScrollableElements and dividing the box size by that number.
+ // However in the tabstrip case we already know the answer to this as,
+ // when we're overflowing, it is always the same as the tab min width or
+ // height. For tab group labels, the number won't exactly match, but
+ // that shouldn't be a problem in practice since the arrowscrollbox
+ // stops at element bounds when finishing scrolling.
+ try {
+ Object.defineProperty(this.scrollbox, 'lineScrollAmount', {
+ get: () => 36,
+ });
+ } catch (e) {
+ console.warn('Failed to set lineScrollAmount', e);
+ }
- // Add them manually since attribute inheritance doesn't work
- // for multiple layers of shadow DOM.
- this.tabsContainer.setAttribute('zen-workspace-id', this.id);
- this.pinnedTabsContainer.setAttribute('zen-workspace-id', this.id);
+ // Add them manually since attribute inheritance doesn't work
+ // for multiple layers of shadow DOM.
+ this.tabsContainer.setAttribute('zen-workspace-id', this.id);
+ this.pinnedTabsContainer.setAttribute('zen-workspace-id', this.id);
- this.#updateOverflow();
+ this.#updateOverflow();
- this.onGradientCacheChanged = this.#onGradientCacheChanged.bind(this);
- window.addEventListener('ZenGradientCacheChanged', this.onGradientCacheChanged);
+ this.onGradientCacheChanged = this.#onGradientCacheChanged.bind(this);
+ window.addEventListener('ZenGradientCacheChanged', this.onGradientCacheChanged);
- this.dispatchEvent(
- new CustomEvent('ZenWorkspaceAttached', {
- bubbles: true,
- composed: true,
- detail: { workspace: this },
- })
- );
- }
-
- disconnectedCallback() {
- window.removeEventListener('ZenGradientCacheChanged', this.onGradientCacheChanged);
- }
+ this.dispatchEvent(
+ new CustomEvent('ZenWorkspaceAttached', {
+ bubbles: true,
+ composed: true,
+ detail: { workspace: this },
+ })
+ );
+ }
- get active() {
- return this.hasAttribute('active');
- }
+ disconnectedCallback() {
+ window.removeEventListener('ZenGradientCacheChanged', this.onGradientCacheChanged);
+ }
- set active(value) {
- if (value) {
- this.setAttribute('active', 'true');
- } else {
- this.removeAttribute('active');
- }
- this.#updateOverflow();
- }
+ get active() {
+ return this.hasAttribute('active');
+ }
- #updateOverflow() {
- if (!this.scrollbox) return;
- if (this.overflows) {
- this.#dispatchEventFromScrollbox('overflow');
- } else {
- this.#dispatchEventFromScrollbox('underflow');
- }
+ set active(value) {
+ if (value) {
+ this.setAttribute('active', 'true');
+ } else {
+ this.removeAttribute('active');
}
+ this.#updateOverflow();
+ }
- #dispatchEventFromScrollbox(type) {
- this.scrollbox.dispatchEvent(new CustomEvent(type, {}));
+ #updateOverflow() {
+ if (!this.scrollbox) return;
+ if (this.overflows) {
+ this.#dispatchEventFromScrollbox('overflow');
+ } else {
+ this.#dispatchEventFromScrollbox('underflow');
}
+ }
- get overflows() {
- return this.scrollbox.overflowing;
- }
+ #dispatchEventFromScrollbox(type) {
+ this.scrollbox.dispatchEvent(new CustomEvent(type, {}));
+ }
- handleEvent(event) {
- if (this.active) {
- gBrowser.tabContainer.handleEvent(event);
- }
- }
+ get overflows() {
+ return this.scrollbox.overflowing;
+ }
- get workspaceUuid() {
- return this.id;
+ handleEvent(event) {
+ if (this.active) {
+ gBrowser.tabContainer.handleEvent(event);
}
+ }
- async onIndicatorRenameFinished(newName) {
- if (newName === '') {
- return;
- }
- let workspaces = (await gZenWorkspaces._workspaces()).workspaces;
- let workspaceData = workspaces.find((workspace) => workspace.uuid === this.workspaceUuid);
- workspaceData.name = newName;
- await gZenWorkspaces.saveWorkspace(workspaceData);
- this.indicator.querySelector('.zen-current-workspace-indicator-name').textContent = newName;
- gZenUIManager.showToast('zen-workspace-renamed-toast');
- }
+ get workspaceUuid() {
+ return this.id;
+ }
- onActionsCommand(event) {
- event.stopPropagation();
- const popup = document.getElementById('zenWorkspaceMoreActions');
- const target = event.target;
- target.setAttribute('open', 'true');
- this.indicator.setAttribute('open', 'true');
- const handlePopupHidden = (event) => {
- if (event.target !== popup) return;
- target.removeAttribute('open');
- this.indicator.removeAttribute('open');
- popup.removeEventListener('popuphidden', handlePopupHidden);
- };
- popup.addEventListener('popuphidden', handlePopupHidden);
- popup.openPopup(event.target, 'after_start');
+ async onIndicatorRenameFinished(newName) {
+ if (newName === '') {
+ return;
}
+ let workspaces = (await gZenWorkspaces._workspaces()).workspaces;
+ let workspaceData = workspaces.find((workspace) => workspace.uuid === this.workspaceUuid);
+ workspaceData.name = newName;
+ await gZenWorkspaces.saveWorkspace(workspaceData);
+ this.indicator.querySelector('.zen-current-workspace-indicator-name').textContent = newName;
+ gZenUIManager.showToast('zen-workspace-renamed-toast');
+ }
- get newTabButton() {
- return this.querySelector('#tabs-newtab-button');
- }
+ onActionsCommand(event) {
+ event.stopPropagation();
+ const popup = document.getElementById('zenWorkspaceMoreActions');
+ const target = event.target;
+ target.setAttribute('open', 'true');
+ this.indicator.setAttribute('open', 'true');
+ const handlePopupHidden = (event) => {
+ if (event.target !== popup) return;
+ target.removeAttribute('open');
+ this.indicator.removeAttribute('open');
+ popup.removeEventListener('popuphidden', handlePopupHidden);
+ };
+ popup.addEventListener('popuphidden', handlePopupHidden);
+ popup.openPopup(event.target, 'after_start');
+ }
- #onGradientCacheChanged() {
- const { isDarkMode, isExplicitMode, toolbarColor, primaryColor } =
- gZenThemePicker.getGradientForWorkspace(
- gZenWorkspaces.getWorkspaceFromId(this.workspaceUuid)
- );
- if (isExplicitMode) {
- this.style.colorScheme = isDarkMode ? 'dark' : 'light';
- } else {
- this.style.colorScheme = '';
- }
- this.style.setProperty('--toolbox-textcolor', `rgba(${toolbarColor.join(',')})`);
- this.style.setProperty('--zen-primary-color', primaryColor);
- }
+ get newTabButton() {
+ return this.querySelector('#tabs-newtab-button');
+ }
- clearThemeStyles() {
+ #onGradientCacheChanged() {
+ const { isDarkMode, isExplicitMode, toolbarColor, primaryColor } =
+ gZenThemePicker.getGradientForWorkspace(
+ gZenWorkspaces.getWorkspaceFromId(this.workspaceUuid)
+ );
+ if (isExplicitMode) {
+ this.style.colorScheme = isDarkMode ? 'dark' : 'light';
+ } else {
this.style.colorScheme = '';
- this.style.removeProperty('--toolbox-textcolor');
- this.style.removeProperty('--zen-primary-color');
}
+ this.style.setProperty('--toolbox-textcolor', `rgba(${toolbarColor.join(',')})`);
+ this.style.setProperty('--zen-primary-color', primaryColor);
}
- customElements.define('zen-workspace', nsZenWorkspace);
+ clearThemeStyles() {
+ this.style.colorScheme = '';
+ this.style.removeProperty('--toolbox-textcolor');
+ this.style.removeProperty('--zen-primary-color');
+ }
}
+
+customElements.define('zen-workspace', nsZenWorkspace);
diff --git a/src/zen/workspaces/ZenWorkspaceCreation.mjs b/src/zen/workspaces/ZenWorkspaceCreation.mjs
index b377ce2492..cbaeed4c2b 100644
--- a/src/zen/workspaces/ZenWorkspaceCreation.mjs
+++ b/src/zen/workspaces/ZenWorkspaceCreation.mjs
@@ -1,29 +1,29 @@
// 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 nsZenWorkspaceCreation extends MozXULElement {
- #wasInCollapsedMode = false;
- promiseInitialized = new Promise((resolve) => {
- this.resolveInitialized = resolve;
- });
+class nsZenWorkspaceCreation extends MozXULElement {
+ #wasInCollapsedMode = false;
- #hiddenElements = [];
-
- static get elementsToDisable() {
- return [
- 'cmd_zenOpenWorkspacePanel',
- 'cmd_zenOpenWorkspaceCreation',
- 'cmd_zenOpenFolderCreation',
- 'cmd_zenToggleSidebar',
- 'cmd_newNavigatorTab',
- 'cmd_newNavigatorTabNoEvent',
- ];
- }
+ promiseInitialized = new Promise((resolve) => {
+ this.resolveInitialized = resolve;
+ });
+
+ #hiddenElements = [];
- static get markup() {
- return `
+ static get elementsToDisable() {
+ return [
+ 'cmd_zenOpenWorkspacePanel',
+ 'cmd_zenOpenWorkspaceCreation',
+ 'cmd_zenOpenFolderCreation',
+ 'cmd_zenToggleSidebar',
+ 'cmd_newNavigatorTab',
+ 'cmd_newNavigatorTabNoEvent',
+ ];
+ }
+
+ static get markup() {
+ return `
`;
- }
+ }
- get workspaceId() {
- return this.getAttribute('workspace-id');
- }
+ get workspaceId() {
+ return this.getAttribute('workspace-id');
+ }
- get previousWorkspaceId() {
- return this.getAttribute('previous-workspace-id');
- }
+ get previousWorkspaceId() {
+ return this.getAttribute('previous-workspace-id');
+ }
- get elementsToAnimate() {
- return [
- this.querySelector('.zen-workspace-creation-title'),
- this.querySelector('.zen-workspace-creation-label').parentElement,
- this.querySelector('.zen-workspace-creation-name-wrapper'),
- this.querySelector('.zen-workspace-creation-profile-wrapper'),
- this.querySelector('.zen-workspace-creation-edit-theme-button'),
- this.createButton.parentNode,
- this.cancelButton,
- ];
- }
+ get elementsToAnimate() {
+ return [
+ this.querySelector('.zen-workspace-creation-title'),
+ this.querySelector('.zen-workspace-creation-label').parentElement,
+ this.querySelector('.zen-workspace-creation-name-wrapper'),
+ this.querySelector('.zen-workspace-creation-profile-wrapper'),
+ this.querySelector('.zen-workspace-creation-edit-theme-button'),
+ this.createButton.parentNode,
+ this.cancelButton,
+ ];
+ }
- connectedCallback() {
- if (this.delayConnectedCallback()) {
- // If we are not ready yet, or if we have already connected, we
- // don't need to do anything.
- return;
- }
+ connectedCallback() {
+ if (this.delayConnectedCallback()) {
+ // If we are not ready yet, or if we have already connected, we
+ // don't need to do anything.
+ return;
+ }
- this.appendChild(this.constructor.fragment);
- this.initializeAttributeInheritance();
+ this.appendChild(this.constructor.fragment);
+ this.initializeAttributeInheritance();
- this.inputName = this.querySelector('.zen-workspace-creation-name');
- this.inputIcon = this.querySelector('.zen-workspace-creation-icon-label');
- this.inputProfile = this.querySelector('.zen-workspace-creation-profile');
- this.createButton = this.querySelector('.zen-workspace-creation-create-button');
- this.cancelButton = this.querySelector('.zen-workspace-creation-cancel-button');
+ this.inputName = this.querySelector('.zen-workspace-creation-name');
+ this.inputIcon = this.querySelector('.zen-workspace-creation-icon-label');
+ this.inputProfile = this.querySelector('.zen-workspace-creation-profile');
+ this.createButton = this.querySelector('.zen-workspace-creation-create-button');
+ this.cancelButton = this.querySelector('.zen-workspace-creation-cancel-button');
- for (const element of this.elementsToAnimate) {
- element.style.opacity = 0;
- }
+ for (const element of this.elementsToAnimate) {
+ element.style.opacity = 0;
+ }
- this.#wasInCollapsedMode =
- document.documentElement.getAttribute('zen-sidebar-expanded') !== 'true';
+ this.#wasInCollapsedMode =
+ document.documentElement.getAttribute('zen-sidebar-expanded') !== 'true';
- gNavToolbox.setAttribute('zen-sidebar-expanded', 'true');
- document.documentElement.setAttribute('zen-sidebar-expanded', 'true');
+ gNavToolbox.setAttribute('zen-sidebar-expanded', 'true');
+ document.documentElement.setAttribute('zen-sidebar-expanded', 'true');
- window.docShell.treeOwner
- .QueryInterface(Ci.nsIInterfaceRequestor)
- .getInterface(Ci.nsIAppWindow)
- .rollupAllPopups();
+ window.docShell.treeOwner
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIAppWindow)
+ .rollupAllPopups();
- this.handleZenWorkspacesChangeBind = this.handleZenWorkspacesChange.bind(this);
+ this.handleZenWorkspacesChangeBind = this.handleZenWorkspacesChange.bind(this);
- for (const element of this.parentElement.children) {
- if (element !== this) {
- element.hidden = true;
- this.#hiddenElements.push(element);
- }
+ for (const element of this.parentElement.children) {
+ if (element !== this) {
+ element.hidden = true;
+ this.#hiddenElements.push(element);
}
+ }
- for (const element of nsZenWorkspaceCreation.elementsToDisable) {
- const el = document.getElementById(element);
- if (el) {
- el.setAttribute('disabled', 'true');
- }
+ for (const element of nsZenWorkspaceCreation.elementsToDisable) {
+ const el = document.getElementById(element);
+ if (el) {
+ el.setAttribute('disabled', 'true');
}
+ }
- this.createButton.addEventListener('command', this.onCreateButtonCommand.bind(this));
- this.cancelButton.addEventListener('command', this.onCancelButtonCommand.bind(this));
-
- this.inputName.addEventListener('input', () => {
- this.createButton.disabled = !this.inputName.value.trim();
- });
+ this.createButton.addEventListener('command', this.onCreateButtonCommand.bind(this));
+ this.cancelButton.addEventListener('command', this.onCancelButtonCommand.bind(this));
- this.inputIcon.addEventListener('command', this.onIconCommand.bind(this));
+ this.inputName.addEventListener('input', () => {
+ this.createButton.disabled = !this.inputName.value.trim();
+ });
- this.profilesPopup = this.querySelector('.zen-workspace-creation-profiles-popup');
+ this.inputIcon.addEventListener('command', this.onIconCommand.bind(this));
- if (gZenWorkspaces.shouldShowContainers) {
- this.inputProfile.addEventListener('command', this.onProfileCommand.bind(this));
- this.profilesPopup.addEventListener('popupshown', this.onProfilePopupShown.bind(this));
- this.profilesPopup.addEventListener('command', this.onProfilePopupCommand.bind(this));
+ this.profilesPopup = this.querySelector('.zen-workspace-creation-profiles-popup');
- this.currentProfile = {
- id: 0,
- name: 'Default',
- };
- } else {
- this.inputProfile.parentNode.hidden = true;
- }
+ if (gZenWorkspaces.shouldShowContainers) {
+ this.inputProfile.addEventListener('command', this.onProfileCommand.bind(this));
+ this.profilesPopup.addEventListener('popupshown', this.onProfilePopupShown.bind(this));
+ this.profilesPopup.addEventListener('command', this.onProfilePopupCommand.bind(this));
- document.getElementById('zen-sidebar-splitter').style.pointerEvents = 'none';
-
- gZenUIManager.motion
- .animate(
- [gBrowser.tabContainer, gURLBar.textbox],
- {
- opacity: [1, 0],
- },
- {
- duration: 0.3,
- type: 'spring',
- bounce: 0,
- }
- )
- .then(() => {
- gBrowser.tabContainer.style.visibility = 'collapse';
- if (gZenVerticalTabsManager._hasSetSingleToolbar) {
- document.getElementById('nav-bar').style.visibility = 'collapse';
- }
- this.style.visibility = 'visible';
- gZenCompactModeManager.getAndApplySidebarWidth();
- this.resolveInitialized();
- gZenUIManager.motion
- .animate(
- this.elementsToAnimate,
- {
- y: [20, 0],
- opacity: [0, 1],
- filter: ['blur(2px)', 'blur(0)'],
- },
- {
- duration: 0.6,
- type: 'spring',
- bounce: 0,
- delay: gZenUIManager.motion.stagger(0.05, { startDelay: 0.2 }),
- }
- )
- .then(() => {
- this.inputName.focus();
- gZenWorkspaces.workspaceElement(this.workspaceId).hidden = false;
- });
- });
+ this.currentProfile = {
+ id: 0,
+ name: 'Default',
+ };
+ } else {
+ this.inputProfile.parentNode.hidden = true;
}
- async onCreateButtonCommand() {
- const workspace = await gZenWorkspaces.getActiveWorkspace();
- workspace.name = this.inputName.value.trim();
- workspace.icon = this.inputIcon.image || this.inputIcon.label || undefined;
- workspace.containerTabId = this.currentProfile;
- await gZenWorkspaces.saveWorkspace(workspace);
+ document.getElementById('zen-sidebar-splitter').style.pointerEvents = 'none';
- await this.#cleanup();
+ gZenUIManager.motion
+ .animate(
+ [gBrowser.tabContainer, gURLBar.textbox],
+ {
+ opacity: [1, 0],
+ },
+ {
+ duration: 0.3,
+ type: 'spring',
+ bounce: 0,
+ }
+ )
+ .then(() => {
+ gBrowser.tabContainer.style.visibility = 'collapse';
+ if (gZenVerticalTabsManager._hasSetSingleToolbar) {
+ document.getElementById('nav-bar').style.visibility = 'collapse';
+ }
+ this.style.visibility = 'visible';
+ gZenCompactModeManager.getAndApplySidebarWidth();
+ this.resolveInitialized();
+ gZenUIManager.motion
+ .animate(
+ this.elementsToAnimate,
+ {
+ y: [20, 0],
+ opacity: [0, 1],
+ filter: ['blur(2px)', 'blur(0)'],
+ },
+ {
+ duration: 0.6,
+ type: 'spring',
+ bounce: 0,
+ delay: gZenUIManager.motion.stagger(0.05, { startDelay: 0.2 }),
+ }
+ )
+ .then(() => {
+ this.inputName.focus();
+ gZenWorkspaces.workspaceElement(this.workspaceId).hidden = false;
+ });
+ });
+ }
- await gZenWorkspaces._organizeWorkspaceStripLocations(workspace, true);
- await gZenWorkspaces.updateTabsContainers();
+ async onCreateButtonCommand() {
+ const workspace = await gZenWorkspaces.getActiveWorkspace();
+ workspace.name = this.inputName.value.trim();
+ workspace.icon = this.inputIcon.image || this.inputIcon.label || undefined;
+ workspace.containerTabId = this.currentProfile;
+ await gZenWorkspaces.saveWorkspace(workspace);
- gBrowser.tabContainer._invalidateCachedTabs();
- }
+ await this.#cleanup();
- async onCancelButtonCommand() {
- await gZenWorkspaces.changeWorkspaceWithID(this.previousWorkspaceId);
- }
+ await gZenWorkspaces._organizeWorkspaceStripLocations(workspace, true);
+ await gZenWorkspaces.updateTabsContainers();
- onIconCommand(event) {
- gZenEmojiPicker
- .open(event.target)
- .then(async (emoji) => {
- const isSvg = emoji && emoji.endsWith('.svg');
- if (isSvg) {
- this.inputIcon.label = '';
- this.inputIcon.image = emoji;
- this.inputIcon.setAttribute('has-svg-icon', 'true');
- } else {
- this.inputIcon.image = '';
- this.inputIcon.label = emoji || '';
- this.inputIcon.removeAttribute('has-svg-icon');
- }
- })
- .catch((error) => {
- console.warn('Error changing workspace icon:', error);
- });
- }
+ gBrowser.tabContainer._invalidateCachedTabs();
+ }
- set currentProfile(profile) {
- this.inputProfile.label = profile.name;
- this._profileId = profile.id;
- }
+ async onCancelButtonCommand() {
+ await gZenWorkspaces.changeWorkspaceWithID(this.previousWorkspaceId);
+ }
- get currentProfile() {
- return this._profileId;
- }
+ onIconCommand(event) {
+ gZenEmojiPicker
+ .open(event.target)
+ .then(async (emoji) => {
+ const isSvg = emoji && emoji.endsWith('.svg');
+ if (isSvg) {
+ this.inputIcon.label = '';
+ this.inputIcon.image = emoji;
+ this.inputIcon.setAttribute('has-svg-icon', 'true');
+ } else {
+ this.inputIcon.image = '';
+ this.inputIcon.label = emoji || '';
+ this.inputIcon.removeAttribute('has-svg-icon');
+ }
+ })
+ .catch((error) => {
+ console.warn('Error changing workspace icon:', error);
+ });
+ }
- onProfileCommand(event) {
- this.profilesPopup.openPopup(event.target, 'after_start');
- }
+ set currentProfile(profile) {
+ this.inputProfile.label = profile.name;
+ this._profileId = profile.id;
+ }
- onProfilePopupShown(event) {
- return window.createUserContextMenu(event, {
- isContextMenu: true,
- showDefaultTab: true,
- });
- }
+ get currentProfile() {
+ return this._profileId;
+ }
- onProfilePopupCommand(event) {
- let userContextId = parseInt(event.target.getAttribute('data-usercontextid'));
- if (isNaN(userContextId)) {
- return;
- }
- this.currentProfile = {
- id: userContextId,
- name: event.target.label,
- };
- }
+ onProfileCommand(event) {
+ this.profilesPopup.openPopup(event.target, 'after_start');
+ }
- finishSetup() {
- gZenWorkspaces.addChangeListeners(this.handleZenWorkspacesChangeBind, { once: true });
- }
+ onProfilePopupShown(event) {
+ return window.createUserContextMenu(event, {
+ isContextMenu: true,
+ showDefaultTab: true,
+ });
+ }
- async handleZenWorkspacesChange() {
- await gZenWorkspaces.removeWorkspace(this.workspaceId);
- await this.#cleanup();
+ onProfilePopupCommand(event) {
+ let userContextId = parseInt(event.target.getAttribute('data-usercontextid'));
+ if (isNaN(userContextId)) {
+ return;
}
+ this.currentProfile = {
+ id: userContextId,
+ name: event.target.label,
+ };
+ }
- async #cleanup() {
- await gZenUIManager.motion.animate(
- this.elementsToAnimate.reverse(),
- {
- y: [0, 20],
- opacity: [1, 0],
- filter: ['blur(0)', 'blur(2px)'],
- },
- {
- duration: 0.4,
- type: 'spring',
- bounce: 0,
- delay: gZenUIManager.motion.stagger(0.05),
- }
- );
-
- document.getElementById('zen-sidebar-splitter').style.pointerEvents = '';
+ finishSetup() {
+ gZenWorkspaces.addChangeListeners(this.handleZenWorkspacesChangeBind, { once: true });
+ }
- gZenWorkspaces.removeChangeListeners(this.handleZenWorkspacesChangeBind);
- for (const element of this.constructor.elementsToDisable) {
- const el = document.getElementById(element);
- if (el) {
- el.removeAttribute('disabled');
- }
- }
+ async handleZenWorkspacesChange() {
+ await gZenWorkspaces.removeWorkspace(this.workspaceId);
+ await this.#cleanup();
+ }
- if (this.#wasInCollapsedMode) {
- gNavToolbox.removeAttribute('zen-sidebar-expanded');
- document.documentElement.removeAttribute('zen-sidebar-expanded');
+ async #cleanup() {
+ await gZenUIManager.motion.animate(
+ this.elementsToAnimate.reverse(),
+ {
+ y: [0, 20],
+ opacity: [1, 0],
+ filter: ['blur(0)', 'blur(2px)'],
+ },
+ {
+ duration: 0.4,
+ type: 'spring',
+ bounce: 0,
+ delay: gZenUIManager.motion.stagger(0.05),
}
+ );
- document.documentElement.removeAttribute('zen-creating-workspace');
+ document.getElementById('zen-sidebar-splitter').style.pointerEvents = '';
- gBrowser.tabContainer.style.visibility = '';
- gBrowser.tabContainer.style.opacity = 0;
- if (gZenVerticalTabsManager._hasSetSingleToolbar) {
- document.getElementById('nav-bar').style.visibility = '';
- gURLBar.textbox.style.opacity = 0;
+ gZenWorkspaces.removeChangeListeners(this.handleZenWorkspacesChangeBind);
+ for (const element of this.constructor.elementsToDisable) {
+ const el = document.getElementById(element);
+ if (el) {
+ el.removeAttribute('disabled');
}
+ }
- this.remove();
- gZenUIManager.updateTabsToolbar();
+ if (this.#wasInCollapsedMode) {
+ gNavToolbox.removeAttribute('zen-sidebar-expanded');
+ document.documentElement.removeAttribute('zen-sidebar-expanded');
+ }
- const workspace = await gZenWorkspaces.getActiveWorkspace();
- await gZenWorkspaces._organizeWorkspaceStripLocations(workspace, true);
- await gZenWorkspaces.updateTabsContainers();
+ document.documentElement.removeAttribute('zen-creating-workspace');
- await gZenUIManager.motion.animate(
- [gBrowser.tabContainer, gURLBar.textbox],
- {
- opacity: [0, 1],
- },
- {
- duration: 0.3,
- type: 'spring',
- bounce: 0,
- }
- );
+ gBrowser.tabContainer.style.visibility = '';
+ gBrowser.tabContainer.style.opacity = 0;
+ if (gZenVerticalTabsManager._hasSetSingleToolbar) {
+ document.getElementById('nav-bar').style.visibility = '';
+ gURLBar.textbox.style.opacity = 0;
+ }
- gBrowser.tabContainer.style.opacity = '';
- if (gZenVerticalTabsManager._hasSetSingleToolbar) {
- gURLBar.textbox.style.opacity = '';
+ this.remove();
+ gZenUIManager.updateTabsToolbar();
+
+ const workspace = await gZenWorkspaces.getActiveWorkspace();
+ await gZenWorkspaces._organizeWorkspaceStripLocations(workspace, true);
+ await gZenWorkspaces.updateTabsContainers();
+
+ await gZenUIManager.motion.animate(
+ [gBrowser.tabContainer, gURLBar.textbox],
+ {
+ opacity: [0, 1],
+ },
+ {
+ duration: 0.3,
+ type: 'spring',
+ bounce: 0,
}
+ );
- for (const element of this.#hiddenElements) {
- element.hidden = false;
- }
+ gBrowser.tabContainer.style.opacity = '';
+ if (gZenVerticalTabsManager._hasSetSingleToolbar) {
+ gURLBar.textbox.style.opacity = '';
+ }
- this.#hiddenElements = [];
+ for (const element of this.#hiddenElements) {
+ element.hidden = false;
}
- }
- customElements.define('zen-workspace-creation', nsZenWorkspaceCreation);
+ this.#hiddenElements = [];
+ }
}
+
+customElements.define('zen-workspace-creation', nsZenWorkspaceCreation);
diff --git a/src/zen/workspaces/ZenWorkspaceIcons.mjs b/src/zen/workspaces/ZenWorkspaceIcons.mjs
index 1d48a5f103..f9186c43f2 100644
--- a/src/zen/workspaces/ZenWorkspaceIcons.mjs
+++ b/src/zen/workspaces/ZenWorkspaceIcons.mjs
@@ -1,210 +1,209 @@
// 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 nsZenWorkspaceIcons extends MozXULElement {
- constructor() {
- super();
- }
-
- connectedCallback() {
- if (this.delayConnectedCallback() || this._hasConnected) {
- return;
- }
- this._hasConnected = true;
- window.addEventListener('ZenWorkspacesUIUpdate', this, true);
+class nsZenWorkspaceIcons extends MozXULElement {
+ constructor() {
+ super();
+ }
- this.initDragAndDrop();
- this.addEventListener('mouseover', (e) => {
- if (this.isReorderMode) {
- return;
- }
- const target = e.target.closest('toolbarbutton[zen-workspace-id]');
- if (target) {
- this.scrollLeft = target.offsetLeft - 10;
- }
- });
+ connectedCallback() {
+ if (this.delayConnectedCallback() || this._hasConnected) {
+ return;
}
- initDragAndDrop() {
- let dragStart = 0;
- let draggedTab = null;
+ this._hasConnected = true;
+ window.addEventListener('ZenWorkspacesUIUpdate', this, true);
- this.addEventListener('mousedown', (e) => {
- const target = e.target.closest('toolbarbutton[zen-workspace-id]');
- if (!target || e.button != 0 || e.ctrlKey || e.shiftKey || e.altKey) {
- return;
- }
+ this.initDragAndDrop();
+ this.addEventListener('mouseover', (e) => {
+ if (this.isReorderMode) {
+ return;
+ }
+ const target = e.target.closest('toolbarbutton[zen-workspace-id]');
+ if (target) {
+ this.scrollLeft = target.offsetLeft - 10;
+ }
+ });
+ }
- const isVertical = document.documentElement.getAttribute('zen-sidebar-expanded') != 'true';
- const clientPos = isVertical ? 'clientY' : 'clientX';
+ initDragAndDrop() {
+ let dragStart = 0;
+ let draggedTab = null;
- this.isReorderMode = false;
- dragStart = e[clientPos];
- draggedTab = target;
- draggedTab.setAttribute('dragged', 'true');
+ this.addEventListener('mousedown', (e) => {
+ const target = e.target.closest('toolbarbutton[zen-workspace-id]');
+ if (!target || e.button != 0 || e.ctrlKey || e.shiftKey || e.altKey) {
+ return;
+ }
- e.stopPropagation();
+ const isVertical = document.documentElement.getAttribute('zen-sidebar-expanded') != 'true';
+ const clientPos = isVertical ? 'clientY' : 'clientX';
- const mouseMoveHandler = (moveEvent) => {
- if (Math.abs(moveEvent[clientPos] - dragStart) > 5) {
- this.isReorderMode = true;
- }
+ this.isReorderMode = false;
+ dragStart = e[clientPos];
+ draggedTab = target;
+ draggedTab.setAttribute('dragged', 'true');
- if (this.isReorderMode) {
- const tabs = [...this.children];
- const mouse = moveEvent[clientPos];
+ e.stopPropagation();
- for (const tab of tabs) {
- if (tab === draggedTab) continue;
- const rect = tab.getBoundingClientRect();
+ const mouseMoveHandler = (moveEvent) => {
+ if (Math.abs(moveEvent[clientPos] - dragStart) > 5) {
+ this.isReorderMode = true;
+ }
+
+ if (this.isReorderMode) {
+ const tabs = [...this.children];
+ const mouse = moveEvent[clientPos];
+
+ for (const tab of tabs) {
+ if (tab === draggedTab) continue;
+ const rect = tab.getBoundingClientRect();
+ if (
+ mouse > rect[isVertical ? 'top' : 'left'] &&
+ mouse < rect[isVertical ? 'bottom' : 'right']
+ ) {
+ const nextSibling = draggedTab.nextSibling;
if (
- mouse > rect[isVertical ? 'top' : 'left'] &&
- mouse < rect[isVertical ? 'bottom' : 'right']
+ mouse <
+ rect[isVertical ? 'top' : 'left'] + rect[isVertical ? 'height' : 'width'] / 2
) {
- const nextSibling = draggedTab.nextSibling;
- if (
- mouse <
- rect[isVertical ? 'top' : 'left'] + rect[isVertical ? 'height' : 'width'] / 2
- ) {
- this.insertBefore(draggedTab, tab);
- } else {
- this.insertBefore(draggedTab, tab.nextSibling);
- }
- if (nextSibling !== draggedTab.nextSibling) {
- Services.zen.playHapticFeedback();
- }
+ this.insertBefore(draggedTab, tab);
+ } else {
+ this.insertBefore(draggedTab, tab.nextSibling);
+ }
+ if (nextSibling !== draggedTab.nextSibling) {
+ Services.zen.playHapticFeedback();
}
}
}
- };
+ }
+ };
- const mouseUpHandler = () => {
- document.removeEventListener('mousemove', mouseMoveHandler);
- document.removeEventListener('mouseup', mouseUpHandler);
+ const mouseUpHandler = () => {
+ document.removeEventListener('mousemove', mouseMoveHandler);
+ document.removeEventListener('mouseup', mouseUpHandler);
- draggedTab.removeAttribute('dragged');
+ draggedTab.removeAttribute('dragged');
- this.reorderWorkspaceToIndex(draggedTab, Array.from(this.children).indexOf(draggedTab));
+ this.reorderWorkspaceToIndex(draggedTab, Array.from(this.children).indexOf(draggedTab));
- draggedTab = null;
- this.isReorderMode = false;
- };
+ draggedTab = null;
+ this.isReorderMode = false;
+ };
- document.addEventListener('mousemove', mouseMoveHandler);
- document.addEventListener('mouseup', mouseUpHandler);
- });
- }
+ document.addEventListener('mousemove', mouseMoveHandler);
+ document.addEventListener('mouseup', mouseUpHandler);
+ });
+ }
- #createWorkspaceIcon(workspace) {
- const button = document.createXULElement('toolbarbutton');
- button.setAttribute('class', 'subviewbutton toolbarbutton-1');
- button.setAttribute('tooltiptext', workspace.name);
- button.setAttribute('zen-workspace-id', workspace.uuid);
- button.setAttribute('context', 'zenWorkspaceMoreActions');
- const icon = document.createXULElement('label');
- icon.setAttribute('class', 'zen-workspace-icon');
- const isSvgIcon = workspace.icon && workspace.icon.endsWith('.svg');
- if (gZenWorkspaces.workspaceHasIcon(workspace)) {
- if (isSvgIcon) {
- const image = document.createElement('img');
- image.src = workspace.icon;
- image.classList.add('zen-workspace-icon');
- button.appendChild(image);
- } else {
- icon.textContent = workspace.icon;
- }
+ #createWorkspaceIcon(workspace) {
+ const button = document.createXULElement('toolbarbutton');
+ button.setAttribute('class', 'subviewbutton toolbarbutton-1');
+ button.setAttribute('tooltiptext', workspace.name);
+ button.setAttribute('zen-workspace-id', workspace.uuid);
+ button.setAttribute('context', 'zenWorkspaceMoreActions');
+ const icon = document.createXULElement('label');
+ icon.setAttribute('class', 'zen-workspace-icon');
+ const isSvgIcon = workspace.icon && workspace.icon.endsWith('.svg');
+ if (gZenWorkspaces.workspaceHasIcon(workspace)) {
+ if (isSvgIcon) {
+ const image = document.createElement('img');
+ image.src = workspace.icon;
+ image.classList.add('zen-workspace-icon');
+ button.appendChild(image);
} else {
- icon.setAttribute('no-icon', true);
- }
- if (!isSvgIcon) {
- button.appendChild(icon);
+ icon.textContent = workspace.icon;
}
- button.addEventListener('command', this);
- return button;
+ } else {
+ icon.setAttribute('no-icon', true);
}
-
- async #updateIcons() {
- const workspaces = await gZenWorkspaces._workspaces();
- this.innerHTML = '';
- for (const workspace of workspaces.workspaces) {
- const button = this.#createWorkspaceIcon(workspace);
- this.appendChild(button);
- }
- if (workspaces.workspaces.length <= 1) {
- this.setAttribute('dont-show', 'true');
- } else {
- this.removeAttribute('dont-show');
- }
- gZenWorkspaces.onWindowResize();
+ if (!isSvgIcon) {
+ button.appendChild(icon);
}
+ button.addEventListener('command', this);
+ return button;
+ }
- on_command(event) {
- const button = event.target;
- const uuid = button.getAttribute('zen-workspace-id');
- if (uuid) {
- gZenWorkspaces.changeWorkspaceWithID(uuid);
- }
+ async #updateIcons() {
+ const workspaces = await gZenWorkspaces._workspaces();
+ this.innerHTML = '';
+ for (const workspace of workspaces.workspaces) {
+ const button = this.#createWorkspaceIcon(workspace);
+ this.appendChild(button);
}
-
- async on_ZenWorkspacesUIUpdate(event) {
- await this.#updateIcons();
- this.activeIndex = event.detail.activeIndex;
+ if (workspaces.workspaces.length <= 1) {
+ this.setAttribute('dont-show', 'true');
+ } else {
+ this.removeAttribute('dont-show');
}
+ gZenWorkspaces.onWindowResize();
+ }
- set activeIndex(uuid) {
- const buttons = this.querySelectorAll('toolbarbutton');
- if (!buttons.length) {
- return;
- }
- let i = 0;
- let selected = -1;
- for (const button of buttons) {
- if (button.getAttribute('zen-workspace-id') == uuid) {
- selected = i;
- } else {
- button.removeAttribute('active');
- }
- i++;
- }
- buttons[selected].setAttribute('active', true);
- this.scrollLeft = buttons[selected].offsetLeft - 10;
- this.setAttribute('selected', selected);
+ on_command(event) {
+ const button = event.target;
+ const uuid = button.getAttribute('zen-workspace-id');
+ if (uuid) {
+ gZenWorkspaces.changeWorkspaceWithID(uuid);
}
+ }
- get activeIndex() {
- const selected = this.getAttribute('selected');
- const buttons = this.querySelectorAll('toolbarbutton');
- let i = 0;
- for (const button of buttons) {
- if (i == selected) {
- return button.getAttribute('zen-workspace-id');
- }
- i++;
- }
- return null;
- }
+ async on_ZenWorkspacesUIUpdate(event) {
+ await this.#updateIcons();
+ this.activeIndex = event.detail.activeIndex;
+ }
- get isReorderMode() {
- return this.hasAttribute('reorder-mode');
+ set activeIndex(uuid) {
+ const buttons = this.querySelectorAll('toolbarbutton');
+ if (!buttons.length) {
+ return;
}
-
- set isReorderMode(value) {
- if (value) {
- this.setAttribute('reorder-mode', 'true');
+ let i = 0;
+ let selected = -1;
+ for (const button of buttons) {
+ if (button.getAttribute('zen-workspace-id') == uuid) {
+ selected = i;
} else {
- this.removeAttribute('reorder-mode');
- this.style.removeProperty('--zen-workspace-icon-width');
- this.style.removeProperty('--zen-workspace-icon-height');
+ button.removeAttribute('active');
}
+ i++;
}
+ buttons[selected].setAttribute('active', true);
+ this.scrollLeft = buttons[selected].offsetLeft - 10;
+ this.setAttribute('selected', selected);
+ }
- reorderWorkspaceToIndex(draggedTab, index) {
- const workspaceId = draggedTab.getAttribute('zen-workspace-id');
- gZenWorkspaces.reorderWorkspace(workspaceId, index);
+ get activeIndex() {
+ const selected = this.getAttribute('selected');
+ const buttons = this.querySelectorAll('toolbarbutton');
+ let i = 0;
+ for (const button of buttons) {
+ if (i == selected) {
+ return button.getAttribute('zen-workspace-id');
+ }
+ i++;
}
+ return null;
}
- customElements.define('zen-workspace-icons', nsZenWorkspaceIcons);
+ get isReorderMode() {
+ return this.hasAttribute('reorder-mode');
+ }
+
+ set isReorderMode(value) {
+ if (value) {
+ this.setAttribute('reorder-mode', 'true');
+ } else {
+ this.removeAttribute('reorder-mode');
+ this.style.removeProperty('--zen-workspace-icon-width');
+ this.style.removeProperty('--zen-workspace-icon-height');
+ }
+ }
+
+ reorderWorkspaceToIndex(draggedTab, index) {
+ const workspaceId = draggedTab.getAttribute('zen-workspace-id');
+ gZenWorkspaces.reorderWorkspace(workspaceId, index);
+ }
}
+
+customElements.define('zen-workspace-icons', nsZenWorkspaceIcons);
diff --git a/src/zen/workspaces/ZenWorkspaces.mjs b/src/zen/workspaces/ZenWorkspaces.mjs
index c409d72748..cb08f58d00 100644
--- a/src/zen/workspaces/ZenWorkspaces.mjs
+++ b/src/zen/workspaces/ZenWorkspaces.mjs
@@ -2,7 +2,10 @@
// 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/.
-var gZenWorkspaces = new (class extends nsZenMultiWindowFeature {
+import { nsZenMultiWindowFeature } from 'chrome://browser/content/zen-components/ZenCommonUtils.mjs';
+import { nsZenThemePicker } from 'chrome://browser/content/zen-components/ZenGradientGenerator.mjs';
+
+class nsZenWorkspaces extends nsZenMultiWindowFeature {
/**
* Stores workspace IDs and their last selected tabs.
*/
@@ -2536,7 +2539,7 @@ var gZenWorkspaces = new (class extends nsZenMultiWindowFeature {
// <= 2 because we have the empty tab and the new tab button
const shouldHideSeparator = fromTabSelection
? pinnedContainer.hasAttribute('hide-separator')
- : pinnedContainer.children.length === 1 || !visibleTabsFound();
+ : !visibleTabsFound();
if (shouldHideSeparator) {
pinnedContainer.setAttribute('hide-separator', 'true');
} else {
@@ -3201,4 +3204,6 @@ var gZenWorkspaces = new (class extends nsZenMultiWindowFeature {
document.getElementById('cmd_closeWindow').doCommand();
}
}
-})();
+}
+
+window.gZenWorkspaces = new nsZenWorkspaces();
diff --git a/src/zen/workspaces/ZenWorkspacesStorage.mjs b/src/zen/workspaces/ZenWorkspacesStorage.mjs
index abfe72dc17..43d183d39f 100644
--- a/src/zen/workspaces/ZenWorkspacesStorage.mjs
+++ b/src/zen/workspaces/ZenWorkspacesStorage.mjs
@@ -2,7 +2,7 @@
// 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/.
-var ZenWorkspacesStorage = {
+window.ZenWorkspacesStorage = {
lazy: {},
async init() {
@@ -426,7 +426,7 @@ var ZenWorkspacesStorage = {
};
// Integration of workspace-specific bookmarks into Places
-var ZenWorkspaceBookmarksStorage = {
+window.ZenWorkspaceBookmarksStorage = {
async init() {
await this._ensureTable();
},
diff --git a/src/zen/workspaces/jar.inc.mn b/src/zen/workspaces/jar.inc.mn
new file mode 100644
index 0000000000..1e4fd273f3
--- /dev/null
+++ b/src/zen/workspaces/jar.inc.mn
@@ -0,0 +1,13 @@
+# 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/.
+
+ content/browser/zen-components/ZenWorkspaceIcons.mjs (../../zen/workspaces/ZenWorkspaceIcons.mjs)
+ content/browser/zen-components/ZenWorkspace.mjs (../../zen/workspaces/ZenWorkspace.mjs)
+ content/browser/zen-components/ZenWorkspaces.mjs (../../zen/workspaces/ZenWorkspaces.mjs)
+ content/browser/zen-components/ZenWorkspaceCreation.mjs (../../zen/workspaces/ZenWorkspaceCreation.mjs)
+ content/browser/zen-components/ZenWorkspacesStorage.mjs (../../zen/workspaces/ZenWorkspacesStorage.mjs)
+ content/browser/zen-components/ZenWorkspacesSync.mjs (../../zen/workspaces/ZenWorkspacesSync.mjs)
+ content/browser/zen-components/ZenGradientGenerator.mjs (../../zen/workspaces/ZenGradientGenerator.mjs)
+* content/browser/zen-styles/zen-workspaces.css (../../zen/workspaces/zen-workspaces.css)
+ content/browser/zen-styles/zen-gradient-generator.css (../../zen/workspaces/zen-gradient-generator.css)
\ No newline at end of file
diff --git a/src/zen/zen.globals.js b/src/zen/zen.globals.js
index 93f2484d1c..197089441a 100644
--- a/src/zen/zen.globals.js
+++ b/src/zen/zen.globals.js
@@ -3,10 +3,6 @@
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
export default [
- 'nsZenMultiWindowFeature',
- 'nsZenDOMOperatedFeature',
- 'nsZenPreloadedFeature',
-
'nsZenSiteDataPanel',
'ZenThemeModifier',
@@ -27,6 +23,8 @@ export default [
'ZenWorkspacesStorage',
'ZenWorkspaceBookmarksStorage',
+ 'ZEN_KEYSET_ID',
+
'gZenPinnedTabManager',
'ZenPinnedTabsStorage',
@@ -36,7 +34,6 @@ export default [
'gZenMediaController',
'gZenGlanceManager',
- 'nsZenThemePicker',
'gZenThemePicker',
'gZenViewSplitter',
diff --git a/surfer.json b/surfer.json
index e8f0205d8a..7f1885ec2e 100644
--- a/surfer.json
+++ b/surfer.json
@@ -5,8 +5,8 @@
"binaryName": "zen",
"version": {
"product": "firefox",
- "version": "145.0.1",
- "candidate": "145.0.1"
+ "version": "145.0.2",
+ "candidate": "145.0.2"
},
"buildOptions": {
"generateBranding": true
@@ -19,7 +19,7 @@
"brandShortName": "Zen",
"brandFullName": "Zen Browser",
"release": {
- "displayVersion": "1.17.8b",
+ "displayVersion": "1.17.10b",
"github": {
"repo": "zen-browser/desktop"
},
@@ -53,4 +53,4 @@
"licenseType": "MPL-2.0"
},
"updateHostname": "updates.zen-browser.app"
-}
\ No newline at end of file
+}