/** * @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>} */ #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; }