tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

PageEventManager.sys.mjs (5181B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 const lazy = {};
      5 ChromeUtils.defineESModuleGetters(lazy, {
      6  EveryWindow: "resource:///modules/EveryWindow.sys.mjs",
      7 });
      8 /**
      9 * Methods for setting up and tearing down page event listeners. These are used
     10 * to dismiss Feature Callouts when the callout's anchor element is clicked.
     11 */
     12 export class PageEventManager {
     13  /**
     14   * A set of parameters defining a page event listener.
     15   *
     16   * @typedef {object} PageEventListenerParams
     17   * @property {string} type Event type string e.g. `click`
     18   * @property {string} [selectors] Target selector, e.g. `tag.class, #id[attr]`
     19   * @property {PageEventListenerOptions} [options] addEventListener options
     20   *
     21   * @typedef {object} PageEventListenerOptions
     22   * @property {boolean} [capture] Use event capturing phase?
     23   * @property {boolean} [once] Remove listener after first event?
     24   * @property {boolean} [preventDefault] Inverted value for `passive` option
     25   * @property {number} [interval] Used only for `timeout` and `interval` event
     26   *   types. These don't set up real event listeners, but instead invoke the
     27   *   action on a timer.
     28   *
     29   * @typedef {object} PageEventListener
     30   * @property {Function} callback Function to call when event is triggered
     31   * @property {AbortController} controller Handle for aborting the listener
     32   *
     33   * @typedef {object} PageEvent
     34   * @property {string} type Event type string e.g. `click`
     35   * @property {Element} [target] Event target
     36   */
     37 
     38  /**
     39   * Maps event listener params to their PageEventListeners, so they can be
     40   * called and cancelled.
     41   *
     42   * @type {Map<PageEventListenerParams, PageEventListener>}
     43   */
     44  _listeners = new Map();
     45 
     46  /**
     47   * @param {Window} win Window containing the document to listen to
     48   */
     49  constructor(win) {
     50    this.win = win;
     51    this.doc = win.document;
     52  }
     53 
     54  /**
     55   * Adds a page event listener.
     56   *
     57   * @param {PageEventListenerParams} params
     58   * @param {Function} callback Function to call when event is triggered
     59   */
     60  on(params, callback) {
     61    if (this._listeners.has(params)) {
     62      return;
     63    }
     64    const { type, selectors, options = {} } = params;
     65    const listener = { callback };
     66    if (selectors) {
     67      const controller = new AbortController();
     68      const opt = {
     69        capture: !!options.capture,
     70        passive: !options.preventDefault,
     71        signal: controller.signal,
     72      };
     73      if (options?.every_window) {
     74        // Using unique ID for each listener in case there are multiple
     75        // listeners with the same type
     76        let uuid = Services.uuid.generateUUID().number;
     77        lazy.EveryWindow.registerCallback(
     78          uuid,
     79          win => {
     80            for (const target of win.document.querySelectorAll(selectors)) {
     81              target.addEventListener(type, callback, opt);
     82            }
     83          },
     84          win => {
     85            for (const target of win.document.querySelectorAll(selectors)) {
     86              target.removeEventListener(type, callback, opt);
     87            }
     88          }
     89        );
     90        listener.uninit = () => lazy.EveryWindow.unregisterCallback(uuid, true);
     91      } else {
     92        for (const target of this.doc.querySelectorAll(selectors)) {
     93          target.addEventListener(type, callback, opt);
     94        }
     95      }
     96      listener.controller = controller;
     97    } else if (["timeout", "interval"].includes(type) && options.interval) {
     98      let interval;
     99      const abort = () => this.win.clearInterval(interval);
    100      const onInterval = () => {
    101        callback({ type, target: type });
    102        if (type === "timeout") {
    103          abort();
    104        }
    105      };
    106      interval = this.win.setInterval(onInterval, options.interval);
    107      listener.callback = onInterval;
    108      listener.controller = { abort };
    109    }
    110    this._listeners.set(params, listener);
    111  }
    112 
    113  /**
    114   * Removes a page event listener.
    115   *
    116   * @param {PageEventListenerParams} params
    117   */
    118  off(params) {
    119    const listener = this._listeners.get(params);
    120    if (!listener) {
    121      return;
    122    }
    123    listener.uninit?.();
    124    listener.controller?.abort();
    125    this._listeners.delete(params);
    126  }
    127 
    128  /**
    129   * Adds a page event listener that is removed after the first event.
    130   *
    131   * @param {PageEventListenerParams} params
    132   * @param {Function} callback Function to call when event is triggered
    133   */
    134  once(params, callback) {
    135    const wrappedCallback = (...args) => {
    136      this.off(params);
    137      callback(...args);
    138    };
    139    this.on(params, wrappedCallback);
    140  }
    141 
    142  /**
    143   * Removes all page event listeners.
    144   */
    145  clear() {
    146    for (const listener of this._listeners.values()) {
    147      listener.uninit?.();
    148      listener.controller?.abort();
    149    }
    150    this._listeners.clear();
    151  }
    152 
    153  /**
    154   * Calls matching page event listeners. A way to dispatch a "fake" event.
    155   *
    156   * @param {PageEvent} event
    157   */
    158  emit(event) {
    159    for (const [params, listener] of this._listeners) {
    160      if (params.type === event.type) {
    161        listener.callback(event);
    162      }
    163    }
    164  }
    165 }