484 lines
14 KiB
JavaScript
484 lines
14 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/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AppInfo: "chrome://remote/content/shared/AppInfo.sys.mjs",
|
|
BrowsingContextListener:
|
|
"chrome://remote/content/shared/listeners/BrowsingContextListener.sys.mjs",
|
|
EventPromise: "chrome://remote/content/shared/Sync.sys.mjs",
|
|
generateUUID: "chrome://remote/content/shared/UUID.sys.mjs",
|
|
MobileTabBrowser: "chrome://remote/content/shared/MobileTabBrowser.sys.mjs",
|
|
UserContextManager:
|
|
"chrome://remote/content/shared/UserContextManager.sys.mjs",
|
|
windowManager: "chrome://remote/content/shared/WindowManager.sys.mjs",
|
|
});
|
|
|
|
class TabManagerClass {
|
|
#browserUniqueIds;
|
|
#contextListener;
|
|
#navigableIds;
|
|
|
|
constructor() {
|
|
// Maps browser's permanentKey to uuid: WeakMap.<Object, string>
|
|
this.#browserUniqueIds = new WeakMap();
|
|
|
|
// Maps browsing contexts to uuid: WeakMap.<BrowsingContext, string>.
|
|
// It's required as a fallback, since in the case when a context was discarded
|
|
// embedderElement is gone, and we cannot retrieve
|
|
// the context id from this.#browserUniqueIds.
|
|
this.#navigableIds = new WeakMap();
|
|
|
|
this.#contextListener = new lazy.BrowsingContextListener();
|
|
this.#contextListener.on("attached", this.#onContextAttached);
|
|
this.#contextListener.startListening();
|
|
|
|
this.browsers.forEach(browser => {
|
|
if (this.isValidCanonicalBrowsingContext(browser.browsingContext)) {
|
|
this.#navigableIds.set(
|
|
browser.browsingContext,
|
|
this.getIdForBrowsingContext(browser.browsingContext)
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retrieve all the browser elements from tabs as contained in open windows.
|
|
*
|
|
* @returns {Array<XULBrowser>}
|
|
* All the found <xul:browser>s. Will return an empty array if
|
|
* no windows and tabs can be found.
|
|
*/
|
|
get browsers() {
|
|
const browsers = [];
|
|
|
|
for (const win of lazy.windowManager.windows) {
|
|
for (const tab of this.getTabsForWindow(win)) {
|
|
const contentBrowser = this.getBrowserForTab(tab);
|
|
if (contentBrowser !== null) {
|
|
browsers.push(contentBrowser);
|
|
}
|
|
}
|
|
}
|
|
|
|
return browsers;
|
|
}
|
|
|
|
/**
|
|
* Retrieve all the browser tabs in open windows.
|
|
*
|
|
* @returns {Array<Tab>}
|
|
* All the open browser tabs. Will return an empty list if tab browser
|
|
* is not available or tabs are undefined.
|
|
*/
|
|
get tabs() {
|
|
const tabs = [];
|
|
|
|
for (const win of lazy.windowManager.windows) {
|
|
tabs.push(...this.getTabsForWindow(win));
|
|
}
|
|
|
|
return tabs;
|
|
}
|
|
|
|
/**
|
|
* Array of unique browser ids (UUIDs) for all content browsers of all
|
|
* windows.
|
|
*
|
|
* TODO: Similarly to getBrowserById, we should improve the performance of
|
|
* this getter in Bug 1750065.
|
|
*
|
|
* @returns {Array<string>}
|
|
* Array of UUIDs for all content browsers.
|
|
*/
|
|
get allBrowserUniqueIds() {
|
|
const browserIds = [];
|
|
|
|
for (const win of lazy.windowManager.windows) {
|
|
// Only return handles for browser windows
|
|
for (const tab of this.getTabsForWindow(win)) {
|
|
const contentBrowser = this.getBrowserForTab(tab);
|
|
const winId = this.getIdForBrowser(contentBrowser);
|
|
if (winId !== null) {
|
|
browserIds.push(winId);
|
|
}
|
|
}
|
|
}
|
|
|
|
return browserIds;
|
|
}
|
|
|
|
/**
|
|
* Get the <code><xul:browser></code> for the specified tab.
|
|
*
|
|
* @param {Tab} tab
|
|
* The tab whose browser needs to be returned.
|
|
*
|
|
* @returns {XULBrowser}
|
|
* The linked browser for the tab or null if no browser can be found.
|
|
*/
|
|
getBrowserForTab(tab) {
|
|
if (tab && "linkedBrowser" in tab) {
|
|
return tab.linkedBrowser;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Return the tab browser for the specified chrome window.
|
|
*
|
|
* @param {ChromeWindow} win
|
|
* Window whose <code>tabbrowser</code> needs to be accessed.
|
|
*
|
|
* @returns {Tab}
|
|
* Tab browser or null if it's not a browser window.
|
|
*/
|
|
getTabBrowser(win) {
|
|
if (lazy.AppInfo.isAndroid) {
|
|
return new lazy.MobileTabBrowser(win);
|
|
} else if (lazy.AppInfo.isFirefox) {
|
|
return win.gBrowser;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Create a new tab.
|
|
*
|
|
* @param {object} options
|
|
* @param {boolean=} options.focus
|
|
* Set to true if the new tab should be focused (selected). Defaults to
|
|
* false. `false` value is not properly supported on Android, additional
|
|
* focus of previously selected tab is required after initial navigation.
|
|
* @param {Tab=} options.referenceTab
|
|
* The reference tab after which the new tab will be added. If no
|
|
* reference tab is provided, the new tab will be added after all the
|
|
* other tabs.
|
|
* @param {string=} options.userContextId
|
|
* A user context id from UserContextManager.
|
|
* @param {window=} options.window
|
|
* The window where the new tab will open. Defaults to Services.wm.getMostRecentWindow
|
|
* if no window is provided. Will be ignored if referenceTab is provided.
|
|
*/
|
|
async addTab(options = {}) {
|
|
let {
|
|
focus = false,
|
|
referenceTab = null,
|
|
userContextId = null,
|
|
window = Services.wm.getMostRecentWindow(null),
|
|
} = options;
|
|
|
|
let tabIndex;
|
|
if (referenceTab != null) {
|
|
// If a reference tab was specified, the window should be the window
|
|
// owning the reference tab.
|
|
window = this.getWindowForTab(referenceTab);
|
|
}
|
|
|
|
if (referenceTab != null) {
|
|
tabIndex = this.getTabsForWindow(window).indexOf(referenceTab) + 1;
|
|
}
|
|
|
|
const tabBrowser = this.getTabBrowser(window);
|
|
|
|
const tab = await tabBrowser.addTab("about:blank", {
|
|
tabIndex,
|
|
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
|
userContextId: lazy.UserContextManager.getInternalIdById(userContextId),
|
|
});
|
|
|
|
if (focus) {
|
|
await this.selectTab(tab);
|
|
}
|
|
|
|
return tab;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the browser element corresponding to the provided unique id,
|
|
* previously generated via getIdForBrowser.
|
|
*
|
|
* TODO: To avoid creating strong references on browser elements and
|
|
* potentially leaking those elements, this method loops over all windows and
|
|
* all tabs. It should be replaced by a faster implementation in Bug 1750065.
|
|
*
|
|
* @param {string} id
|
|
* A browser unique id created by getIdForBrowser.
|
|
* @returns {XULBrowser}
|
|
* The <xul:browser> corresponding to the provided id. Will return null if
|
|
* no matching browser element is found.
|
|
*/
|
|
getBrowserById(id) {
|
|
for (const win of lazy.windowManager.windows) {
|
|
for (const tab of this.getTabsForWindow(win)) {
|
|
const contentBrowser = this.getBrowserForTab(tab);
|
|
if (this.getIdForBrowser(contentBrowser) == id) {
|
|
return contentBrowser;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the browsing context corresponding to the provided unique id.
|
|
*
|
|
* @param {string} id
|
|
* A browsing context unique id (created by getIdForBrowsingContext).
|
|
* @returns {BrowsingContext=}
|
|
* The browsing context found for this id, null if none was found or
|
|
* browsing context is discarded.
|
|
*/
|
|
getBrowsingContextById(id) {
|
|
const browser = this.getBrowserById(id);
|
|
let browsingContext;
|
|
if (browser) {
|
|
browsingContext = browser.browsingContext;
|
|
} else {
|
|
browsingContext = BrowsingContext.get(id);
|
|
}
|
|
|
|
if (!browsingContext || browsingContext.isDiscarded) {
|
|
return null;
|
|
}
|
|
return browsingContext;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the unique id for the given xul browser element. The id is a
|
|
* dynamically generated uuid associated with the permanentKey property of the
|
|
* given browser element. This method is preferable over getIdForBrowsingContext
|
|
* in case of working with browser element of a tab, since we can not guarantee
|
|
* that browsing context is attached to it.
|
|
*
|
|
* @param {XULBrowser} browserElement
|
|
* The <xul:browser> for which we want to retrieve the id.
|
|
* @returns {string} The unique id for this browser.
|
|
*/
|
|
getIdForBrowser(browserElement) {
|
|
if (browserElement === null) {
|
|
return null;
|
|
}
|
|
|
|
const key = browserElement.permanentKey;
|
|
if (key === undefined) {
|
|
return null;
|
|
}
|
|
|
|
if (!this.#browserUniqueIds.has(key)) {
|
|
this.#browserUniqueIds.set(key, lazy.generateUUID());
|
|
}
|
|
return this.#browserUniqueIds.get(key);
|
|
}
|
|
|
|
/**
|
|
* Retrieve the id of a Browsing Context.
|
|
*
|
|
* For a top-level browsing context a custom unique id will be returned.
|
|
*
|
|
* @param {BrowsingContext=} browsingContext
|
|
* The browsing context to get the id from.
|
|
*
|
|
* @returns {string}
|
|
* The id of the browsing context.
|
|
*/
|
|
getIdForBrowsingContext(browsingContext) {
|
|
if (!browsingContext) {
|
|
return null;
|
|
}
|
|
|
|
if (!browsingContext.parent) {
|
|
// Top-level browsing contexts have their own custom unique id.
|
|
// If a context was discarded, embedderElement is already gone,
|
|
// so use navigable id instead.
|
|
return browsingContext.embedderElement
|
|
? this.getIdForBrowser(browsingContext.embedderElement)
|
|
: this.#navigableIds.get(browsingContext);
|
|
}
|
|
|
|
return browsingContext.id.toString();
|
|
}
|
|
|
|
/**
|
|
* Get the navigable for the given browsing context.
|
|
*
|
|
* Because Gecko doesn't support the Navigable concept in content
|
|
* scope the content browser could be used to uniquely identify
|
|
* top-level browsing contexts.
|
|
*
|
|
* @param {BrowsingContext} browsingContext
|
|
*
|
|
* @returns {BrowsingContext|XULBrowser} The navigable
|
|
*
|
|
* @throws {TypeError}
|
|
* If `browsingContext` is not a CanonicalBrowsingContext instance.
|
|
*/
|
|
getNavigableForBrowsingContext(browsingContext) {
|
|
if (!this.isValidCanonicalBrowsingContext(browsingContext)) {
|
|
throw new TypeError(
|
|
`Expected browsingContext to be a CanonicalBrowsingContext, got ${browsingContext}`
|
|
);
|
|
}
|
|
|
|
if (browsingContext.isContent && browsingContext.parent === null) {
|
|
return browsingContext.embedderElement;
|
|
}
|
|
|
|
return browsingContext;
|
|
}
|
|
|
|
getTabCount() {
|
|
let count = 0;
|
|
for (const win of lazy.windowManager.windows) {
|
|
// For browser windows count the tabs. Otherwise take the window itself.
|
|
const tabsLength = this.getTabsForWindow(win).length;
|
|
count += tabsLength ? tabsLength : 1;
|
|
}
|
|
return count;
|
|
}
|
|
|
|
/**
|
|
* Retrieve the tab owning a Browsing Context.
|
|
*
|
|
* @param {BrowsingContext=} browsingContext
|
|
* The browsing context to get the tab from.
|
|
*
|
|
* @returns {Tab|null}
|
|
* The tab owning the Browsing Context.
|
|
*/
|
|
getTabForBrowsingContext(browsingContext) {
|
|
const browser = browsingContext?.top.embedderElement;
|
|
if (!browser) {
|
|
return null;
|
|
}
|
|
|
|
const tabBrowser = this.getTabBrowser(browser.ownerGlobal);
|
|
return tabBrowser.getTabForBrowser(browser);
|
|
}
|
|
|
|
/**
|
|
* Retrieve the list of tabs for a given window.
|
|
*
|
|
* @param {ChromeWindow} win
|
|
* Window whose <code>tabs</code> need to be returned.
|
|
*
|
|
* @returns {Array<Tab>}
|
|
* The list of tabs. Will return an empty list if tab browser is not available
|
|
* or tabs are undefined.
|
|
*/
|
|
getTabsForWindow(win) {
|
|
const tabBrowser = this.getTabBrowser(win);
|
|
// For web-platform reftests a faked tabbrowser is used,
|
|
// which does not actually have tabs.
|
|
if (tabBrowser && tabBrowser.tabs) {
|
|
return tabBrowser.tabs;
|
|
}
|
|
return [];
|
|
}
|
|
|
|
getWindowForTab(tab) {
|
|
// `.linkedBrowser.ownerGlobal` works both with Firefox Desktop and Mobile.
|
|
// Other accessors (eg `.ownerGlobal` or `.browser.ownerGlobal`) fail on one
|
|
// of the platforms.
|
|
return tab.linkedBrowser.ownerGlobal;
|
|
}
|
|
|
|
/**
|
|
* Check if the given argument is a valid canonical browsing context and was not
|
|
* discarded.
|
|
*
|
|
* @param {BrowsingContext} browsingContext
|
|
* The browsing context to check.
|
|
*
|
|
* @returns {boolean}
|
|
* True if the browsing context is valid, false otherwise.
|
|
*/
|
|
isValidCanonicalBrowsingContext(browsingContext) {
|
|
return (
|
|
CanonicalBrowsingContext.isInstance(browsingContext) &&
|
|
!browsingContext.isDiscarded
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Remove the given tab.
|
|
*
|
|
* @param {Tab} tab
|
|
* Tab to remove.
|
|
* @param {object=} options
|
|
* @param {boolean=} options.skipPermitUnload
|
|
* Flag to indicate if a potential beforeunload prompt should be skipped
|
|
* when closing the tab. Defaults to false.
|
|
*/
|
|
async removeTab(tab, options = {}) {
|
|
const { skipPermitUnload = false } = options;
|
|
|
|
if (!tab) {
|
|
return;
|
|
}
|
|
|
|
const ownerWindow = this.getWindowForTab(tab);
|
|
const tabBrowser = this.getTabBrowser(ownerWindow);
|
|
await tabBrowser.removeTab(tab, {
|
|
skipPermitUnload,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Select the given tab.
|
|
*
|
|
* @param {Tab} tab
|
|
* Tab to select.
|
|
*
|
|
* @returns {Promise}
|
|
* Promise that resolves when the given tab has been selected.
|
|
*/
|
|
async selectTab(tab) {
|
|
if (!tab) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const ownerWindow = this.getWindowForTab(tab);
|
|
const tabBrowser = this.getTabBrowser(ownerWindow);
|
|
|
|
if (tab === tabBrowser.selectedTab) {
|
|
return Promise.resolve();
|
|
}
|
|
|
|
const selected = new lazy.EventPromise(ownerWindow, "TabSelect");
|
|
tabBrowser.selectedTab = tab;
|
|
|
|
await selected;
|
|
|
|
// Sometimes at that point window is not focused.
|
|
if (Services.focus.activeWindow != ownerWindow) {
|
|
const activated = new lazy.EventPromise(ownerWindow, "activate");
|
|
ownerWindow.focus();
|
|
return activated;
|
|
}
|
|
|
|
return Promise.resolve();
|
|
}
|
|
|
|
supportsTabs() {
|
|
return lazy.AppInfo.isAndroid || lazy.AppInfo.isFirefox;
|
|
}
|
|
|
|
#onContextAttached = (eventName, data = {}) => {
|
|
const { browsingContext } = data;
|
|
if (this.isValidCanonicalBrowsingContext(browsingContext)) {
|
|
this.#navigableIds.set(
|
|
browsingContext,
|
|
this.getIdForBrowsingContext(browsingContext)
|
|
);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Expose a shared singleton.
|
|
export const TabManager = new TabManagerClass();
|