summaryrefslogtreecommitdiffstats
path: root/browser/components/asrouter/modules/PageEventManager.sys.mjs
blob: 44f1293385dcc7712dfdc8783f6e80b84f2e5a6c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
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);
      }
    }
  }
}