diff options
Diffstat (limited to '')
-rw-r--r-- | remote/shared/listeners/BrowsingContextListener.sys.mjs | 122 |
1 files changed, 122 insertions, 0 deletions
diff --git a/remote/shared/listeners/BrowsingContextListener.sys.mjs b/remote/shared/listeners/BrowsingContextListener.sys.mjs new file mode 100644 index 0000000000..d4e3539ca9 --- /dev/null +++ b/remote/shared/listeners/BrowsingContextListener.sys.mjs @@ -0,0 +1,122 @@ +/* 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; + } +} |