@converse/skeletor
Version:
Models and Collections for modern web apps
173 lines (148 loc) • 6.07 kB
text/typescript
/**
* @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;