{ class ZenGlanceManager extends ZenDOMOperatedFeature { _animating = false; _lazyPref = {}; #glances = new Map(); #currentGlanceID = null; init() { window.addEventListener('keydown', this.onKeyDown.bind(this)); window.addEventListener('TabClose', this.onTabClose.bind(this)); XPCOMUtils.defineLazyPreferenceGetter( this._lazyPref, 'SHOULD_OPEN_EXTERNAL_TABS_IN_GLANCE', 'zen.glance.open-essential-external-links', false ); ChromeUtils.defineLazyGetter(this, 'sidebarButtons', () => document.getElementById('zen-glance-sidebar-container')); document.getElementById('tabbrowser-tabpanels').addEventListener('click', this.onOverlayClick.bind(this)); Services.obs.addObserver(this, 'quit-application-requested'); } get #currentBrowser() { return this.#glances.get(this.#currentGlanceID)?.browser; } get #currentTab() { return this.#glances.get(this.#currentGlanceID)?.tab; } get #currentParentTab() { return this.#glances.get(this.#currentGlanceID)?.parentTab; } onKeyDown(event) { if (event.key === 'Escape' && this.#currentGlanceID) { event.preventDefault(); event.stopPropagation(); this.closeGlance({ onTabClose: true }); } } onOverlayClick(event) { if (event.target === this.overlay && event.originalTarget !== this.contentWrapper) { this.closeGlance({ onTabClose: true }); } } observe(subject, topic) { switch (topic) { case 'quit-application-requested': this.onUnload(); break; } } onUnload() { // clear everything for (let [id, glance] of this.#glances) { gBrowser.removeTab(glance.tab, { animate: false }); } } getTabPosition(tab) { return Math.max(gBrowser._numVisiblePinTabs, tab._tPos) + 1; } createBrowserElement(url, currentTab, existingTab = null) { const newTabOptions = { userContextId: currentTab.getAttribute('usercontextid') || '', skipBackgroundNotify: true, insertTab: true, skipLoad: false, index: this.getTabPosition(currentTab), }; currentTab._selected = true; const newUUID = gZenUIManager.generateUuidv4(); const newTab = existingTab ?? gBrowser.addTrustedTab(Services.io.newURI(url).spec, newTabOptions); if (currentTab.hasAttribute('zenDefaultUserContextId')) { newTab.setAttribute('zenDefaultUserContextId', true); } currentTab.querySelector('.tab-content').appendChild(newTab); newTab.setAttribute('zen-glance-tab', true); newTab.setAttribute('glance-id', newUUID); currentTab.setAttribute('glance-id', newUUID); this.#glances.set(newUUID, { tab: newTab, parentTab: currentTab, browser: newTab.linkedBrowser, }); this.#currentGlanceID = newUUID; return this.#currentBrowser; } fillOverlay(browser) { this.overlay = browser.closest('.browserSidebarContainer'); this.browserWrapper = browser.closest('.browserContainer'); this.contentWrapper = browser.closest('.browserStack'); } showSidebarButtons(animate = false) { if (this.sidebarButtons.hasAttribute('hidden') && animate) { gZenUIManager.motion.animate( this.sidebarButtons.querySelectorAll('toolbarbutton'), { x: [-50, 0], opacity: [0, 1] }, { delay: gZenUIManager.motion.stagger(0.2) } ); } this.sidebarButtons.removeAttribute('hidden'); } hideSidebarButtons() { this.sidebarButtons.setAttribute('hidden', true); } openGlance(data, existingTab = null, ownerTab = null) { if (this.#currentBrowser) { return; } const initialX = data.x; const initialY = data.y; const initialWidth = data.width; const initialHeight = data.height; this.browserWrapper?.removeAttribute('animate'); this.browserWrapper?.removeAttribute('animate-end'); this.browserWrapper?.removeAttribute('animate-full'); this.browserWrapper?.removeAttribute('has-finished-animation'); this.overlay?.removeAttribute('post-fade-out'); const currentTab = ownerTab ?? gBrowser.selectedTab; this.animatingOpen = true; this._animating = true; const browserElement = this.createBrowserElement(data.url, currentTab, existingTab); this.fillOverlay(browserElement); this.showSidebarButtons(true); this.overlay.classList.add('zen-glance-overlay'); this.browserWrapper.removeAttribute('animate-end'); window.requestAnimationFrame(() => { this.quickOpenGlance(); gZenUIManager.motion .animate( this.#currentParentTab.linkedBrowser.closest('.browserSidebarContainer'), { scale: 0.98, backdropFilter: 'blur(5px)', opacity: 0.6, }, { duration: 0.4, type: 'spring', bounce: 0.2, } ); this.overlay.removeAttribute('fade-out'); this.browserWrapper.setAttribute('animate', true); const top = initialY + initialHeight / 2; const left = initialX + initialWidth / 2; this.browserWrapper.style.top = `${top}px`; this.browserWrapper.style.left = `${left}px`; this.browserWrapper.style.width = `${initialWidth}px`; this.browserWrapper.style.height = `${initialHeight}px`; this.browserWrapper.style.opacity = 0.8; 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, }; this.browserWrapper.style.transform = 'translate(-50%, -50%)'; this.overlay.style.overflow = 'visible'; gZenUIManager.motion .animate( this.browserWrapper, { top: '50%', left: '50%', width: '85%', height: '100%', opacity: 1, }, { duration: 0.4, type: 'spring', bounce: 0.2, } ) .then(() => { this.overlay.style.removeProperty('overflow'); this.browserWrapper.removeAttribute('animate'); this.browserWrapper.setAttribute('animate-end', true); this.browserWrapper.setAttribute('has-finished-animation', true); this._animating = false; this.animatingOpen = false; }); }); } closeGlance({ noAnimation = false, onTabClose = false } = {}) { if (this._animating || !this.#currentBrowser || this.animatingOpen || this._duringOpening) { return; } this.browserWrapper.removeAttribute('has-finished-animation'); if (noAnimation) { this.#currentParentTab.linkedBrowser.closest('.browserSidebarContainer').removeAttribute('style'); this.quickCloseGlance({ closeCurrentTab: false }); return; } this._animating = true; gBrowser._insertTabAtIndex(this.#currentTab, { index: this.getTabPosition(this.#currentParentTab), }); let quikcCloseZen = false; if (onTabClose) { // Create new tab if no more ex if (gBrowser.tabs.length === 1) { BrowserCommands.openTab(); return; } } // do NOT touch here, I don't know what it does, but it works... this.#currentTab.style.display = 'none'; this.overlay.setAttribute('fade-out', true); this.overlay.style.pointerEvents = 'none'; this.quickCloseGlance({ justAnimateParent: true, clearID: false }); const originalPosition = this.#glances.get(this.#currentGlanceID).originalPosition; this.#currentParentTab.linkedBrowser.closest('.browserSidebarContainer').removeAttribute('style'); gZenUIManager.motion .animate( this.browserWrapper, { ...originalPosition, opacity: 0, }, { type: 'spring', bounce: 0, duration: 0.7 } ) .then(() => { this.browserWrapper.removeAttribute('animate'); this.browserWrapper.removeAttribute('animate-end'); if (!this.#currentParentTab) { return; } if (!onTabClose || quikcCloseZen) { this.quickCloseGlance({ clearID: false }); } this.overlay.removeAttribute('fade-out'); this.browserWrapper.removeAttribute('animate'); this.lastCurrentTab = this.#currentTab; this.overlay.classList.remove('zen-glance-overlay'); gBrowser._getSwitcher().setTabStateNoAction(this.lastCurrentTab, gBrowser.AsyncTabSwitcher.STATE_UNLOADED); if (!onTabClose) { this.#currentParentTab._visuallySelected = false; } // reset everything const prevOverlay = this.overlay; this.browserWrapper = null; this.overlay = null; this.contentWrapper = null; this.lastCurrentTab.removeAttribute('zen-glance-tab'); this.lastCurrentTab._closingGlance = true; gBrowser.tabContainer._invalidateCachedTabs(); gBrowser.selectedTab = this.#currentParentTab; gBrowser.removeTab(this.lastCurrentTab, { animate: false }); this.#currentBrowser.remove(); setTimeout(() => { prevOverlay.remove(); // Just to be sure }, 0); this.#currentParentTab.removeAttribute('glance-id'); this.#glances.delete(this.#currentGlanceID); this.#currentGlanceID = null; this.lastCurrentTab = null; this._duringOpening = false; this._animating = false; }); } quickOpenGlance() { if (!this.#currentBrowser || this._duringOpening) { return; } this._duringOpening = true; this.showSidebarButtons(); gBrowser.selectedTab = this.#currentTab; const parentBrowserContainer = this.#currentParentTab.linkedBrowser.closest('.browserSidebarContainer'); parentBrowserContainer.classList.add('zen-glance-background'); parentBrowserContainer.classList.remove('zen-glance-overlay'); parentBrowserContainer.classList.add('deck-selected'); 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; setTimeout(() => { // just to make sure parentBrowserContainer.classList.add('deck-selected'); this.#currentParentTab._visuallySelected = true; }, 0); this.overlay.classList.add('deck-selected'); this.overlay.classList.add('zen-glance-overlay'); this._duringOpening = false; } quickCloseGlance({ closeCurrentTab = true, closeParentTab = true, justAnimateParent = false, clearID = true } = {}) { const parentHasBrowser = !!this.#currentParentTab.linkedBrowser; this.hideSidebarButtons(); if (!justAnimateParent) { if (parentHasBrowser) { if (closeParentTab) { this.#currentParentTab.linkedBrowser.closest('.browserSidebarContainer').classList.remove('deck-selected'); } this.#currentParentTab.linkedBrowser.zenModeActive = false; } this.#currentBrowser.zenModeActive = false; if (closeParentTab && parentHasBrowser) { this.#currentParentTab.linkedBrowser.docShellIsActive = false; } if (closeCurrentTab) { this.#currentBrowser.docShellIsActive = false; this.overlay.classList.remove('deck-selected'); this.#currentTab._selected = false; } if (!this.#currentParentTab._visuallySelected && closeParentTab) { this.#currentParentTab._visuallySelected = false; } this.#currentBrowser.removeAttribute('zen-glance-selected'); this.overlay.classList.remove('zen-glance-overlay'); } if (parentHasBrowser) { this.#currentParentTab.linkedBrowser.closest('.browserSidebarContainer').classList.remove('zen-glance-background'); } if (clearID) { this.#currentGlanceID = null; } } // note: must be async to avoid timing issues onLocationChange(browser) { const tab = gBrowser.getTabForBrowser(browser); if (this.animatingFullOpen) { return; } if (this._duringOpening || !tab.hasAttribute('glance-id')) { if (this.#currentGlanceID && !this._duringOpening) { this.quickCloseGlance(); } return; } if (this.#currentGlanceID && this.#currentGlanceID !== tab.getAttribute('glance-id')) { this.quickCloseGlance(); } this.#currentGlanceID = tab.getAttribute('glance-id'); if (gBrowser.selectedTab === this.#currentParentTab && this.#currentBrowser) { const curTab = this.#currentTab; setTimeout(() => { gBrowser.selectedTab = curTab; }, 0); } else if (gBrowser.selectedTab === this.#currentTab && this.#currentParentTab) { setTimeout(this.quickOpenGlance.bind(this), 0); } } onTabClose(event) { if (event.target === this.#currentParentTab) { this.closeGlance({ onTabClose: true }); } } manageTabClose(tab) { if (tab.hasAttribute('glance-id')) { const oldGlanceID = this.#currentGlanceID; const newGlanceID = tab.getAttribute('glance-id'); this.#currentGlanceID = newGlanceID; const isDifferent = newGlanceID !== oldGlanceID; if (this._ignoreClose) { this._ignoreClose = false; return false; } this._ignoreClose = true; this.closeGlance({ noAnimation: isDifferent, onTabClose: true }); if (isDifferent) { this.#currentGlanceID = oldGlanceID; } // only keep continueing tab close if we are not on the currently selected tab return !isDifferent; } return false; } tabDomainsDiffer(tab1, url2) { try { if (!tab1) { return true; } let url1 = tab1.linkedBrowser.currentURI.spec; if (url1.startsWith('about:')) { return true; } return Services.io.newURI(url1).host !== url2.host; } catch (e) { return true; } } shouldOpenTabInGlance(tab, uri) { let owner = tab.owner; return ( owner && owner.getAttribute('zen-essential') === 'true' && this._lazyPref.SHOULD_OPEN_EXTERNAL_TABS_IN_GLANCE && owner.linkedBrowser?.docShellIsActive && owner.linkedBrowser?.browsingContext?.isAppTab && this.tabDomainsDiffer(owner, uri) && Services.prefs.getBoolPref('zen.glance.enabled', true) ); } onTabOpen(browser, uri) { let tab = gBrowser.getTabForBrowser(browser); if (!tab) { return; } try { if (this.shouldOpenTabInGlance(tab, uri)) { this.openGlance({ url: undefined, x: 0, y: 0, width: 0, height: 0 }, tab, tab.owner); } } catch (e) { console.error(e); } } fullyOpenGlance() { this.animatingFullOpen = true; gBrowser._insertTabAtIndex(this.#currentTab, { index: this.getTabPosition(this.#currentTab), }); this.#currentParentTab._visuallySelected = false; this.browserWrapper.removeAttribute('style'); this.browserWrapper.removeAttribute('has-finished-animation'); this.browserWrapper.setAttribute('animate-full', true); this.#currentTab.removeAttribute('zen-glance-tab'); 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.hideSidebarButtons(); gZenUIManager.motion .animate( this.browserWrapper, { width: ['85%', '100%'], height: ['100%', '100%'], }, { duration: 0.4, type: 'spring', } ) .then(() => { this.browserWrapper.removeAttribute('animate-full'); this.overlay.classList.remove('zen-glance-overlay'); this.browserWrapper.removeAttribute('style'); this.animatingFullOpen = false; this.closeGlance({ noAnimation: true }); this.#glances.delete(this.#currentGlanceID); }); } openGlanceForBookmark(event) { const activationMethod = Services.prefs.getStringPref('zen.glance.activation-method', 'ctrl'); if (activationMethod === 'ctrl' && !event.ctrlKey) { return; } else if (activationMethod === 'alt' && !event.altKey) { return; } else if (activationMethod === 'shift' && !event.shiftKey) { return; } else if (activationMethod === 'meta' && !event.metaKey) { return; } else if (activationMethod === 'mantain' || typeof activationMethod === 'undefined') { return; } event.preventDefault(); event.stopPropagation(); const rect = event.target.getBoundingClientRect(); const data = { url: event.target._placesNode.uri, x: rect.left, y: rect.top, width: rect.width, height: rect.height, }; this.openGlance(data); return false; } getFocusedTab(aDir) { return aDir< 0 ? this.#currentParentTab : this.#currentTab; } } window.gZenGlanceManager = new ZenGlanceManager(); function registerWindowActors() { if (Services.prefs.getBoolPref('zen.glance.enabled', true)) { gZenActorsManager.addJSWindowActor('ZenGlance', { parent: { esModuleURI: 'chrome://browser/content/zen-components/actors/ZenGlanceParent.sys.mjs', }, child: { esModuleURI: 'chrome://browser/content/zen-components/actors/ZenGlanceChild.sys.mjs', events: { DOMContentLoaded: {}, }, }, }); } } registerWindowActors(); }