108 lines
4.0 KiB
JavaScript
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;
|
|
}
|