UNPKG

@haxiomic/event-signal

Version:

A single event emitter with priority support for TypeScript

132 lines (113 loc) 4.42 kB
/** * Event emitter with a notion of explicit ordering via priority */ export class EventSignal<Payload = undefined, E = EventSignal.Emitted<Payload>> { private listeners = new Array<{ priority: number listener: (event: E) => void }>() public addListener(listener: (event: E) => void, priority: number = 0): EventSignal.Listener { const eventObj = { priority, listener, remove: () => this.removeListener(listener), removeOnSignal: (signal: EventSignal<any, any>) => { return signal.once(() => this.removeListener(listener)); } } if (listener !== null) { this.listeners.push(eventObj) } return eventObj } /** Alias for addListener */ public on(listener: (event: E) => void, priority?: number) { return this.addListener(listener, priority); } public removeListener(listener: (event: E) => void) { // remove listener from array let i = 0 for (; i < this.listeners.length; i++) { if (this.listeners[i].listener === listener) { break } } this.listeners.splice(i, 1) } public once(listener: (event: E) => void, priority: number = 0) { const tempListener = (event: E) => { listener(event) this.removeListener(tempListener) } return this.addListener(tempListener, priority) } /** * Dispatch an event by providing a payload * The underlying event object will be created and populated with the payload * * @param maxPriority If provided, only listeners with a priority equal or lower than this will be called */ public dispatch(payload: Payload, maxPriority?: number) { if (this.listeners.length === 0) return return this.dispatchWithExistingEvent(payload, maxPriority) } /** * Dispatch an event with an existing event object * * This is useful if you want to forward an event from another source like a DOM event */ public dispatchWithExistingEvent(payload: Payload, maxPriority: number = Infinity) { if (this.listeners.length === 0) return let event = this.patchPayload(payload); // sort listeners by priority before dispatch (priority can change at runtime) this.sortPriorityDescending() // clone listeners to avoid issues with removing listeners while iterating let listeners = this.listeners.slice(0); // enumerate listeners, highest priority first for (let i = 0; i < listeners.length; i++) { let item = listeners[i] if (item.priority > maxPriority) continue; // skip listeners[i].listener(event as any) // stop propagation if event was prevented if (typeof event === 'object' && (event as any).propagationStopped) { return } } } public hasListeners() { return this.listeners.length > 0 } private sortPriorityDescending() { this.listeners.sort((a, b) => b.priority - a.priority) } private patchPayload(payload: Payload): E { // patch event object to support `propagationStopped` if (payload instanceof Event && (payload as any).propagationStopped === undefined) { (payload as any).propagationStopped = false; (payload as any)._stopPropagation = payload.stopPropagation; payload.stopPropagation = stopPropagationOverride; } return payload as any; } } function stopPropagationOverride(this: Event) { (this as any).propagationStopped = true; (this as any)._stopPropagation(); } export namespace EventSignal { // we patch events to support `propagationStopped` export type Emitted<T> = T extends Event ? T & { propagationStopped?: boolean // _stopPropagation?: () => void } : T; export type Listener = { /** Priority of the listener, higher numbers indicate higher priority, this is allowed to change at runtime */ priority: number listener: (event: any) => void remove: () => void /** Removes the listener when the provided signal is dispatched */ removeOnSignal: (signal: EventSignal<any, any>) => void } }