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 }