UNPKG

@converse/skeletor

Version:

Models and Collections for modern web apps

173 lines (148 loc) 6.07 kB
/** * @copyright 2010-2019 Jeremy Ashkenas and DocumentCloud * @copyright 2023 JC Brand */ import isEmpty from 'lodash-es/isEmpty'; import keys from 'lodash-es/keys'; import uniqueId from 'lodash-es/uniqueId'; import Listening from './listening'; import { eventsApi, onApi, offApi, onceMap, tryCatchOn, triggerApi } from './utils/events'; import { ClassConstructor, EventCallback, EventCallbackMap, EventContext, EventHandlersMap, EventListenerMap, ObjectListenedTo, } from './types'; // A private global variable to share between listeners and listenees. let _listening: Listening | undefined; /** * @public */ export function EventEmitter<T extends ClassConstructor>(Base: T) { return class EventEmitter extends Base implements EventEmitter { _events?: EventHandlersMap; _listeners?: EventListenerMap; _listeningTo?: EventListenerMap; _listenId?: string; /** * Bind an event to a `callback` function. Passing `"all"` will bind * the callback to all events fired. */ on(name: string | EventCallbackMap, callback?: EventCallback | EventContext, context?: EventContext): this { this._events = eventsApi(onApi, this._events || {}, name, callback, { context: context, ctx: this, listening: _listening, }) as EventHandlersMap; if (_listening) { const listeners = this._listeners || (this._listeners = {}); listeners[_listening.id] = _listening; // Allow the listening to use a counter, instead of tracking // callbacks for library interop _listening.interop = false; } return this; } /** * Inversion-of-control versions of `on`. Tell *this* object to listen to * an event in another object... keeping track of what it's listening to * for easier unbinding later. */ listenTo(obj: ObjectListenedTo, name: string | EventCallbackMap, callback?: EventCallback): this { if (!obj) return this; const id = obj._listenId || (obj._listenId = uniqueId('l')); const listeningTo = this._listeningTo || (this._listeningTo = {}); let listening = (_listening = listeningTo[id]); // This object is not listening to any other events on `obj` yet. // Setup the necessary references to track the listening callbacks. if (!listening) { this._listenId || (this._listenId = uniqueId('l')); listening = _listening = listeningTo[id] = new Listening(this, obj); } // Bind callbacks on obj. const error = tryCatchOn(obj, name, callback, this); _listening = undefined; if (error) throw error; // If the target obj is not Backbone.Events, track events manually. if (listening.interop) listening.start(name, callback, this, _listening); return this; } /** * Remove one or many callbacks. If `context` is null, removes all * callbacks with that function. If `callback` is null, removes all * callbacks for the event. If `name` is null, removes all bound * callbacks for all events. */ off( name?: string | EventCallbackMap | null, callback?: EventCallback | EventContext | null, context?: EventContext ): this { if (!this._events) return this; this._events = eventsApi(offApi, this._events, name, callback, { context: context, listeners: this._listeners, }) as EventHandlersMap; return this; } /** * Tell this object to stop listening to either specific events ... or * to every object it's currently listening to. */ stopListening(obj?: any, name?: string | EventCallbackMap, callback?: EventCallback): this { const listeningTo = this._listeningTo; if (!listeningTo) return this; const ids = obj ? [obj._listenId] : keys(listeningTo); for (let i = 0; i < ids.length; i++) { const listening = listeningTo[ids[i]]; // If listening doesn't exist, this object is not currently // listening to obj. Break out early. if (!listening) break; listening.obj.off(name, callback, this); if (listening.interop) listening.stop(name, callback); } if (isEmpty(listeningTo)) this._listeningTo = undefined; return this; } /** * Bind an event to only be triggered a single time. After the first time * the callback is invoked, its listener will be removed. If multiple events * are passed in using the space-separated syntax, the handler will fire * once for each event, not once for a combination of all events. */ once(name: string | EventCallbackMap, callback?: EventCallback | EventContext, context?: EventContext): this { // Map the event into a `{event: once}` object. const events = eventsApi(onceMap, {}, name, callback, this.off.bind(this)); if (typeof name === 'string' && (context === null || context === undefined)) callback = undefined; return this.on(events as EventCallbackMap, callback, context); } /** * Inversion-of-control versions of `once`. */ listenToOnce(obj: any, name: string | EventCallbackMap, callback?: EventCallback): this { // Map the event into a `{event: once}` object. const events = eventsApi(onceMap, {}, name, callback, this.stopListening.bind(this, obj)); return this.listenTo(obj, events as string | EventCallbackMap); } /** * Trigger one or many events, firing all bound callbacks. Callbacks are * passed the same arguments as `trigger` is, apart from the event name * (unless you're listening on `"all"`, which will cause your callback to * receive the true name of the event as the first argument). */ trigger(name: string, ...args: any[]): this { if (!this._events) return this; eventsApi(triggerApi, this._events, name, undefined, args); return this; } }; } /** * @public */ const EventEmitterObject = EventEmitter(Object); export { EventEmitterObject }; export default EventEmitter;