293 lines
10 KiB
JavaScript
293 lines
10 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 {
|
|
log as internalLogger,
|
|
dumpTab,
|
|
wait,
|
|
mapAndFilterUniq,
|
|
countMatched,
|
|
configs
|
|
} from '/common/common.js';
|
|
import * as Constants from '/common/constants.js';
|
|
import * as TabsStore from '/common/tabs-store.js';
|
|
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
|
|
import * as TreeBehavior from '/common/tree-behavior.js';
|
|
|
|
import { Tab } from '/common/TreeItem.js';
|
|
|
|
import * as Background from './background.js';
|
|
import * as TabsGroup from './tabs-group.js';
|
|
import * as Tree from './tree.js';
|
|
import * as Commands from './commands.js';
|
|
|
|
function log(...args) {
|
|
internalLogger('background/handle-removed-tabs', ...args);
|
|
}
|
|
|
|
|
|
Tab.onRemoving.addListener(async (tab, removeInfo = {}) => {
|
|
log('Tabs.onRemoving ', dumpTab(tab), removeInfo);
|
|
if (removeInfo.isWindowClosing)
|
|
return;
|
|
|
|
let closeParentBehavior;
|
|
let newParent;
|
|
const successor = tab.$TST.possibleSuccessorWithDifferentContainer;
|
|
if (successor) {
|
|
log('override closeParentBehaior with kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD for different container successor ', successor);
|
|
closeParentBehavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD;
|
|
// When a new tab is created with a different container and this tab
|
|
// is removed immediately before the new tab is completely handled,
|
|
// TST fails to detect the new tab as the successor of this tab. Thus,
|
|
// we treat the new tab as the successor - the first child of this
|
|
// (actually not attached to this tab yet).
|
|
if (successor && successor != tab.$TST.firstChild)
|
|
newParent = successor;
|
|
}
|
|
else {
|
|
closeParentBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
|
|
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE,
|
|
...removeInfo,
|
|
});
|
|
log('detected closeParentBehaior: ', closeParentBehavior);
|
|
}
|
|
|
|
if (closeParentBehavior != Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE &&
|
|
tab.$TST.subtreeCollapsed)
|
|
Tree.collapseExpandSubtree(tab, {
|
|
collapsed: false,
|
|
justNow: true,
|
|
broadcast: false // because the tab is going to be closed, broadcasted Tree.collapseExpandSubtree can be ignored.
|
|
});
|
|
|
|
const postProcessParams = {
|
|
windowId: tab.windowId,
|
|
removedTab: tab.$TST.export(true),
|
|
structure: TreeBehavior.getTreeStructureFromTabs([tab, ...tab.$TST.descendants], {
|
|
full: true,
|
|
keepParentOfRootTabs: true
|
|
}),
|
|
insertBefore: tab, // not firstChild, because the "tab" is disappeared from tree.
|
|
parent: tab.$TST.parent,
|
|
newParent,
|
|
children: tab.$TST.children,
|
|
descendants: tab.$TST.descendants,
|
|
nearestFollowingRootTab: tab.$TST.nearestFollowingRootTab,
|
|
closeParentBehavior
|
|
};
|
|
|
|
if (tab.$TST.subtreeCollapsed) {
|
|
tryGrantCloseTab(tab, closeParentBehavior).then(async granted => {
|
|
if (!granted)
|
|
return;
|
|
log('Tabs.onRemoving: granted to close ', dumpTab(tab));
|
|
handleRemovingPostProcess(postProcessParams)
|
|
});
|
|
// First we always need to detach children from the closing parent.
|
|
// They will be processed again after confirmation.
|
|
Tree.detachAllChildren(tab, {
|
|
newParent,
|
|
behavior: Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_ALL_CHILDREN,
|
|
dontExpand: true,
|
|
dontUpdateIndent: true,
|
|
broadcast: true
|
|
});
|
|
}
|
|
else {
|
|
await handleRemovingPostProcess(postProcessParams)
|
|
}
|
|
|
|
const win = TabsStore.windows.get(tab.windowId);
|
|
if (!win.internalClosingTabs.has(tab.$TST.parentId))
|
|
Tree.detachTab(tab, {
|
|
dontUpdateIndent: true,
|
|
dontSyncParentToOpenerTab: true,
|
|
broadcast: true
|
|
});
|
|
});
|
|
async function handleRemovingPostProcess({ closeParentBehavior, windowId, parent, newParent, insertBefore, nearestFollowingRootTab, children, descendants, removedTab, structure } = {}) {
|
|
log('handleRemovingPostProcess ', { closeParentBehavior, windowId, parent, newParent, insertBefore, nearestFollowingRootTab, children, descendants, removedTab, structure });
|
|
if (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
|
|
await closeChildTabs(descendants, {
|
|
triggerTab: removedTab,
|
|
originalStructure: structure
|
|
});
|
|
|
|
const replacedGroupTab = (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_REPLACE_WITH_GROUP_TAB) ?
|
|
await TabsGroup.tryReplaceTabWithGroup(null, { windowId, parent, children, insertBefore, newParent }) :
|
|
null;
|
|
|
|
if (!replacedGroupTab) {
|
|
await Tree.detachAllChildren(null, {
|
|
windowId,
|
|
parent,
|
|
newParent,
|
|
children,
|
|
descendants,
|
|
nearestFollowingRootTab,
|
|
behavior: closeParentBehavior,
|
|
broadcast: true
|
|
});
|
|
}
|
|
}
|
|
|
|
async function tryGrantCloseTab(tab, closeParentBehavior) {
|
|
log('tryGrantClose: ', { alreadyGranted: configs.grantedRemovingTabIds, closing: dumpTab(tab) });
|
|
const alreadyGranted = configs.grantedRemovingTabIds.includes(tab.id);
|
|
configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => id != tab.id);
|
|
if (!tab || alreadyGranted) {
|
|
log(' => no need to confirm');
|
|
return true;
|
|
}
|
|
|
|
const self = tryGrantCloseTab;
|
|
|
|
self.closingTabIds.push(tab.id);
|
|
if (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE) {
|
|
self.closingDescendantTabIds = self.closingDescendantTabIds
|
|
.concat(TreeBehavior.getClosingTabsFromParent(tab).map(tab => tab.id));
|
|
self.closingDescendantTabIds = Array.from(new Set(self.closingDescendantTabIds));
|
|
}
|
|
|
|
if (self.promisedGrantedToCloseTabs) {
|
|
log(' => have promisedGrantedToCloseTabs');
|
|
return self.promisedGrantedToCloseTabs;
|
|
}
|
|
|
|
self.closingTabWasActive = self.closingTabWasActive || tab.active;
|
|
|
|
let shouldRestoreCount;
|
|
self.promisedGrantedToCloseTabs = wait(250).then(async () => {
|
|
log(' => confirmation with delay');
|
|
const closingTabIds = new Set(self.closingTabIds);
|
|
let allClosingTabs = new Set();
|
|
allClosingTabs.add(tab);
|
|
self.closingTabIds = Array.from(closingTabIds);
|
|
self.closingDescendantTabIds = mapAndFilterUniq(self.closingDescendantTabIds, id => {
|
|
if (closingTabIds.has(id))
|
|
return undefined;
|
|
const tab = Tab.get(id);
|
|
if (tab) // ignore already closed tabs
|
|
allClosingTabs.add(tab);
|
|
return id;
|
|
});
|
|
allClosingTabs = Array.from(allClosingTabs);
|
|
shouldRestoreCount = self.closingTabIds.length;
|
|
const restorableClosingTabsCount = countMatched(
|
|
allClosingTabs,
|
|
tab => tab.url != 'about:blank' &&
|
|
!tab.$TST.isNewTabCommandTab
|
|
);
|
|
log(' => restorableClosingTabsCount: ', restorableClosingTabsCount);
|
|
if (restorableClosingTabsCount > 0) {
|
|
log('tryGrantClose: show confirmation for ', allClosingTabs);
|
|
return Background.confirmToCloseTabs(allClosingTabs.slice(1).map(tab => tab.$TST.sanitized), {
|
|
windowId: tab.windowId,
|
|
messageKey: 'warnOnCloseTabs_fromOutside_message',
|
|
titleKey: 'warnOnCloseTabs_fromOutside_title',
|
|
minConfirmCount: 0
|
|
});
|
|
}
|
|
return true;
|
|
})
|
|
.then(async (granted) => {
|
|
log(' => granted: ', granted);
|
|
// remove the closed tab itself because it is already closed!
|
|
configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => id != tab.id);
|
|
if (granted)
|
|
return true;
|
|
log(`tryGrantClose: not granted, restore ${shouldRestoreCount} tabs`);
|
|
// this is required to wait until the closing tab is stored to the "recently closed" list
|
|
wait(0).then(async () => {
|
|
const restoredTabs = await Commands.restoreTabs(shouldRestoreCount);
|
|
log('tryGrantClose: restored ', restoredTabs);
|
|
});
|
|
return false;
|
|
});
|
|
|
|
const granted = await self.promisedGrantedToCloseTabs;
|
|
self.closingTabIds = [];
|
|
self.closingDescendantTabIds = [];
|
|
self.closingTabWasActive = false;
|
|
self.promisedGrantedToCloseTabs = null;
|
|
return granted;
|
|
}
|
|
tryGrantCloseTab.closingTabIds = [];
|
|
tryGrantCloseTab.closingDescendantTabIds = [];
|
|
tryGrantCloseTab.closingTabWasActive = false;
|
|
tryGrantCloseTab.promisedGrantedToCloseTabs = null;
|
|
|
|
async function closeChildTabs(tabs, { triggerTab, originalStructure } = {}) {
|
|
//if (!fireTabSubtreeClosingEvent(parent, tabs))
|
|
// return;
|
|
|
|
//markAsClosedSet([parent].concat(tabs));
|
|
// close bottom to top!
|
|
await TabsInternalOperation.removeTabs(tabs.slice(0).reverse(), { triggerTab, originalStructure });
|
|
//fireTabSubtreeClosedEvent(parent, tabs);
|
|
}
|
|
|
|
Tab.onRemoved.addListener((tab, info) => {
|
|
log('Tabs.onRemoved: removed ', dumpTab(tab));
|
|
configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => id != tab.id);
|
|
|
|
if (info.isWindowClosing)
|
|
return;
|
|
|
|
// The removing tab may be attached to another tab or
|
|
// other tabs may be attached to the removing tab.
|
|
// We need to detach such relations always on this timing.
|
|
if (info.oldChildren.length > 0) {
|
|
Tree.detachAllChildren(tab, {
|
|
children: info.oldChildren,
|
|
parent: info.oldParent,
|
|
behavior: Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD,
|
|
broadcast: true
|
|
});
|
|
}
|
|
if (info.oldParent) {
|
|
Tree.detachTab(tab, {
|
|
dontSyncParentToOpenerTab: true,
|
|
parent: info.oldParent,
|
|
broadcast: true
|
|
});
|
|
}
|
|
});
|
|
|
|
browser.windows.onRemoved.addListener(windowId => {
|
|
const win = TabsStore.windows.get(windowId);
|
|
if (!win)
|
|
return;
|
|
configs.grantedRemovingTabIds = configs.grantedRemovingTabIds.filter(id => !win.tabs.has(id));
|
|
});
|
|
|
|
|
|
Tab.onDetached.addListener((tab, info = {}) => {
|
|
log('Tabs.onDetached ', dumpTab(tab));
|
|
let closeParentBehavior = TreeBehavior.getParentTabOperationBehavior(tab, {
|
|
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_MOVE,
|
|
...info
|
|
});
|
|
if (closeParentBehavior == Constants.kPARENT_TAB_OPERATION_BEHAVIOR_ENTIRE_TREE)
|
|
closeParentBehavior = Constants.kPARENT_TAB_OPERATION_BEHAVIOR_PROMOTE_FIRST_CHILD;
|
|
|
|
const dontSyncParentToOpenerTab = info.trigger == 'tabs.onDetached';
|
|
Tree.detachAllChildren(tab, {
|
|
dontSyncParentToOpenerTab,
|
|
behavior: closeParentBehavior,
|
|
broadcast: true
|
|
});
|
|
//reserveCloseRelatedTabs(toBeClosedTabs);
|
|
Tree.detachTab(tab, {
|
|
dontSyncParentToOpenerTab,
|
|
dontUpdateIndent: true,
|
|
broadcast: true
|
|
});
|
|
//restoreTabAttributes(tab, backupAttributes);
|
|
});
|