summaryrefslogtreecommitdiffstats
path: root/browser/components/asrouter/modules/PageEventManager.sys.mjs
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/asrouter/modules/PageEventManager.sys.mjs')
-rw-r--r--browser/components/asrouter/modules/PageEventManager.sys.mjs135
1 files changed, 135 insertions, 0 deletions
diff --git a/browser/components/asrouter/modules/PageEventManager.sys.mjs b/browser/components/asrouter/modules/PageEventManager.sys.mjs
new file mode 100644
index 0000000000..44f1293385
--- /dev/null
+++ b/browser/components/asrouter/modules/PageEventManager.sys.mjs
@@ -0,0 +1,135 @@
+/* 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/. */
+
+/**
+ * Methods for setting up and tearing down page event listeners. These are used
+ * to dismiss Feature Callouts when the callout's anchor element is clicked.
+ */
+export class PageEventManager {
+ /**
+ * A set of parameters defining a page event listener.
+ * @typedef {Object} PageEventListenerParams
+ * @property {String} type Event type string e.g. `click`
+ * @property {String} [selectors] Target selector, e.g. `tag.class, #id[attr]`
+ * @property {PageEventListenerOptions} [options] addEventListener options
+ *
+ * @typedef {Object} PageEventListenerOptions
+ * @property {Boolean} [capture] Use event capturing phase?
+ * @property {Boolean} [once] Remove listener after first event?
+ * @property {Boolean} [preventDefault] Inverted value for `passive` option
+ * @property {Number} [interval] Used only for `timeout` and `interval` event
+ * types. These don't set up real event listeners, but instead invoke the
+ * action on a timer.
+ *
+ * @typedef {Object} PageEventListener
+ * @property {Function} callback Function to call when event is triggered
+ * @property {AbortController} controller Handle for aborting the listener
+ *
+ * @typedef {Object} PageEvent
+ * @property {String} type Event type string e.g. `click`
+ * @property {Element} [target] Event target
+ */
+
+ /**
+ * Maps event listener params to their PageEventListeners, so they can be
+ * called and cancelled.
+ * @type {Map<PageEventListenerParams, PageEventListener>}
+ */
+ _listeners = new Map();
+
+ /**
+ * @param {Window} win Window containing the document to listen to
+ */
+ constructor(win) {
+ this.win = win;
+ this.doc = win.document;
+ }
+
+ /**
+ * Adds a page event listener.
+ * @param {PageEventListenerParams} params
+ * @param {Function} callback Function to call when event is triggered
+ */
+ on(params, callback) {
+ if (this._listeners.has(params)) {
+ return;
+ }
+ const { type, selectors, options = {} } = params;
+ const listener = { callback };
+ if (selectors) {
+ const controller = new AbortController();
+ const opt = {
+ capture: !!options.capture,
+ passive: !options.preventDefault,
+ signal: controller.signal,
+ };
+ const targets = this.doc.querySelectorAll(selectors);
+ for (const target of targets) {
+ target.addEventListener(type, callback, opt);
+ }
+ listener.controller = controller;
+ } else if (["timeout", "interval"].includes(type) && options.interval) {
+ let interval;
+ const abort = () => this.win.clearInterval(interval);
+ const onInterval = () => {
+ callback({ type, target: type });
+ if (type === "timeout") {
+ abort();
+ }
+ };
+ interval = this.win.setInterval(onInterval, options.interval);
+ listener.callback = onInterval;
+ listener.controller = { abort };
+ }
+ this._listeners.set(params, listener);
+ }
+
+ /**
+ * Removes a page event listener.
+ * @param {PageEventListenerParams} params
+ */
+ off(params) {
+ const listener = this._listeners.get(params);
+ if (!listener) {
+ return;
+ }
+ listener.controller?.abort();
+ this._listeners.delete(params);
+ }
+
+ /**
+ * Adds a page event listener that is removed after the first event.
+ * @param {PageEventListenerParams} params
+ * @param {Function} callback Function to call when event is triggered
+ */
+ once(params, callback) {
+ const wrappedCallback = (...args) => {
+ this.off(params);
+ callback(...args);
+ };
+ this.on(params, wrappedCallback);
+ }
+
+ /**
+ * Removes all page event listeners.
+ */
+ clear() {
+ for (const listener of this._listeners.values()) {
+ listener.controller?.abort();
+ }
+ this._listeners.clear();
+ }
+
+ /**
+ * Calls matching page event listeners. A way to dispatch a "fake" event.
+ * @param {PageEvent} event
+ */
+ emit(event) {
+ for (const [params, listener] of this._listeners) {
+ if (params.type === event.type) {
+ listener.callback(event);
+ }
+ }
+ }
+}