663 lines
23 KiB
JavaScript
663 lines
23 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,
|
|
configs,
|
|
wait,
|
|
countMatched,
|
|
dumpTab
|
|
} from '/common/common.js';
|
|
import * as ApiTabs from '/common/api-tabs.js';
|
|
import * as Constants from '/common/constants.js';
|
|
import * as SidebarConnection from '/common/sidebar-connection.js';
|
|
import * as TabsStore from '/common/tabs-store.js';
|
|
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
|
|
import * as TabsUpdate from '/common/tabs-update.js';
|
|
import * as TreeBehavior from '/common/tree-behavior.js';
|
|
|
|
import { Tab } from '/common/TreeItem.js';
|
|
|
|
import * as TabsMove from './tabs-move.js';
|
|
import * as TabsOpen from './tabs-open.js';
|
|
import * as Tree from './tree.js';
|
|
|
|
function log(...args) {
|
|
internalLogger('background/tabs-group', ...args);
|
|
}
|
|
|
|
export function makeGroupTabURI({ title, temporary, temporaryAggressive, openerTabId, aliasTabId, replacedParentCount } = {}) {
|
|
const url = new URL(Constants.kGROUP_TAB_URI);
|
|
|
|
if (title)
|
|
url.searchParams.set('title', title);
|
|
|
|
if (temporaryAggressive)
|
|
url.searchParams.set('temporaryAggressive', 'true');
|
|
else if (temporary)
|
|
url.searchParams.set('temporary', 'true');
|
|
|
|
if (openerTabId)
|
|
url.searchParams.set('openerTabId', openerTabId);
|
|
|
|
if (aliasTabId)
|
|
url.searchParams.set('aliasTabId', aliasTabId);
|
|
|
|
if (replacedParentCount)
|
|
url.searchParams.set('replacedParentCount', replacedParentCount);
|
|
|
|
return url.href;
|
|
}
|
|
|
|
export function temporaryStateParams(state) {
|
|
switch (state) {
|
|
case Constants.kGROUP_TAB_TEMPORARY_STATE_PASSIVE:
|
|
return {
|
|
temporary: true,
|
|
temporaryAggressive: false,
|
|
};
|
|
case Constants.kGROUP_TAB_TEMPORARY_STATE_AGGRESSIVE:
|
|
return {
|
|
temporary: false,
|
|
temporaryAggressive: true,
|
|
};
|
|
default:
|
|
break;
|
|
}
|
|
return {
|
|
temporary: false,
|
|
temporaryAggressive: false,
|
|
};
|
|
}
|
|
|
|
export async function groupTabs(tabs, { broadcast, parent, withDescendants, ...groupTabOptions } = {}) {
|
|
const rootTabs = Tab.collectRootTabs(tabs);
|
|
if (rootTabs.length <= 0)
|
|
return null;
|
|
|
|
log('groupTabs: ', () => tabs.map(dumpTab), { broadcast, parent, withDescendants });
|
|
|
|
const uri = makeGroupTabURI({
|
|
title: browser.i18n.getMessage('groupTab_label', rootTabs[0].title),
|
|
temporary: true,
|
|
...groupTabOptions
|
|
});
|
|
const groupTab = await TabsOpen.openURIInTab(uri, {
|
|
windowId: rootTabs[0].windowId,
|
|
parent: parent || rootTabs[0].$TST.parent,
|
|
insertBefore: rootTabs[0],
|
|
inBackground: true
|
|
});
|
|
|
|
if (!withDescendants) {
|
|
const structure = TreeBehavior.getTreeStructureFromTabs(tabs);
|
|
|
|
await Tree.detachTabsFromTree(tabs, {
|
|
broadcast: !!broadcast,
|
|
});
|
|
|
|
log('structure: ', structure);
|
|
await Tree.applyTreeStructureToTabs(tabs, structure, {
|
|
broadcast: !!broadcast,
|
|
});
|
|
}
|
|
|
|
await TabsMove.moveTabsAfter(tabs.slice(1), tabs[0], {
|
|
broadcast: !!broadcast
|
|
});
|
|
for (const tab of rootTabs) {
|
|
await Tree.attachTabTo(tab, groupTab, {
|
|
forceExpand: true, // this is required to avoid the group tab itself is active from active tab in collapsed tree
|
|
dontMove: true,
|
|
broadcast: !!broadcast,
|
|
});
|
|
}
|
|
return groupTab;
|
|
}
|
|
|
|
function reserveToCleanupNeedlessGroupTab(tabOrTabs) {
|
|
const tabs = Array.isArray(tabOrTabs) ? tabOrTabs : [tabOrTabs] ;
|
|
for (const tab of tabs) {
|
|
if (!TabsStore.ensureLivingItem(tab))
|
|
continue;
|
|
if (tab.$TST.temporaryMetadata.has('reservedCleanupNeedlessGroupTab'))
|
|
clearTimeout(tab.$TST.temporaryMetadata.get('reservedCleanupNeedlessGroupTab'));
|
|
tab.$TST.temporaryMetadata.set('reservedCleanupNeedlessGroupTab', setTimeout(() => {
|
|
if (!tab.$TST)
|
|
return;
|
|
tab.$TST.temporaryMetadata.delete('reservedCleanupNeedlessGroupTab');
|
|
cleanupNeedlssGroupTab(tab);
|
|
}, 100));
|
|
}
|
|
}
|
|
|
|
function cleanupNeedlssGroupTab(tabs) {
|
|
if (!Array.isArray(tabs))
|
|
tabs = [tabs];
|
|
log('trying to clanup needless temporary group tabs from ', () => tabs.map(dumpTab));
|
|
const tabsToBeRemoved = [];
|
|
for (const tab of tabs) {
|
|
if (tab.$TST.temporaryMetadata.has('movingAcrossWindows'))
|
|
continue;
|
|
|
|
if (tab.$TST.isTemporaryGroupTab) {
|
|
if (tab.$TST.childIds.length > 1)
|
|
break;
|
|
const lastChild = tab.$TST.firstChild;
|
|
if (lastChild &&
|
|
!lastChild.$TST.isTemporaryGroupTab &&
|
|
!lastChild.$TST.isTemporaryAggressiveGroupTab)
|
|
break;
|
|
}
|
|
else if (tab.$TST.isTemporaryAggressiveGroupTab) {
|
|
if (tab.$TST.childIds.length > 1)
|
|
break;
|
|
}
|
|
else {
|
|
break;
|
|
}
|
|
tabsToBeRemoved.push(tab);
|
|
}
|
|
log('=> to be removed: ', () => tabsToBeRemoved.map(dumpTab));
|
|
TabsInternalOperation.removeTabs(tabsToBeRemoved, { keepDescendants: true });
|
|
}
|
|
|
|
export async function tryReplaceTabWithGroup(tab, { windowId, parent, children, insertBefore, newParent } = {}) {
|
|
if (tab) {
|
|
windowId = tab.windowId;
|
|
parent = tab.$TST.parent;
|
|
children = tab.$TST.children;
|
|
insertBefore = insertBefore || tab.$TST.unsafeNextTab;
|
|
}
|
|
|
|
if (children.length <= 1 ||
|
|
countMatched(children,
|
|
tab => !tab.$TST.states.has(Constants.kTAB_STATE_TO_BE_REMOVED)) <= 1)
|
|
return null;
|
|
|
|
log('trying to replace the closing tab with a new group tab');
|
|
|
|
const firstChild = children[0];
|
|
const uri = makeGroupTabURI({
|
|
title: browser.i18n.getMessage('groupTab_label', firstChild.title),
|
|
...temporaryStateParams(configs.groupTabTemporaryStateForOrphanedTabs),
|
|
replacedParentCount: (tab?.$TST?.replacedParentGroupTabCount || 0) + 1,
|
|
});
|
|
const win = TabsStore.windows.get(windowId);
|
|
win.toBeOpenedTabsWithPositions++;
|
|
const groupTab = await TabsOpen.openURIInTab(uri, {
|
|
windowId,
|
|
insertBefore,
|
|
inBackground: true
|
|
});
|
|
log('group tab: ', dumpTab(groupTab));
|
|
if (!groupTab) // the window is closed!
|
|
return;
|
|
if (newParent || parent)
|
|
await Tree.attachTabTo(groupTab, newParent || parent, {
|
|
dontMove: true,
|
|
broadcast: true
|
|
});
|
|
for (const child of children) {
|
|
await Tree.attachTabTo(child, groupTab, {
|
|
dontMove: true,
|
|
broadcast: true
|
|
});
|
|
}
|
|
|
|
// This can be triggered on closing of multiple tabs,
|
|
// so we should cleanup it on such cases for safety.
|
|
// https://github.com/piroor/treestyletab/issues/2317
|
|
wait(1000).then(() => reserveToCleanupNeedlessGroupTab(groupTab));
|
|
|
|
return groupTab;
|
|
}
|
|
|
|
|
|
// ====================================================================
|
|
// init/update group tabs
|
|
// ====================================================================
|
|
|
|
/*
|
|
To prevent the tab is closed by Firefox, we need to inject scripts dynamically.
|
|
See also: https://github.com/piroor/treestyletab/issues/1670#issuecomment-350964087
|
|
*/
|
|
async function tryInitGroupTab(tab) {
|
|
if (!tab.$TST.isGroupTab &&
|
|
!tab.$TST.hasGroupTabURL)
|
|
return;
|
|
log('tryInitGroupTab ', tab);
|
|
const v3Options = {
|
|
target: { tabId: tab.id },
|
|
};
|
|
const v2Options = {
|
|
runAt: 'document_start',
|
|
matchAboutBlank: true
|
|
};
|
|
try {
|
|
const getPageState = function getPageState() {
|
|
return [window.prepared, document.documentElement.matches('.initialized')];
|
|
};
|
|
const [prepared, initialized, reloaded] = (browser.scripting ?
|
|
browser.scripting.executeScript({ // Manifest V3
|
|
...v3Options,
|
|
func: getPageState,
|
|
}).then(results => results && results[0] && results[0].result || []) :
|
|
browser.tabs.executeScript(tab.id, {
|
|
...v2Options,
|
|
code: `(${getPageState.toString()})()`,
|
|
}).then(results => results && results[0] || [])
|
|
).catch(error => {
|
|
if (ApiTabs.isMissingHostPermissionError(error) &&
|
|
tab.$TST.hasGroupTabURL) {
|
|
log(' tryInitGroupTab: failed to run script for restored/discarded tab, reload the tab for safety ', tab.id);
|
|
browser.tabs.reload(tab.id);
|
|
return [[false, false, true]];
|
|
}
|
|
return ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError)(error);
|
|
});
|
|
log(' tryInitGroupTab: groupt tab state ', tab.id, { prepared, initialized, reloaded });
|
|
if (reloaded) {
|
|
log(' => reloaded ', tab.id);
|
|
return;
|
|
}
|
|
if (prepared && initialized) {
|
|
log(' => already initialized ', tab.id);
|
|
return;
|
|
}
|
|
}
|
|
catch(error) {
|
|
log(' tryInitGroupTab: error while checking initialized: ', tab.id, error);
|
|
}
|
|
try {
|
|
const getTitleExistence = function getState() {
|
|
return !!document.querySelector('#title');
|
|
};
|
|
const titleElementExists = (browser.scripting ?
|
|
browser.scripting.executeScript({ // Manifest V3
|
|
...v3Options,
|
|
func: getTitleExistence,
|
|
}).then(results => results && results[0] && results[0].result) :
|
|
browser.tabs.executeScript(tab.id, {
|
|
...v2Options,
|
|
code: `(${getTitleExistence.toString()})()`,
|
|
}).then(results => results && results[0])
|
|
).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError));
|
|
if (!titleElementExists && tab.status == 'complete') { // we need to load resources/group-tab.html at first.
|
|
log(' => title element exists, load again ', tab.id);
|
|
return browser.tabs.update(tab.id, { url: tab.url }).catch(ApiTabs.createErrorSuppressor());
|
|
}
|
|
}
|
|
catch(error) {
|
|
log(' tryInitGroupTab error while checking title element: ', tab.id, error);
|
|
}
|
|
|
|
(browser.scripting ?
|
|
browser.scripting.executeScript({ // Manifest V3
|
|
...v3Options,
|
|
files: [
|
|
'/extlib/l10n-classic.js', // ES module does not supported as a content script...
|
|
'/resources/group-tab.js',
|
|
],
|
|
}) :
|
|
Promise.all([
|
|
browser.tabs.executeScript(tab.id, {
|
|
...v2Options,
|
|
//file: '/common/l10n.js'
|
|
file: '/extlib/l10n-classic.js', // ES module does not supported as a content script...
|
|
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError)),
|
|
browser.tabs.executeScript(tab.id, {
|
|
...v2Options,
|
|
file: '/resources/group-tab.js',
|
|
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError, ApiTabs.handleMissingHostPermissionError)),
|
|
])
|
|
).then(() => {
|
|
log('tryInitGroupTab completely initialized: ', tab.id);
|
|
});
|
|
|
|
if (tab.$TST.states.has(Constants.kTAB_STATE_UNREAD)) {
|
|
tab.$TST.removeState(Constants.kTAB_STATE_UNREAD, { permanently: true });
|
|
SidebarConnection.sendMessage({
|
|
type: Constants.kCOMMAND_NOTIFY_TAB_UPDATED,
|
|
windowId: tab.windowId,
|
|
tabId: tab.id,
|
|
removedStates: [Constants.kTAB_STATE_UNREAD]
|
|
});
|
|
}
|
|
}
|
|
|
|
function reserveToUpdateRelatedGroupTabs(tab, changedInfo) {
|
|
const tabMetadata = tab.$TST.temporaryMetadata;
|
|
const updatingTabs = tabMetadata.get('reserveToUpdateRelatedGroupTabsUpdatingTabs') || new Set();
|
|
if (!tabMetadata.has('reserveToUpdateRelatedGroupTabsUpdatingTabs'))
|
|
tabMetadata.set('reserveToUpdateRelatedGroupTabsUpdatingTabs', updatingTabs);
|
|
|
|
const ancestorGroupTabs = [
|
|
tab,
|
|
tab.$TST.bundledTab,
|
|
...tab.$TST.ancestors,
|
|
...tab.$TST.ancestors.map(tab => tab.$TST.bundledTab),
|
|
].filter(tab => tab?.$TST.isGroupTab);
|
|
for (const updatingTab of ancestorGroupTabs) {
|
|
const updatingMetadata = updatingTab.$TST.temporaryMetadata;
|
|
const reservedChangedInfo = updatingMetadata.get('reservedUpdateRelatedGroupTabChangedInfo') || new Set();
|
|
for (const info of changedInfo) {
|
|
reservedChangedInfo.add(info);
|
|
}
|
|
if (updatingTabs.has(updatingTab.id))
|
|
continue;
|
|
updatingTabs.add(updatingTab.id);
|
|
const triggeredUpdates = updatingMetadata.get('reservedUpdateRelatedGroupTabTriggeredUpdates') || new Set();
|
|
triggeredUpdates.add(updatingTabs);
|
|
updatingMetadata.set('reservedUpdateRelatedGroupTabTriggeredUpdates', triggeredUpdates);
|
|
if (updatingMetadata.has('reservedUpdateRelatedGroupTab'))
|
|
clearTimeout(updatingMetadata.get('reservedUpdateRelatedGroupTab'));
|
|
updatingMetadata.set('reservedUpdateRelatedGroupTabChangedInfo', reservedChangedInfo);
|
|
updatingMetadata.set('reservedUpdateRelatedGroupTab', setTimeout(() => {
|
|
updatingMetadata.delete('reservedUpdateRelatedGroupTab');
|
|
if (updatingTab.$TST) {
|
|
try {
|
|
if (reservedChangedInfo.size > 0)
|
|
updateRelatedGroupTab(updatingTab, [...reservedChangedInfo]);
|
|
}
|
|
catch(_error) {
|
|
}
|
|
updatingMetadata.delete('reservedUpdateRelatedGroupTabChangedInfo');
|
|
}
|
|
setTimeout(() => {
|
|
const triggerUpdates = updatingMetadata.get('reservedUpdateRelatedGroupTabTriggeredUpdates')
|
|
updatingMetadata.delete('reservedUpdateRelatedGroupTabTriggeredUpdates');
|
|
if (!triggerUpdates)
|
|
return;
|
|
for (const updatingTabs of triggerUpdates) {
|
|
updatingTabs.delete(updatingTab.id);
|
|
}
|
|
}, 100)
|
|
}, 100));
|
|
}
|
|
}
|
|
|
|
async function updateRelatedGroupTab(groupTab, changedInfo = []) {
|
|
if (!TabsStore.ensureLivingItem(groupTab))
|
|
return;
|
|
|
|
await tryInitGroupTab(groupTab);
|
|
if (changedInfo.includes('tree')) {
|
|
try {
|
|
await browser.tabs.sendMessage(groupTab.id, {
|
|
type: 'ws:update-tree',
|
|
}).catch(error => {
|
|
if (ApiTabs.isMissingHostPermissionError(error))
|
|
throw error;
|
|
return ApiTabs.createErrorSuppressor(ApiTabs.handleMissingTabError, ApiTabs.handleUnloadedError)(error);
|
|
});
|
|
}
|
|
catch(error) {
|
|
if (ApiTabs.isMissingHostPermissionError(error)) {
|
|
log(' updateRelatedGroupTab: failed to run script for restored/discarded tab, reload the tab for safety ', groupTab.id);
|
|
browser.tabs.reload(groupTab.id);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
const firstChild = groupTab.$TST.firstChild;
|
|
if (!firstChild) // the tab can be closed while waiting...
|
|
return;
|
|
|
|
if (changedInfo.includes('title')) {
|
|
let newTitle;
|
|
if (Constants.kGROUP_TAB_DEFAULT_TITLE_MATCHER.test(groupTab.title)) {
|
|
newTitle = browser.i18n.getMessage('groupTab_label', firstChild.title);
|
|
}
|
|
else if (Constants.kGROUP_TAB_FROM_PINNED_DEFAULT_TITLE_MATCHER.test(groupTab.title)) {
|
|
const opener = groupTab.$TST.openerTab;
|
|
if (opener) {
|
|
if (opener &&
|
|
opener.favIconUrl) {
|
|
SidebarConnection.sendMessage({
|
|
type: Constants.kCOMMAND_NOTIFY_TAB_FAVICON_UPDATED,
|
|
windowId: groupTab.windowId,
|
|
tabId: groupTab.id,
|
|
favIconUrl: opener.favIconUrl
|
|
});
|
|
}
|
|
newTitle = browser.i18n.getMessage('groupTab_fromPinnedTab_label', opener.title);
|
|
}
|
|
}
|
|
|
|
if (newTitle && groupTab.title != newTitle) {
|
|
browser.tabs.sendMessage(groupTab.id, {
|
|
type: 'ws:update-title',
|
|
title: newTitle,
|
|
}).catch(ApiTabs.createErrorHandler(
|
|
ApiTabs.handleMissingTabError,
|
|
ApiTabs.handleMissingHostPermissionError,
|
|
_error => {
|
|
// failed to update the title by group tab itself, so we try to update it from outside
|
|
groupTab.title = newTitle;
|
|
TabsUpdate.updateTab(groupTab, { title: newTitle });
|
|
}
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
Tab.onRemoved.addListener((tab, _closeInfo = {}) => {
|
|
const ancestors = tab.$TST.ancestors;
|
|
wait(0).then(() => {
|
|
reserveToCleanupNeedlessGroupTab(ancestors);
|
|
});
|
|
});
|
|
|
|
Tab.onUpdated.addListener((tab, changeInfo) => {
|
|
if ('url' in changeInfo ||
|
|
'previousUrl' in changeInfo ||
|
|
'state' in changeInfo) {
|
|
const status = changeInfo.status || tab?.status;
|
|
const url = changeInfo.url ? changeInfo.url :
|
|
status == 'complete' && tab ? tab.url : '';
|
|
if (tab &&
|
|
status == 'complete') {
|
|
if (url.indexOf(Constants.kGROUP_TAB_URI) == 0) {
|
|
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
|
|
}
|
|
else if (!Constants.kSHORTHAND_ABOUT_URI.test(url)) {
|
|
tab.$TST.getPermanentStates().then(async (states) => {
|
|
if (url.indexOf(Constants.kGROUP_TAB_URI) == 0)
|
|
return;
|
|
// Detect group tab from different session - which can have different UUID for the URL.
|
|
const PREFIX_REMOVER = /^moz-extension:\/\/[^\/]+/;
|
|
const pathPart = url.replace(PREFIX_REMOVER, '');
|
|
if (states.includes(Constants.kTAB_STATE_GROUP_TAB) &&
|
|
pathPart.split('?')[0] == Constants.kGROUP_TAB_URI.replace(PREFIX_REMOVER, '')) {
|
|
const parameters = pathPart.replace(/^[^\?]+\?/, '');
|
|
const oldUrl = tab.url;
|
|
await wait(100); // for safety
|
|
if (tab.url != oldUrl)
|
|
return;
|
|
browser.tabs.update(tab.id, {
|
|
url: `${Constants.kGROUP_TAB_URI}?${parameters}`
|
|
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
|
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB);
|
|
}
|
|
else {
|
|
tab.$TST.removeState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
|
|
}
|
|
});
|
|
}
|
|
}
|
|
// restored tab can be replaced with blank tab. we need to restore it manually.
|
|
else if (changeInfo.url == 'about:blank' &&
|
|
changeInfo.previousUrl &&
|
|
changeInfo.previousUrl.indexOf(Constants.kGROUP_TAB_URI) == 0) {
|
|
const oldUrl = tab.url;
|
|
wait(100).then(() => { // redirect with delay to avoid infinite loop of recursive redirections.
|
|
if (tab.url != oldUrl)
|
|
return;
|
|
browser.tabs.update(tab.id, {
|
|
url: changeInfo.previousUrl
|
|
}).catch(ApiTabs.createErrorHandler(ApiTabs.handleMissingTabError));
|
|
tab.$TST.addState(Constants.kTAB_STATE_GROUP_TAB, { permanently: true });
|
|
});
|
|
}
|
|
|
|
if (changeInfo.status ||
|
|
changeInfo.url ||
|
|
url.indexOf(Constants.kGROUP_TAB_URI) == 0)
|
|
tryInitGroupTab(tab);
|
|
}
|
|
|
|
if ('title' in changeInfo) {
|
|
const group = Tab.getGroupTabForOpener(tab);
|
|
if (group)
|
|
reserveToUpdateRelatedGroupTabs(group, ['title', 'tree']);
|
|
}
|
|
});
|
|
|
|
Tab.onGroupTabDetected.addListener(tab => {
|
|
tryInitGroupTab(tab);
|
|
});
|
|
|
|
Tab.onLabelUpdated.addListener(tab => {
|
|
reserveToUpdateRelatedGroupTabs(tab, ['title', 'tree']);
|
|
});
|
|
|
|
Tab.onActivating.addListener((tab, _info = {}) => {
|
|
tryInitGroupTab(tab);
|
|
});
|
|
|
|
// returns a boolean: need to reload or not.
|
|
export async function clearTemporaryState(tab) {
|
|
if (!tab.$TST.isTemporaryGroupTab &&
|
|
!tab.$TST.isTemporaryAggressiveGroupTab)
|
|
return;
|
|
|
|
const url = new URL(tab.url);
|
|
url.searchParams.delete('temporary');
|
|
url.searchParams.delete('temporaryAggressive');
|
|
await Promise.all([
|
|
browser.tabs.sendMessage(tab.id, {
|
|
type: 'ws:clear-temporary-state',
|
|
}).catch(ApiTabs.createErrorHandler()),
|
|
browser.tabs.executeScript(tab.id, { // failsafe
|
|
runAt: 'document_start',
|
|
code: `history.replaceState({}, document.title, ${JSON.stringify(url.href)});`,
|
|
}).catch(ApiTabs.createErrorHandler()),
|
|
]);
|
|
tab.url = url.href;
|
|
}
|
|
|
|
Tab.onPinned.addListener(async tab => {
|
|
log('handlePinnedParentTab ', tab);
|
|
|
|
await Tree.collapseExpandSubtree(tab, {
|
|
collapsed: false,
|
|
broadcast: true
|
|
});
|
|
|
|
log(' childIdsBeforeMoved: ', tab.$TST.temporaryMetadata.get('childIdsBeforeMoved'));
|
|
log(' parentIdBeforeMoved: ', tab.$TST.temporaryMetadata.get('parentIdBeforeMoved'));
|
|
|
|
const children = (
|
|
tab.$TST.temporaryMetadata.has('childIdsBeforeMoved') ?
|
|
tab.$TST.temporaryMetadata.get('childIdsBeforeMoved').map(id => Tab.get(id)) :
|
|
tab.$TST.children
|
|
).filter(tab => TabsStore.ensureLivingItem(tab));
|
|
const parent = TabsStore.ensureLivingItem(
|
|
tab.$TST.temporaryMetadata.has('parentIdBeforeMoved') ?
|
|
Tab.get(tab.$TST.temporaryMetadata.get('parentIdBeforeMoved')) :
|
|
tab.$TST.parent
|
|
);
|
|
|
|
let openedGroupTab;
|
|
const shouldGroupChildren = configs.autoGroupNewTabsFromPinned || tab.$TST.isGroupTab;
|
|
if (shouldGroupChildren) {
|
|
log(' => trying to group left tabs with a group: ', children);
|
|
openedGroupTab = await groupTabs(children, {
|
|
// If the tab is a group tab, the opened tab should be treated as an alias of the pinned group tab.
|
|
// Otherwise it should be treated just as a temporary group tab to group children.
|
|
title: tab.$TST.isGroupTab ? tab.title : browser.i18n.getMessage('groupTab_fromPinnedTab_label', tab.title),
|
|
temporary: !tab.$TST.isGroupTab,
|
|
openerTabId: tab.$TST.uniqueId.id,
|
|
parent,
|
|
withDescendants: true,
|
|
});
|
|
log(' openedGroupTab: ', openedGroupTab);
|
|
// Tree structure of left tabs can be modified by someone like tryFixupTreeForInsertedTab@handle-moved-tabs.js.
|
|
// On such cases we need to restore the original tree structure.
|
|
const modifiedChildren = children.filter(child => children.includes(child.$TST.parent));
|
|
log(' modifiedChildren: ', modifiedChildren);
|
|
if (modifiedChildren.length > 0) {
|
|
for (const child of modifiedChildren) {
|
|
await Tree.detachTab(child, {
|
|
broadcast: true,
|
|
});
|
|
await Tree.attachTabTo(child, openedGroupTab, {
|
|
dontMove: true,
|
|
broadcast: true,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
else {
|
|
log(' => no need to group left tabs, just detaching');
|
|
await Tree.detachAllChildren(tab, {
|
|
behavior: TreeBehavior.getParentTabOperationBehavior(tab, {
|
|
context: Constants.kPARENT_TAB_OPERATION_CONTEXT_CLOSE,
|
|
preventEntireTreeBehavior: true,
|
|
}),
|
|
broadcast: true
|
|
});
|
|
}
|
|
await Tree.detachTab(tab, {
|
|
broadcast: true
|
|
});
|
|
|
|
// Such a group tab will be closed automatically when all children are detached.
|
|
// To prevent the auto close behavior, the tab type need to be turned to permanent.
|
|
await clearTemporaryState(tab);
|
|
|
|
if (tab.$TST.isGroupTab && openedGroupTab) {
|
|
const url = new URL(tab.url);
|
|
url.searchParams.set('aliasTabId', openedGroupTab.$TST.uniqueId.id);
|
|
await Promise.all([
|
|
browser.tabs.sendMessage(tab.id, {
|
|
type: 'ws:replace-state-url',
|
|
url: url.href,
|
|
}).catch(ApiTabs.createErrorHandler()),
|
|
browser.tabs.executeScript(tab.id, { // failsafe
|
|
runAt: 'document_start',
|
|
code: `history.replaceState({}, document.title, ${JSON.stringify(url.href)});`,
|
|
}).catch(ApiTabs.createErrorHandler()),
|
|
]);
|
|
await browser.tabs.sendMessage(tab.id, {
|
|
type: 'ws:update-tree',
|
|
url: url.href,
|
|
}).catch(ApiTabs.createErrorHandler());
|
|
tab.url = url.href;
|
|
}
|
|
});
|
|
|
|
Tree.onAttached.addListener((tab, _info = {}) => {
|
|
reserveToUpdateRelatedGroupTabs(tab, ['tree']);
|
|
});
|
|
|
|
Tree.onDetached.addListener((_tab, detachInfo) => {
|
|
if (!detachInfo.oldParentTab)
|
|
return;
|
|
if (detachInfo.oldParentTab.$TST.isGroupTab)
|
|
reserveToCleanupNeedlessGroupTab(detachInfo.oldParentTab);
|
|
reserveToUpdateRelatedGroupTabs(detachInfo.oldParentTab, ['tree']);
|
|
});
|
|
|
|
/*
|
|
Tree.onSubtreeCollapsedStateChanging.addListener((tab, _info) => {
|
|
reserveToUpdateRelatedGroupTabs(tab);
|
|
});
|
|
*/
|