mirror of
https://github.com/zen-browser/desktop.git
synced 2025-10-06 18:06:35 +00:00
562 lines
18 KiB
JavaScript
562 lines
18 KiB
JavaScript
{
|
|
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();
|
|
}
|