import {extend, type Subscription} from './util.ts'; /** * A listener method used as a callback to events */ export type Listener = (event: E) => any; /** * A mapping between event names and the event each of them carries. */ export type EventTypeMap = Record; /** * The event names of an {@link EventTypeMap}. */ export type EventNames = Extract; /** * Properties merged into every event bubbled up to an evented parent. */ export type EventedParentData = Record; type Listeners = {[K in keyof EventType]?: Array>}; function _addEventListener>(type: K, listener: Listener, listenerList: Listeners) { const listenerExists = listenerList[type]?.includes(listener); if (!listenerExists) { listenerList[type] ||= []; listenerList[type].push(listener); } } function _removeEventListener>(type: K, listener: Listener, listenerList: Listeners) { if (listenerList?.[type]) { const index = listenerList[type].indexOf(listener); if (index !== -1) { listenerList[type].splice(index, 1); } } } /** * The event class */ export class Event { readonly type: TType; /** * The object that fired the event. Set when the event is fired, and narrowed to a more * specific type (e.g. `Map`, `Marker`) by the event subclasses. */ target?: unknown; constructor(type: TType, data: object = {}) { extend(this, data); this.type = type; } } type ErrorLike = { message: string; }; /** * An error event */ export class ErrorEvent extends Event<'error'> { error: ErrorLike; constructor(error: ErrorLike, data: object = {}) { super('error', extend({error}, data)); } } /** * The event map of an {@link Evented} that only reports errors. */ export type ErrorEventType = { error: ErrorEvent; }; /** * Methods mixed in to other classes for event capabilities. * * @group Event Related */ export abstract class Evented { _listeners?: Listeners; _oneTimeListeners?: Listeners; _eventedParent?: Evented; _eventedParentData?: EventedParentData | (() => EventedParentData); /** * Adds a listener to a specified event type. * * @param type - The event type to add a listen for. * @param listener - The function to be called when the event is fired. * The listener function is called with the data object passed to `fire`, * extended with `target` and `type` properties. */ on>(type: T, listener: (event: EventType[T]) => void): Subscription { this._listeners ||= {}; _addEventListener(type, listener, this._listeners); return { unsubscribe: () => { this.off(type, listener); } }; } /** * Removes a previously registered event listener. * * @param type - The event type to remove listeners for. * @param listener - The listener function to remove. */ off>(type: T, listener: (event: EventType[T]) => void): this { _removeEventListener(type, listener, this._listeners); _removeEventListener(type, listener, this._oneTimeListeners); return this; } /** * Adds a listener that will be called only once to a specified event type. * * The listener will be called first time the event fires after the listener is registered. * * @param type - The event type to listen for. * @returns a promise that resolves with the event */ once>(type: T): Promise; /** * Adds a listener that will be called only once to a specified event type. * * The listener will be called first time the event fires after the listener is registered. * * @param type - The event type to listen for. * @param listener - The function to be called when the event is fired the first time. * @returns `this` when a listener is provided */ once>(type: T, listener: (event: EventType[T]) => void): this; once>(type: T, listener?: (event: EventType[T]) => void): this | Promise { if (!listener) { return new Promise((resolve) => this.once(type, resolve)); } this._oneTimeListeners ||= {}; _addEventListener(type, listener, this._oneTimeListeners); return this; } /** * Calls every listener registered for the event's type. */ fire(event: EventType[EventNames]): this; /** * Compatibility with the (type: string, properties: Object) signature from previous versions. * See https://github.com/mapbox/mapbox-gl-js/issues/6522, * https://github.com/mapbox/mapbox-gl-draw/issues/766 */ fire(type: EventNames, properties?: object): this; fire(event: EventType[EventNames] | EventNames, properties?: object): this { const firedEvent: Event = typeof event === 'string' ? new Event(event, properties || {}) : event; const type = firedEvent.type as EventNames; if (this.listens(type)) { firedEvent.target = this; // make sure adding or removing listeners inside other listeners won't cause an infinite loop const listeners = this._listeners?.[type]?.slice() ?? []; for (const listener of listeners) { listener.call(this, firedEvent); } const oneTimeListeners = this._oneTimeListeners?.[type]?.slice() ?? []; for (const listener of oneTimeListeners) { _removeEventListener(type, listener, this._oneTimeListeners); listener.call(this, firedEvent); } const parent = this._eventedParent; if (parent) { extend( firedEvent, typeof this._eventedParentData === 'function' ? this._eventedParentData() : this._eventedParentData ); parent.fire(firedEvent); } // To ensure that no error events are dropped, print them to the // console if they have no listeners. } else if (firedEvent instanceof ErrorEvent) { console.error(firedEvent.error); } return this; } /** * Returns a true if this instance of Evented or any forwardeed instances of Evented have a listener for the specified type. * * @param type - The event type * @returns `true` if there is at least one registered listener for specified event type, `false` otherwise */ listens(type: EventNames): boolean { return Boolean( this._listeners?.[type]?.length || this._oneTimeListeners?.[type]?.length || this._eventedParent?.listens(type) ); } /** * Bubble all events fired by this instance of Evented to this parent instance of Evented. */ setEventedParent(parent?: Evented | null, data?: EventedParentData | (() => EventedParentData)): this { this._eventedParent = parent; this._eventedParentData = data; return this; } }