maplibre-gl
Version:
BSD licensed community fork of mapbox-gl, a WebGL interactive maps library
224 lines (193 loc) • 7.7 kB
text/typescript
import {extend, type Subscription} from './util.ts';
/**
* A listener method used as a callback to events
*/
export type Listener<E extends Event = Event> = (event: E) => any;
/**
* A mapping between event names and the event each of them carries.
*/
export type EventTypeMap = Record<string, Event>;
/**
* The event names of an {@link EventTypeMap}.
*/
export type EventNames<EventType extends EventTypeMap> = Extract<keyof EventType, string>;
/**
* Properties merged into every event bubbled up to an evented parent.
*/
export type EventedParentData = Record<string, unknown>;
type Listeners<EventType extends EventTypeMap> = {[K in keyof EventType]?: Array<Listener<EventType[K]>>};
function _addEventListener<T extends EventTypeMap, K extends EventNames<T>>(type: K, listener: Listener<T[K]>, listenerList: Listeners<T>) {
const listenerExists = listenerList[type]?.includes(listener);
if (!listenerExists) {
listenerList[type] ||= [];
listenerList[type].push(listener);
}
}
function _removeEventListener<T extends EventTypeMap, K extends EventNames<T>>(type: K, listener: Listener<T[K]>, listenerList: Listeners<T>) {
if (listenerList?.[type]) {
const index = listenerList[type].indexOf(listener);
if (index !== -1) {
listenerList[type].splice(index, 1);
}
}
}
/**
* The event class
*/
export class Event<TType extends string = string> {
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<EventType extends EventTypeMap = EventTypeMap> {
_listeners?: Listeners<EventType>;
_oneTimeListeners?: Listeners<EventType>;
_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<T extends EventNames<EventType>>(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<T extends EventNames<EventType>>(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<T extends EventNames<EventType>>(type: T): Promise<EventType[T]>;
/**
* 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<T extends EventNames<EventType>>(type: T, listener: (event: EventType[T]) => void): this;
once<T extends EventNames<EventType>>(type: T, listener?: (event: EventType[T]) => void): this | Promise<EventType[T]> {
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<EventType>]): 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<EventType>, properties?: object): this;
fire(event: EventType[EventNames<EventType>] | EventNames<EventType>, properties?: object): this {
const firedEvent: Event = typeof event === 'string' ? new Event(event, properties || {}) : event;
const type = firedEvent.type as EventNames<EventType>;
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<EventType>): 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;
}
}