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
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
EventEmitter: "resource://gre/modules/EventEmitter.sys.mjs",
});
const OBSERVER_TOPIC_ATTACHED = "browsing-context-attached";
const OBSERVER_TOPIC_DISCARDED = "browsing-context-discarded";
const OBSERVER_TOPIC_SET_EMBEDDER = "browsing-context-did-set-embedder";
/**
* The BrowsingContextListener can be used to listen for notifications coming
* from browsing contexts that get attached or discarded.
*
* Example:
* ```
* const listener = new BrowsingContextListener();
* listener.on("attached", onAttached);
* listener.startListening();
*
* const onAttached = (eventName, data = {}) => {
* const { browsingContext, why } = data;
* ...
* };
* ```
*
* @fires message
* The BrowsingContextListener emits "attached" and "discarded" events,
* with the following object as payload:
* - {BrowsingContext} browsingContext
* Browsing context the notification relates to.
* - {string} why
* Usually "attach" or "discard", but will contain "replace" if the
* browsing context gets replaced by a cross-group navigation.
*/
export class BrowsingContextListener {
#listening;
#topContextsToAttach;
/**
* Create a new BrowsingContextListener instance.
*/
constructor() {
lazy.EventEmitter.decorate(this);
// A map that temporarily holds attached top-level browsing contexts until
// their embedder element is set, which is required to successfully
// retrieve a unique id for the content browser by the TabManager.
this.#topContextsToAttach = new Map();
this.#listening = false;
}
destroy() {
this.stopListening();
this.#topContextsToAttach = null;
}
observe(subject, topic, data) {
switch (topic) {
case OBSERVER_TOPIC_ATTACHED:
// Delay emitting the event for top-level browsing contexts until
// the embedder element has been set.
if (!subject.parent) {
this.#topContextsToAttach.set(subject, data);
return;
}
this.emit("attached", { browsingContext: subject, why: data });
break;
case OBSERVER_TOPIC_DISCARDED:
// Remove a recently attached top-level browsing context if it's
// immediately discarded.
if (this.#topContextsToAttach.has(subject)) {
this.#topContextsToAttach.delete(subject);
}
this.emit("discarded", { browsingContext: subject, why: data });
break;
case OBSERVER_TOPIC_SET_EMBEDDER:
const why = this.#topContextsToAttach.get(subject);
if (why !== undefined) {
this.emit("attached", { browsingContext: subject, why });
this.#topContextsToAttach.delete(subject);
}
break;
}
}
startListening() {
if (this.#listening) {
return;
}
Services.obs.addObserver(this, OBSERVER_TOPIC_ATTACHED);
Services.obs.addObserver(this, OBSERVER_TOPIC_DISCARDED);
Services.obs.addObserver(this, OBSERVER_TOPIC_SET_EMBEDDER);
this.#listening = true;
}
stopListening() {
if (!this.#listening) {
return;
}
Services.obs.removeObserver(this, OBSERVER_TOPIC_ATTACHED);
Services.obs.removeObserver(this, OBSERVER_TOPIC_DISCARDED);
Services.obs.removeObserver(this, OBSERVER_TOPIC_SET_EMBEDDER);
this.#topContextsToAttach.clear();
this.#listening = false;
}
}