Files
tubestation/waterfox/browser/components/sidebar/common/Window.js
2025-11-06 14:13:52 +00:00

304 lines
8.8 KiB
JavaScript

/*
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
'use strict';
import EventListenerManager from '/extlib/EventListenerManager.js';
import {
log as internalLogger,
dumpTab,
configs
} from './common.js';
import * as TabsStore from './tabs-store.js';
function log(...args) {
internalLogger('common/Window', ...args);
}
export default class Window {
constructor(windowId, tabGroups) {
const alreadyTracked = TabsStore.windows.get(windowId);
if (alreadyTracked)
return alreadyTracked;
log(`window ${windowId} is newly tracked`);
this.id = windowId;
this.tabs = new Map();
this.tabGroups = new Map();
if (tabGroups) {
this.initTabGroups(tabGroups);
}
this.order = [];
this.containerElement = null;
this.containerClassList = null;
this.pinnedContainerElement = null;
this.internalMovingTabs = new Map();
this.alreadyMovedTabs = new Map();
this.internalClosingTabs = new Set();
this.keepDescendantsTabs = new Set();
this.highlightingTabs = new Set();
this.tabsToBeHighlightedAlone = new Set();
this.internallyMovingTabsForUpdatedNativeTabGroups = new Set();
this.subTreeMovingCount =
this.subTreeChildrenMovingCount =
this.doingIntelligentlyCollapseExpandCount =
this.duplicatingTabsCount = 0;
this.internallyFocusingTabs = new Set();
this.internallyFocusingByMouseTabs = new Set();
this.internallyFocusingSilentlyTabs = new Set();
this.preventToDetectTabBunchesUntil = Date.now() + configs.tabBunchesDetectionDelayOnNewWindow;
this.openingTabs = new Set();
this.openedNewTabs = new Map();
this.bypassTabControlCount = 0;
this.toBeOpenedNewTabCommandTab = 0;
this.toBeOpenedTabsWithPositions = 0;
this.toBeOpenedTabsWithCookieStoreId = 0;
this.toBeOpenedOrphanTabs = 0;
this.toBeAttachedTabs = new Set();
this.toBeDetachedTabs = new Set();
this.lastRelatedTabs = new Map();
this.previousLastRelatedTabs = new Map();
TabsStore.windows.set(windowId, this);
TabsStore.prepareIndexesForWindow(windowId);
// We should initialize private properties with blank value for better performance with a fixed shape.
this.delayedDestroy = null;
}
initTabGroups(tabGroups) {
log(`initializing tabGroups of window ${this.id}: `, tabGroups);
this.tabGroups = new Map((tabGroups || []).map(group => [group.id, group]));
}
destroy() {
for (const tab of this.tabs.values()) {
if (tab.$TST)
tab.$TST.destroy();
}
this.tabs.clear();
this.tabGroups.clear();
TabsStore.windows.delete(this.id);
TabsStore.unprepareIndexesForWindow(this.id);
if (this.containerElement) {
const element = this.containerElement;
if (element.parentNode && !element.hasChildNodes())
element.parentNode.removeChild(element);
}
if (this.pinnedContainerElement) {
const element = this.element;
if (element.parentNode && !element.hasChildNodes())
element.parentNode.removeChild(element);
}
this.unbindElements();
this.tabs = null;
this.tabGroups = null;
this.order = null;
this.id = null;
}
clear() {
this.tabs.clear();
this.order = [];
TabsStore.unprepareIndexesForWindow(this.id);
TabsStore.prepareIndexesForWindow(this.id);
this.clearLastRelatedTabs();
}
clearLastRelatedTabs() {
for (const openerId of this.lastRelatedTabs.keys()) {
const opener = this.tabs.get(openerId);
if (!opener)
continue;
opener.$TST.newRelatedTabsCount = 0;
}
this.lastRelatedTabs.clear();
this.previousLastRelatedTabs.clear();
}
bindContainerElement(element) {
this.containerElement = element;
this.containerClassList = element.classList;
}
bindPinnedContainerElement(element) {
this.pinnedContainerElement = element;
this.pinnedContainerClassList = element.classList;
}
unbindElements() {
this.containerElement = null;
this.containerClassList = null;
this.pinnedContainerElement = null;
this.pinnedContainerClassList = null;
}
getOrderedTabs(startId, endId, tabs) {
const orderedIds = this.sliceOrder(startId, endId);
tabs = tabs || this.tabs;
return (function*() {
for (const id of orderedIds) {
const tab = tabs.get(id);
if (tab)
yield tab;
}
}).call(this);
}
getReversedOrderedTabs(startId, endId, tabs) {
const orderedIds = this.sliceOrder(startId, endId, this.order.slice(0).reverse());
tabs = tabs || this.tabs;
return (function*() {
for (const id of orderedIds) {
const tab = tabs.get(id);
if (tab)
yield tab;
}
}).call(this);
}
sliceOrder(startId, endId, orderedIds) {
if (!orderedIds)
orderedIds = this.order;
if (startId) {
if (!this.tabs.has(startId))
return [];
orderedIds = orderedIds.slice(orderedIds.indexOf(startId));
}
if (endId) {
if (!this.tabs.has(endId))
return [];
orderedIds = orderedIds.slice(0, orderedIds.indexOf(endId) + 1);
}
return orderedIds;
}
trackTab(tab) {
const alreadyTracked = TabsStore.tabs.get(tab.id);
if (alreadyTracked)
tab = alreadyTracked;
if (this.delayedDestroy) {
clearTimeout(this.delayedDestroy);
this.delayedDestroy = null;
}
const order = this.order;
if (this.tabs.has(tab.id)) { // already tracked: update
const prevState = tab.reindexedBy;
const index = order.indexOf(tab.id);
order.splice(index, 1);
order.splice(tab.index, 0, tab.id);
for (let i = Math.min(index, tab.index), maxi = Math.min(Math.max(index, tab.index) + 1, order.length); i < maxi; i++) {
const tab = this.tabs.get(order[i]);
if (!tab)
throw new Error(`Unknown tab: ${i}/${order[i]} (${order.join(', ')})`);
tab.index = i;
tab.reindexedBy = `Window.property.trackTab/update (${tab.index})`;
tab.$TST.invalidateCache();
}
const parent = tab.$TST.parent;
if (parent) {
parent.$TST.sortAndInvalidateChildren();
parent.$TST.invalidateCachedAncestors();
}
log(`tab ${dumpTab(tab)} is re-tracked under the window ${this.id}: `, prevState, index, '=>', tab.reindexedBy, order.join(', '));
}
else { // not tracked yet: add
this.tabs.set(tab.id, tab);
order.splice(tab.index, 0, tab.id);
for (let i = tab.index + 1, maxi = order.length; i < maxi; i++) {
const tab = this.tabs.get(order[i]);
if (!tab)
throw new Error(`Unknown tab: ${i}/${order[i]} (${order.join(', ')})`);
tab.index = i;
tab.reindexedBy = `Window.property.trackTab/new (${tab.index})`;
tab.$TST.invalidateCache();
}
log(`tab ${dumpTab(tab)} is newly tracked under the window ${this.id}: `, order);
}
TabsStore.updateIndexesForTab(tab);
tab.$TST.invalidateCache();
return tab;
}
detachTab(tabId) {
const tab = TabsStore.tabs.get(tabId);
if (!tab)
return;
TabsStore.removeTabFromIndexes(tab);
tab.$TST.detach();
this.tabs.delete(tabId);
const order = this.order;
const index = order.indexOf(tab.id);
if (index < 0) // the tab is not tracked yet!
return;
order.splice(index, 1);
if (this.tabs.size == 0) {
if (!TabsStore.getCurrentWindowId()) { // only in the background page - the sidebar has no need to destroy itself manually.
// the last tab can be removed with browser.tabs.closeWindowWithLastTab=false,
// so we should not destroy the window immediately.
if (this.delayedDestroy)
clearTimeout(this.delayedDestroy);
this.delayedDestroy = setTimeout(() => {
if (this.tabs &&
this.tabs.size == 0)
this.destroy();
}, (configs.collapseDuration, 1000) * 5);
}
}
else {
for (let i = index, maxi = order.length; i < maxi; i++) {
this.tabs.get(order[i]).index = i;
}
}
return tab;
}
untrackTab(tabId) {
const tab = this.detachTab(tabId);
if (tab)
tab.$TST.destroy();
}
export(full) {
const tabs = [];
for (const tab of this.getOrderedTabs()) {
tabs.push(tab.$TST.export(full));
}
return {
tabs,
tabGroups: [...this.tabGroups.values()].map(group => group.$TST.sanitized),
};
}
}
Window.onInitialized = new EventListenerManager();
Window.init = (windowId, tabGroups) => {
const win = TabsStore.windows.get(windowId) || new Window(windowId, tabGroups);
if (tabGroups && tabGroups.size != win.tabGroups.size) {
win.initTabGroups(tabGroups);
}
Window.onInitialized.dispatch(win);
return win;
}