Source code
Revision control
Copy as Markdown
Other Tools
/* 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
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
EventPromise: "chrome://remote/content/shared/Sync.sys.mjs",
});
// TODO(ato):
//
// The DOM team is working on pulling browsing context related behaviour,
// such as window and tab handling, out of product code and into the platform.
// This will have implication for the remote agent,
// and as the platform gains support for product-independent events
// we can likely get rid of this entire module.
/**
* Observe Firefox tabs as they open and close.
*
* "open" fires when a tab opens.
* "close" fires when a tab closes.
*/
export class TabObserver {
/**
* @param {boolean?} [false] registerExisting
* Events will be fired for ChromeWIndows and their respective tabs
* at the time when the observer is started.
*/
constructor({ registerExisting = false } = {}) {
lazy.EventEmitter.decorate(this);
this.registerExisting = registerExisting;
this.onTabOpen = this.onTabOpen.bind(this);
this.onTabClose = this.onTabClose.bind(this);
}
async start() {
Services.wm.addListener(this);
if (this.registerExisting) {
// Start listening for events on already open windows
for (const win of Services.wm.getEnumerator("navigator:browser")) {
this._registerDOMWindow(win);
}
}
}
stop() {
Services.wm.removeListener(this);
// Stop listening for events on still opened windows
for (const win of Services.wm.getEnumerator("navigator:browser")) {
this._unregisterDOMWindow(win);
}
}
// Event emitters
onTabOpen({ target }) {
this.emit("open", target);
}
onTabClose({ target }) {
this.emit("close", target);
}
// Internal methods
_registerDOMWindow(win) {
for (const tab of win.gBrowser.tabs) {
// a missing linkedBrowser means the tab is still initialising,
// and a TabOpen event will fire once it is ready
if (!tab.linkedBrowser) {
continue;
}
this.onTabOpen({ target: tab });
}
win.gBrowser.tabContainer.addEventListener("TabOpen", this.onTabOpen);
win.gBrowser.tabContainer.addEventListener("TabClose", this.onTabClose);
}
_unregisterDOMWindow(win) {
for (const tab of win.gBrowser.tabs) {
// a missing linkedBrowser means the tab is still initialising
if (!tab.linkedBrowser) {
continue;
}
// Emulate custom "TabClose" events because that event is not
// fired for each of the tabs when the window closes.
this.onTabClose({ target: tab });
}
win.gBrowser.tabContainer.removeEventListener("TabOpen", this.onTabOpen);
win.gBrowser.tabContainer.removeEventListener("TabClose", this.onTabClose);
}
// nsIWindowMediatorListener
async onOpenWindow(xulWindow) {
const win = xulWindow.docShell.domWindow;
await new lazy.EventPromise(win, "load");
// Return early if it's not a browser window
if (
win.document.documentElement.getAttribute("windowtype") !=
"navigator:browser"
) {
return;
}
this._registerDOMWindow(win);
}
onCloseWindow(xulWindow) {
const win = xulWindow.docShell.domWindow;
// Return early if it's not a browser window
if (
win.document.documentElement.getAttribute("windowtype") !=
"navigator:browser"
) {
return;
}
this._unregisterDOMWindow(win);
}
// XPCOM
get QueryInterface() {
return ChromeUtils.generateQI(["nsIWindowMediatorListener"]);
}
}