@haxiomic/event-signal
Version:
A single event emitter with priority support for TypeScript
119 lines (101 loc) • 3.81 kB
text/typescript
/**
* 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) {
let eventObj = {
priority,
listener,
remove: () => 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()
// enumerate listeners, highest priority first
for (let i = 0; i < this.listeners.length; i++) {
let item = this.listeners[i]
if (item.priority > maxPriority) continue; // skip
this.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 = ReturnType<EventSignal['addListener']>
}