304 lines
8.8 KiB
JavaScript
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;
|
|
}
|