Files
tubestation/waterfox/browser/components/sidebar/background/handle-misc.js
2025-11-06 14:13:52 +00:00

1162 lines
37 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,
wait,
mapAndFilterUniq,
configs,
loadUserStyleRules,
doProgressively,
} from '/common/common.js';
import * as ApiTabs from '/common/api-tabs.js';
import * as Bookmark from '/common/bookmark.js';
import * as BrowserTheme from '/common/browser-theme.js';
import * as Constants from '/common/constants.js';
import * as ContextualIdentities from '/common/contextual-identities.js';
import * as Permissions from '/common/permissions.js';
import * as SidebarConnection from '/common/sidebar-connection.js';
import * as TabsInternalOperation from '/common/tabs-internal-operation.js';
import * as TabsStore from '/common/tabs-store.js';
import * as TabsUpdate from '/common/tabs-update.js';
import * as TreeBehavior from '/common/tree-behavior.js';
import * as TSTAPI from '/common/tst-api.js';
import { Tab, TreeItem } from '/common/TreeItem.js';
import * as Background from './background.js';
import * as Commands from './commands.js';
import * as Migration from './migration.js';
import * as TabsGroup from './tabs-group.js';
import * as TabsOpen from './tabs-open.js';
import * as Tree from './tree.js';
function log(...args) {
internalLogger('background/handle-misc', ...args);
}
/* message observer */
// We cannot start listening of messages of browser.runtime.onMessage(External)
// at here and wait processing until promises are resolved like ApiTabsListener
// and BackgroundConnection, because making listeners asynchornous (async
// functions) will break things - those listeners must not return Promise for
// unneeded cases.
// So we simply ignore messages delivered before completely initialized, for now.
// See also: https://github.com/piroor/treestyletab/issues/2200
const PHASE_LOADING = 0;
const PHASE_BACKGROUND_INITIALIZED = 1;
const PHASE_BACKGROUND_BUILT = 2;
const PHASE_BACKGROUND_READY = 3;
let mInitializationPhase = PHASE_LOADING;
Background.onInit.addListener(() => {
mInitializationPhase = PHASE_BACKGROUND_INITIALIZED;
});
Background.onBuilt.addListener(() => {
mInitializationPhase = PHASE_BACKGROUND_BUILT;
});
Background.onReady.addListener(() => {
mInitializationPhase = PHASE_BACKGROUND_READY;
});
if (browser.sidebarAction)
(browser.action || browser.browserAction)?.onClicked.addListener(onToolbarButtonClick);
browser.commands.onCommand.addListener(onShortcutCommand);
browser.runtime.onMessage.addListener(onMessage);
TSTAPI.onMessageExternal.addListener(onMessageExternal);
Background.onReady.addListener(() => {
Bookmark.startTracking();
});
Background.onDestroy.addListener(() => {
browser.runtime.onMessage.removeListener(onMessage);
TSTAPI.onMessageExternal.removeListener(onMessageExternal);
if (browser.sidebarAction)
(browser.action || browser.browserAction)?.onClicked.removeListener(onToolbarButtonClick);
});
function onToolbarButtonClick(tab) {
if (mInitializationPhase < PHASE_BACKGROUND_INITIALIZED ||
Permissions.requestPostProcess()) {
return;
}
if (Migration.isInitialStartup()) {
Migration.openInitialStartupPage();
return;
}
if (typeof browser.sidebarAction.toggle == 'function')
browser.sidebarAction.toggle();
else if (SidebarConnection.isSidebarOpen(tab.windowId))
browser.sidebarAction.close();
else
browser.sidebarAction.open();
}
async function onShortcutCommand(command) {
if (mInitializationPhase < PHASE_BACKGROUND_INITIALIZED)
return;
let activeTabs = command.tab ? [command.tab] : await browser.tabs.query({
active: true,
currentWindow: true,
}).catch(ApiTabs.createErrorHandler());
if (activeTabs.length == 0)
activeTabs = await browser.tabs.query({
currentWindow: true,
}).catch(ApiTabs.createErrorHandler());
const activeTab = Tab.get(activeTabs[0].id);
const selectedTabs = activeTab.$TST.multiselected ? Tab.getSelectedTabs(activeTab.windowId) : [activeTab];
log('onShortcutCommand ', { command, activeTab, selectedTabs });
switch (command) {
case '_execute_browser_action':
return;
case 'reloadTree':
Commands.reloadTree(selectedTabs);
return;
case 'reloadDescendants':
Commands.reloadDescendants(selectedTabs);
return;
case 'toggleMuteTree':
Commands.toggleMuteTree(selectedTabs);
return;
case 'toggleMuteDescendants':
Commands.toggleMuteDescendants(selectedTabs);
return;
case 'closeTree':
Commands.closeTree(selectedTabs);
return;
case 'closeDescendants':
Commands.closeDescendants(selectedTabs);
return;
case 'closeOthers':
Commands.closeOthers(selectedTabs);
return;
case 'toggleSticky':
Commands.toggleSticky(selectedTabs);
return;
case 'collapseTree':
Commands.collapseTree(selectedTabs);
return;
case 'collapseTreeRecursively':
Commands.collapseTree(selectedTabs, { recursively: true });
return;
case 'collapseAll':
Commands.collapseAll(activeTab.windowId);
return;
case 'expandTree':
Commands.expandTree(selectedTabs);
return;
case 'expandTreeRecursively':
Commands.expandTree(selectedTabs, { recursively: true });
return;
case 'expandAll':
Commands.expandAll(activeTab.windowId);
return;
case 'bookmarkTree':
Commands.bookmarkTree(selectedTabs);
return;
case 'newIndependentTab':
Commands.openNewTabAs({
baseTab: activeTab,
as: Constants.kNEWTAB_OPEN_AS_ORPHAN
});
return;
case 'newChildTab':
Commands.openNewTabAs({
baseTab: activeTab,
as: Constants.kNEWTAB_OPEN_AS_CHILD
});
return;
case 'newChildTabTop':
Commands.openNewTabAs({
baseTab: activeTab,
as: Constants.kNEWTAB_OPEN_AS_CHILD_TOP
});
return;
case 'newChildTabEnd':
Commands.openNewTabAs({
baseTab: activeTab,
as: Constants.kNEWTAB_OPEN_AS_CHILD_END
});
return;
case 'newSiblingTab':
Commands.openNewTabAs({
baseTab: activeTab,
as: Constants.kNEWTAB_OPEN_AS_SIBLING
});
return;
case 'newNextSiblingTab':
Commands.openNewTabAs({
baseTab: activeTab,
as: Constants.kNEWTAB_OPEN_AS_NEXT_SIBLING
});
return;
case 'newContainerTab':
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_SHOW_CONTAINER_SELECTOR,
windowId: activeTab.windowId
});
return;
case 'tabMoveUp':
Commands.moveUp(activeTab, { followChildren: false });
return;
case 'treeMoveUp':
Commands.moveUp(activeTab, { followChildren: true });
return;
case 'tabMoveDown':
Commands.moveDown(activeTab, { followChildren: false });
return;
case 'treeMoveDown':
Commands.moveDown(activeTab, { followChildren: true });
return;
case 'focusPrevious':
focusPrevious(activeTab);
return;
case 'focusPreviousSilently':
focusPreviousSilently(activeTab);
return;
case 'focusNext':
focusNext(activeTab);
return;
case 'focusNextSilently':
focusNextSilently(activeTab);
return;
case 'focusParent':
TabsInternalOperation.activateTab(activeTab.$TST.parent);
return;
case 'focusParentOrCollapse':
collapseOrFocusToParent(activeTab);
return;
case 'focusFirstChild':
TabsInternalOperation.activateTab(activeTab.$TST.firstChild);
return;
case 'focusFirstChildOrExpand':
expandOrFocusToFirstChild(activeTab);
return;
case 'focusLastChild':
TabsInternalOperation.activateTab(activeTab.$TST.lastChild);
return;
case 'focusPreviousSibling':
TabsInternalOperation.activateTab(
activeTab.$TST.previousSiblingTab ||
(activeTab.$TST.parent ?
activeTab.$TST.parent.$TST.lastChild :
Tab.getLastRootTab(activeTab.windowId))
);
return;
case 'focusNextSibling':
TabsInternalOperation.activateTab(
activeTab.$TST.nextSiblingTab ||
(activeTab.$TST.parent ?
activeTab.$TST.parent.$TST.firstChild :
Tab.getFirstVisibleTab(activeTab.windowId))
);
return;
case 'simulateUpOnTree':
if (SidebarConnection.isOpen(activeTab.windowId)) {
if (configs.faviconizePinnedTabs &&
(activeTab.pinned ||
activeTab == Tab.getFirstNormalTab(activeTab.windowId))) {
const nextActiveId = await browser.runtime.sendMessage({
type: Constants.kCOMMAND_GET_ABOVE_TAB,
windowId: activeTab.windowId,
tabId: activeTab.id,
});
log(`simulateUpOnTree: nextActiveId = ${nextActiveId}`);
const nextActive = (
Tab.get(nextActiveId) ||
Tab.getLastVisibleTab(activeTab.windowId)
);
TabsInternalOperation.activateTab(nextActive, {
silently: true,
});
}
else {
focusPreviousSilently(activeTab);
}
}
else {
focusPrevious(activeTab);
}
return;
case 'simulateDownOnTree':
if (SidebarConnection.isOpen(activeTab.windowId)) {
if (configs.faviconizePinnedTabs &&
activeTab.pinned) {
const nextActiveId = await browser.runtime.sendMessage({
type: Constants.kCOMMAND_GET_BELOW_TAB,
windowId: activeTab.windowId,
tabId: activeTab.id,
});
log(`simulateDownOnTree: nextActiveId = ${nextActiveId}`);
const nextActive = (
Tab.get(nextActiveId) ||
Tab.getFirstNormalTab(activeTab.windowId)
);
TabsInternalOperation.activateTab(nextActive, {
silently: true,
});
}
else {
focusNextSilently(activeTab);
}
}
else {
focusNext(activeTab);
}
return;
case 'simulateLeftOnTree':
if (SidebarConnection.isOpen(activeTab.windowId)) {
if (configs.faviconizePinnedTabs &&
activeTab.pinned) {
const nextActiveId = await browser.runtime.sendMessage({
type: Constants.kCOMMAND_GET_LEFT_TAB,
windowId: activeTab.windowId,
tabId: activeTab.id,
});
log(`simulateLeftOnTree: nextActiveId = ${nextActiveId}`);
TabsInternalOperation.activateTab(Tab.get(nextActiveId), {
silently: true,
});
}
else if (await isSidebarRightSide(activeTab.windowId)) {
expandOrFocusToFirstChild(activeTab);
}
else {
collapseOrFocusToParent(activeTab);
}
}
else {
focusPrevious(activeTab);
}
return;
case 'simulateRightOnTree':
if (SidebarConnection.isOpen(activeTab.windowId)) {
if (configs.faviconizePinnedTabs &&
activeTab.pinned) {
const nextActiveId = await browser.runtime.sendMessage({
type: Constants.kCOMMAND_GET_RIGHT_TAB,
windowId: activeTab.windowId,
tabId: activeTab.id,
});
log(`simulateRightOnTree: nextActiveId = ${nextActiveId}`);
TabsInternalOperation.activateTab(Tab.get(nextActiveId), {
silently: true,
});
}
else if (await isSidebarRightSide(activeTab.windowId)) {
collapseOrFocusToParent(activeTab);
}
else {
expandOrFocusToFirstChild(activeTab);
}
}
else {
focusNext(activeTab);
}
return;
case 'tabbarUp':
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_SCROLL_TABBAR,
windowId: activeTab.windowId,
by: 'lineup'
});
return;
case 'tabbarPageUp':
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_SCROLL_TABBAR,
windowId: activeTab.windowId,
by: 'pageup'
});
return;
case 'tabbarHome':
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_SCROLL_TABBAR,
windowId: activeTab.windowId,
to: 'top'
});
return;
case 'tabbarDown':
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_SCROLL_TABBAR,
windowId: activeTab.windowId,
by: 'linedown'
});
return;
case 'tabbarPageDown':
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_SCROLL_TABBAR,
windowId: activeTab.windowId,
by: 'pagedown'
});
return;
case 'tabbarEnd':
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_SCROLL_TABBAR,
windowId: activeTab.windowId,
to: 'bottom'
});
return;
case 'toggleTreeCollapsed':
if (activeTab.$TST.subtreeCollapsed)
Commands.expandTree(selectedTabs);
else
Commands.collapseTree(selectedTabs);
return;
case 'toggleTreeCollapsedRecursively':
if (activeTab.$TST.subtreeCollapsed)
Commands.expandTree(selectedTabs, { recursively: true });
else
Commands.collapseTree(selectedTabs, { recursively: true });
return;
case 'toggleSubPanel':
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_TOGGLE_SUBPANEL,
windowId: activeTab.windowId
});
return;
case 'switchSubPanel':
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_SWITCH_SUBPANEL,
windowId: activeTab.windowId
});
return;
case 'increaseSubPanel':
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_INCREASE_SUBPANEL,
windowId: activeTab.windowId
});
return;
case 'decreaseSubPanel':
SidebarConnection.sendMessage({
type: Constants.kCOMMAND_DECREASE_SUBPANEL,
windowId: activeTab.windowId
});
return;
}
}
function focusPrevious(activeTab) {
const nextActive = activeTab.$TST.nearestVisiblePrecedingTab ||
(!SidebarConnection.isOpen(activeTab.windowId) && activeTab.$TST.previousTab) ||
Tab.getLastVisibleTab(activeTab.windowId);
TabsInternalOperation.activateTab(nextActive);
}
function focusPreviousSilently(activeTab) {
const nextActive = activeTab.$TST.nearestVisiblePrecedingTab ||
(!SidebarConnection.isOpen(activeTab.windowId) && activeTab.$TST.previousTab) ||
Tab.getLastVisibleTab(activeTab.windowId);
TabsInternalOperation.activateTab(nextActive, {
silently: true,
});
}
function focusNext(activeTab) {
const nextActive = activeTab.$TST.nearestVisibleFollowingTab ||
(!SidebarConnection.isOpen(activeTab.windowId) && activeTab.$TST.nextTab) ||
Tab.getFirstVisibleTab(activeTab.windowId);
TabsInternalOperation.activateTab(nextActive);
}
function focusNextSilently(activeTab) {
const nextActive = activeTab.$TST.nearestVisibleFollowingTab ||
(!SidebarConnection.isOpen(activeTab.windowId) && activeTab.$TST.nextTab) ||
Tab.getFirstVisibleTab(activeTab.windowId);
TabsInternalOperation.activateTab(nextActive, {
silently: true,
});
}
async function isSidebarRightSide(windowId) {
const position = await browser.runtime.sendMessage({
type: Constants.kCOMMAND_GET_SIDEBAR_POSITION,
windowId,
});
return position == Constants.kTABBAR_POSITION_RIGHT;
}
function collapseOrFocusToParent(activeTab) {
if (!activeTab.$TST.subtreeCollapsed && activeTab.$TST.hasChild)
Commands.collapseTree(activeTab);
else
TabsInternalOperation.activateTab(activeTab.$TST.parent);
}
function expandOrFocusToFirstChild(activeTab) {
if (activeTab.$TST.subtreeCollapsed && activeTab.$TST.hasChild)
Commands.expandTree(activeTab);
else
TabsInternalOperation.activateTab(activeTab.$TST.firstChild, {
silently: true,
});
}
// This must be synchronous and return Promise on demando, to avoid
// blocking to other listeners.
function onMessage(message, sender) {
if (mInitializationPhase < PHASE_BACKGROUND_BUILT ||
!message ||
typeof message.type != 'string' ||
message.type.indexOf('ws:') != 0)
return;
//log('onMessage: ', message, sender);
switch (message.type) {
case Constants.kCOMMAND_GET_INSTANCE_ID:
return Promise.resolve(Background.instanceId);
case Constants.kCOMMAND_RELOAD:
return Background.reload({ all: message.all });
case Constants.kCOMMAND_REQUEST_UNIQUE_ID:
return (async () => {
if (!Tab.get(message.tabId))
await Tab.waitUntilTracked(message.tabId);
const tab = Tab.get(message.tabId);
return tab ? tab.$TST.promisedUniqueId : {};
})();
case Constants.kCOMMAND_GET_THEME_DECLARATIONS:
return browser.theme.getCurrent().then(theme => BrowserTheme.generateThemeDeclarations(theme));
case Constants.kCOMMAND_GET_CONTEXTUAL_IDENTITIES_COLOR_INFO:
return Promise.resolve(ContextualIdentities.getColorInfo());
case Constants.kCOMMAND_GET_CONFIG_VALUE:
if (Array.isArray(message.keys)) {
const values = {};
for (const key of message.keys) {
values[key] = configs[key];
}
return Promise.resolve(values);
}
return Promise.resolve(configs[message.key]);
case Constants.kCOMMAND_SET_CONFIG_VALUE:
return Promise.resolve(configs[message.key] = message.value);
case Constants.kCOMMAND_GET_USER_STYLE_RULES:
return Promise.resolve(loadUserStyleRules());
case Constants.kCOMMAND_PING_TO_BACKGROUND: // return tabs as the pong, to optimizie further initialization tasks in the sidebar
TabsUpdate.completeLoadingTabs(message.windowId); // don't wait here for better perfomance
return Promise.resolve(TabsStore.windows.get(message.windowId).export(true));
case Constants.kCOMMAND_PULL_TABS:
if (message.windowId) {
TabsUpdate.completeLoadingTabs(message.windowId); // don't wait here for better perfomance
return Promise.resolve(TabsStore.windows.get(message.windowId).export(true).tabs);
}
return Promise.resolve(message.tabIds.map(id => {
const tab = Tab.get(id);
return tab?.$TST.export(true);
}));
case Constants.kCOMMAND_PULL_TABS_ORDER:
return Promise.resolve(TabsStore.windows.get(message.windowId).order);
case Constants.kCOMMAND_PULL_TREE_STRUCTURE:
return (async () => {
while (mInitializationPhase < PHASE_BACKGROUND_READY) {
await wait(10);
}
const structure = TreeBehavior.getTreeStructureFromTabs(
message.windowId ?
Tab.getAllTabs(message.windowId) :
message.tabIds.map(id => Tab.get(id))
);
return { structure };
})();
case Constants.kCOMMAND_NOTIFY_PERMISSIONS_GRANTED:
return (async () => {
const grantedPermission = JSON.stringify(message.permissions);
if (grantedPermission == JSON.stringify(Permissions.ALL_URLS)) {
const tabs = await browser.tabs.query({}).catch(ApiTabs.createErrorHandler());
await Tab.waitUntilTracked(tabs.map(tab => tab.id));
for (const tab of tabs) {
Background.tryStartHandleAccelKeyOnTab(Tab.get(tab.id));
}
}
else if (grantedPermission == JSON.stringify(Permissions.BOOKMARKS)) {
Migration.migrateBookmarkUrls();
Bookmark.startTracking();
}
})();
case Constants.kCOMMAND_SIMULATE_SIDEBAR_MESSAGE: {
SidebarConnection.onMessage.dispatch(message.message.windowId, message.message);
}; break;
case Constants.kCOMMAND_CONFIRM_TO_CLOSE_TABS:
log('kCOMMAND_CONFIRM_TO_CLOSE_TABS: ', { message });
return Background.confirmToCloseTabs(message.tabs, message);
default:
if (TSTAPI.INTERNAL_CALL_PREFIX_MATCHER.test(message.type)) {
return onMessageExternal({
...message,
type: message.type.replace(TSTAPI.INTERNAL_CALL_PREFIX_MATCHER, ''),
}, sender);
}
break;
}
}
// This must be synchronous and return Promise on demando, to avoid
// blocking to other listeners.
function onMessageExternal(message, sender) {
if (mInitializationPhase < PHASE_BACKGROUND_INITIALIZED)
return;
switch (message.type) {
case TSTAPI.kGET_VERSION:
return Promise.resolve(browser.runtime.getManifest().version);
case TSTAPI.kGET_TREE:
return (async () => {
const tabs = await (message.rendered ?
TSTAPI.getTargetRenderedTabs(message, sender) :
TSTAPI.getTargetTabs(message, sender));
const cache = {};
const treeItems = Array.from(tabs, tab => TSTAPI.exportTab(tab, {
interval: message.interval,
cache,
}));
const result = TSTAPI.formatTabResult(
treeItems,
{
...message,
// This must return an array of root tabs if just the window id is specified.
// See also: https://github.com/piroor/treestyletab/issues/2763
...((message.window || message.windowId) && !message.tab && !message.tabs ? { tab: '*' } : {})
},
sender.id
);
TSTAPI.clearCache(cache);
return result;
})();
case TSTAPI.kGET_LIGHT_TREE:
return (async () => {
const tabs = await (message.rendered ?
TSTAPI.getTargetRenderedTabs(message, sender) :
TSTAPI.getTargetTabs(message, sender));
const cache = {};
const treeItems = Array.from(tabs, tab => TSTAPI.exportTab(tab, {
light: true,
interval: message.interval,
cache,
}));
const result = TSTAPI.formatTabResult(
treeItems,
{
...message,
// This must return an array of root tabs if just the window id is specified.
// See also: https://github.com/piroor/treestyletab/issues/2763
...((message.window || message.windowId) && !message.tab && !message.tabs ? { tab: '*' } : {})
},
sender.id
);
TSTAPI.clearCache(cache);
return result;
})();
case TSTAPI.kSTICK_TAB:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
await doProgressively(
tabs,
tab => Commands.toggleSticky(tab, true),
message.interval
);
return true;
})();
case TSTAPI.kUNSTICK_TAB:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
await doProgressively(
tabs,
tab => Commands.toggleSticky(tab, false),
message.interval
);
return true;
})();
case TSTAPI.kTOGGLE_STICKY_STATE:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
let firstTabIsSticky = undefined;
await doProgressively(
tabs,
tab => {
if (firstTabIsSticky === undefined)
firstTabIsSticky = tab.$TST.sticky;
Commands.toggleSticky(tab, !firstTabIsSticky);
},
message.interval
);
return true;
})();
case TSTAPI.kCOLLAPSE_TREE:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
await doProgressively(
tabs,
tab => Commands.collapseTree(tab, {
recursively: !!message.recursively
}),
message.interval
);
return true;
})();
case TSTAPI.kEXPAND_TREE:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
await doProgressively(
tabs,
tab => Commands.expandTree(tab, {
recursively: !!message.recursively
}),
message.interval
);
return true;
})();
case TSTAPI.kTOGGLE_TREE_COLLAPSED:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
await doProgressively(
tabs,
tab => Tree.collapseExpandSubtree(tab, {
collapsed: !tab.$TST.subtreeCollapsed,
broadcast: true
}),
message.interval
);
return true;
})();
case TSTAPI.kATTACH:
return (async () => {
await Tab.waitUntilTracked([
message.child,
message.parent,
message.insertBefore,
message.insertAfter
]);
const child = Tab.get(message.child);
const parent = Tab.get(message.parent);
if (!child ||
!parent ||
child.windowId != parent.windowId)
return false;
await Tree.attachTabTo(child, parent, {
broadcast: true,
insertBefore: Tab.get(message.insertBefore),
insertAfter: Tab.get(message.insertAfter)
});
if (child.$TST.collapsed &&
!parent.$TST.collapsed &&
!parent.$TST.subtreeCollapsed) {
await Tree.collapseExpandTabAndSubtree(child, {
collapsed: false,
bradcast: true
});
}
return true;
})();
case TSTAPI.kDETACH:
return (async () => {
await Tab.waitUntilTracked(message.tab);
const tab = Tab.get(message.tab);
if (!tab)
return false;
await Tree.detachTab(tab, {
broadcast: true
});
if (tab.$TST.collapsed) {
await Tree.collapseExpandTabAndSubtree(tab, {
collapsed: false,
bradcast: true
});
}
return true;
})();
case TSTAPI.kINDENT:
case TSTAPI.kDEMOTE:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
const results = await doProgressively(
tabs,
tab => Commands.indent(tab, message),
message.interval
);
return TSTAPI.formatResult(results, message);
})();
case TSTAPI.kOUTDENT:
case TSTAPI.kPROMOTE:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
const results = await doProgressively(
tabs,
tab => Commands.outdent(tab, message),
message.interval
);
return TSTAPI.formatResult(results, message);
})();
case TSTAPI.kMOVE_UP:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
const results = await doProgressively(
tabs,
tab => Commands.moveUp(tab, message),
message.interval
);
return TSTAPI.formatResult(results, message);
})();
case TSTAPI.kMOVE_TO_START:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
await Commands.moveTabsToStart(Array.from(tabs));
return true;
})();
case TSTAPI.kMOVE_DOWN:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
const results = await doProgressively(
tabs,
tab => Commands.moveDown(tab, message),
message.interval
);
return TSTAPI.formatResult(results, message);
})();
case TSTAPI.kMOVE_TO_END:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
await Commands.moveTabsToEnd(Array.from(tabs));
return true;
})();
case TSTAPI.kMOVE_BEFORE:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
const results = await doProgressively(
tabs,
tab => Commands.moveBefore(tab, message),
message.interval
);
return TSTAPI.formatResult(results, message);
})();
case TSTAPI.kMOVE_AFTER:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
const results = await doProgressively(
tabs,
tab => Commands.moveAfter(tab, message),
message.interval
);
return TSTAPI.formatResult(results, message);
})();
case TSTAPI.kFOCUS:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
const tabsArray = await doProgressively(
tabs,
tab => {
TabsInternalOperation.activateTab(tab, {
silently: message.silently
});
return tab;
},
message.interval
);
return TSTAPI.formatResult(tabsArray.map(() => true), message);
})();
case TSTAPI.kCREATE:
return (async () => {
const windowId = message.params.windowId;
const win = TabsStore.windows.get(windowId);
if (!win)
throw new Error(`invalid windowId ${windowId}: it must be valid window id`);
win.bypassTabControlCount++;
const tab = await TabsOpen.openURIInTab(message.params, { windowId });
return TSTAPI.exportTab(tab, { addonId: sender.id });
})();
case TSTAPI.kDUPLICATE:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
let behavior = configs.autoAttachOnDuplicated;
switch (String(message.as || 'sibling').toLowerCase()) {
case 'child':
behavior = behavior == Constants.kNEWTAB_OPEN_AS_CHILD_TOP ?
Constants.kNEWTAB_OPEN_AS_CHILD_TOP :
behavior == Constants.kNEWTAB_OPEN_AS_CHILD_END ?
Constants.kNEWTAB_OPEN_AS_CHILD_END :
Constants.kNEWTAB_OPEN_AS_CHILD;
break;
case 'first-child':
behavior = Constants.kNEWTAB_OPEN_AS_CHILD_TOP;
break;
case 'last-child':
behavior = Constants.kNEWTAB_OPEN_AS_CHILD_END;
break;
case 'sibling':
behavior = Constants.kNEWTAB_OPEN_AS_SIBLING;
break;
case 'nextsibling':
behavior = Constants.kNEWTAB_OPEN_AS_NEXT_SIBLING;
break;
case 'orphan':
behavior = Constants.kNEWTAB_OPEN_AS_ORPHAN;
break;
default:
break;
}
const tabsArray = await doProgressively(
tabs,
async tab => {
return Commands.duplicateTab(tab, {
destinationWindowId: tab.windowId,
behavior,
multiselected: false
});
},
message.interval
);
return TSTAPI.formatResult(tabsArray.map(() => true), message);
})();
case TSTAPI.kGROUP_TABS:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
const temporaryStateParams = (message.temporary && !message.temporaryAggressive) ?
{
temporary: true,
temporaryAggressive: false,
} :
(!message.temporary && message.temporaryAggressive) ?
{
temporary: false,
temporaryAggressive: true,
} :
(message.temporaryAggressive === false && message.temporary === false) ?
{
temporary: false,
temporaryAggressive: false,
} :
{};
const tab = await TabsGroup.groupTabs(Array.from(tabs), {
title: message.title,
broadcast: true,
...TabsGroup.temporaryStateParams(configs.groupTabTemporaryStateForAPI),
...temporaryStateParams,
});
if (!tab)
return null;
return TSTAPI.exportTab(tab, { addonId: sender.id });
})();
case TSTAPI.kOPEN_IN_NEW_WINDOW:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
const windowId = await Commands.openTabsInWindow(Array.from(tabs), {
multiselected: false
});
return windowId;
})();
case TSTAPI.kREOPEN_IN_CONTAINER:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
const reopenedTabs = await Commands.reopenInContainer(
Array.from(tabs),
message.containerId || 'firefox-default'
);
const cache = {};
const result = await TSTAPI.formatTabResult(
reopenedTabs.map(tab => TSTAPI.exportTab(tab, {
interval: message.interval,
addonId: sender.id,
cache,
})),
message
);
TSTAPI.clearCache(cache);
return result;
})();
case TSTAPI.kGET_TREE_STRUCTURE:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
return Promise.resolve(TreeBehavior.getTreeStructureFromTabs(Array.from(tabs)));
})();
case TSTAPI.kSET_TREE_STRUCTURE:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
await Tree.applyTreeStructureToTabs(
Array.from(tabs),
message.structure,
{ broadcast: true }
);
return Promise.resolve(true);
})();
case TSTAPI.kADD_TAB_STATE:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
let states = message.state || message.states;
if (!Array.isArray(states))
states = [states];
states = mapAndFilterUniq(states, state => state && String(state) || undefined);
if (states.length == 0)
return true;
const tabsArray = await doProgressively(
tabs,
tab => {
for (const state of states) {
tab.$TST.addState(state);
}
return tab;
},
message.interval
);
Tab.broadcastState(tabsArray, {
add: states
});
return true;
})();
case TSTAPI.kREMOVE_TAB_STATE:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
let states = message.state || message.states;
if (!Array.isArray(states))
states = [states];
states = mapAndFilterUniq(states, state => state && String(state) || undefined);
if (states.length == 0)
return true;
const tabsArray = await doProgressively(
tabs,
tab => {
for (const state of states) {
tab.$TST.removeState(state);
}
return tab;
},
message.interval
);
Tab.broadcastState(tabsArray, {
remove: states
});
return true;
})();
case TSTAPI.kGRANT_TO_REMOVE_TABS:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
configs.grantedRemovingTabIds = mapAndFilterUniq(configs.grantedRemovingTabIds.concat(tabs), tab => {
tab = TabsStore.ensureLivingItem(tab);
return tab?.id || undefined;
});
return true;
})();
case TSTAPI.kSTART_CUSTOM_DRAG:
return (async () => {
SidebarConnection.sendMessage({
type: Constants.kNOTIFY_TAB_MOUSEDOWN_EXPIRED,
windowId: message.windowId || (await browser.windows.getLastFocused({ populate: false }).catch(ApiTabs.createErrorHandler())).id,
button: message.button || 0
});
})();
case TSTAPI.kOPEN_ALL_BOOKMARKS_WITH_STRUCTURE:
return Commands.openAllBookmarksWithStructure(
message.id || message.bookmarkId,
{ discarded: message.discarded }
);
case TSTAPI.kSET_TOOLTIP_TEXT:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
for (const tab of tabs) {
tab.$TST.registerTooltipText(sender.id, message.text || '', !!message.force);
}
return true;
})();
case TSTAPI.kCLEAR_TOOLTIP_TEXT:
return (async () => {
const tabs = await TSTAPI.getTargetTabs(message, sender);
for (const tab of tabs) {
tab.$TST.unregisterTooltipText(sender.id);
}
return true;
})();
case TSTAPI.kREGISTER_AUTO_STICKY_STATES:
TreeItem.registerAutoStickyState(sender.id, message.state || message.states);
break;
case TSTAPI.kUNREGISTER_AUTO_STICKY_STATES:
TreeItem.unregisterAutoStickyState(sender.id, message.state || message.states);
break;
}
}
Tab.onStateChanged.addListener((tab, state, added) => {
switch (state) {
case Constants.kTAB_STATE_STICKY:
if (TSTAPI.hasListenerForMessageType(TSTAPI.kNOTIFY_TAB_STICKY_STATE_CHANGED)) {
TSTAPI.broadcastMessage({
type: TSTAPI.kNOTIFY_TAB_STICKY_STATE_CHANGED,
tab,
sticky: !!added,
}, { tabProperties: ['tab'] }).catch(_error => {});
}
break;
}
});