Files
Foundry-VTT-Docker/resources/app/common/utils/event-emitter.mjs
2025-01-04 00:34:03 +01:00

108 lines
4.0 KiB
JavaScript

/**
* @typedef {import("../types.mjs").Constructor} Constructor
*/
/**
* @callback EmittedEventListener
* @param {Event} event The emitted event
* @returns {any}
*/
/**
* Augment a base class with EventEmitter behavior.
* @template {Constructor} BaseClass
* @param {BaseClass} BaseClass Some base class augmented with event emitter functionality
*/
export default function EventEmitterMixin(BaseClass) {
/**
* A mixin class which implements the behavior of EventTarget.
* This is useful in cases where a class wants EventTarget-like behavior but needs to extend some other class.
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget
*/
class EventEmitter extends BaseClass {
/**
* An array of event types which are valid for this class.
* @type {string[]}
*/
static emittedEvents = [];
/**
* A mapping of registered events.
* @type {Record<string, Map<EmittedEventListener, {fn: EmittedEventListener, once: boolean}>>}
*/
#events = {};
/* -------------------------------------------- */
/**
* Add a new event listener for a certain type of event.
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener
* @param {string} type The type of event being registered for
* @param {EmittedEventListener} listener The listener function called when the event occurs
* @param {object} [options={}] Options which configure the event listener
* @param {boolean} [options.once=false] Should the event only be responded to once and then removed
*/
addEventListener(type, listener, {once = false} = {}) {
if ( !this.constructor.emittedEvents.includes(type) ) {
throw new Error(`"${type}" is not a supported event of the ${this.constructor.name} class`);
}
this.#events[type] ||= new Map();
this.#events[type].set(listener, {fn: listener, once});
}
/* -------------------------------------------- */
/**
* Remove an event listener for a certain type of event.
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener
* @param {string} type The type of event being removed
* @param {EmittedEventListener} listener The listener function being removed
*/
removeEventListener(type, listener) {
this.#events[type]?.delete(listener);
}
/* -------------------------------------------- */
/**
* Dispatch an event on this target.
* @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent
* @param {Event} event The Event to dispatch
* @returns {boolean} Was default behavior for the event prevented?
*/
dispatchEvent(event) {
if ( !(event instanceof Event) ) {
throw new Error("EventEmitter#dispatchEvent must be provided an Event instance");
}
if ( !this.constructor.emittedEvents.includes(event?.type) ) {
throw new Error(`"${event.type}" is not a supported event of the ${this.constructor.name} class`);
}
const listeners = this.#events[event.type];
if ( !listeners ) return true;
// Extend and configure the Event
Object.defineProperties(event, {
target: {value: this},
stopPropagation: {value: function() {
event.propagationStopped = true;
Event.prototype.stopPropagation.call(this);
}},
stopImmediatePropagation: {value: function() {
event.propagationStopped = true;
Event.prototype.stopImmediatePropagation.call(this);
}}
});
// Call registered listeners
for ( const listener of listeners.values() ) {
listener.fn(event);
if ( listener.once ) this.removeEventListener(event.type, listener.fn);
if ( event.propagationStopped ) break;
}
return event.defaultPrevented;
}
}
return EventEmitter;
}