/** * Описание пользовательского обработчика события. */ export type TEventListener = { fn: (event: Event) => void; }; /** * Данные слушателей события для конкретного элемента. */ export type TEventListenerData = { options: TEventListenerOptions; isListening: boolean; callbacks: TEventListener[]; handler?: EventListener; }; /** * Параметры подписки DOM-слушателя. */ export type TEventListenerOptions = AddEventListenerOptions | null; /** * Набор событий для пакетного заполнения шины. */ export type TEventListeners = Record; /** * Основное хранилище подписок Event Bus. */ export type TEventStore = Map>; /** * Event Bus. Шина для хранения и управления событиями. * @module EventBus */ export class EventBus { store: TEventStore; constructor() { this.store = new Map(); } #removeDomListener(eventName: string, el: EventTarget, data: TEventListenerData) { if (data.handler) { el.removeEventListener(eventName, data.handler, data.options || undefined); } data.isListening = false; data.handler = undefined; } #addEventListener(eventName: string, el?: EventTarget, callback?: TEventListener["fn"]) { const isExist = this.isEventExist(eventName); const add = (el: EventTarget, data: TEventListenerData) => { if (!data.isListening) { const handler = this.#handleEvent.bind(this); el.addEventListener(eventName, handler, data.options || undefined); data.isListening = true; data.handler = handler; } }; if (isExist) { const eventData = this.getMapByEventName(eventName); if (!eventData) { return; } eventData.forEach((value, elem) => { if (el) { if (el === elem) { if (callback) { value.callbacks.forEach(({ fn }: TEventListener) => { if (fn === callback) { add(elem, value); } }); } else { add(elem, value); } } } else { add(elem, value); } }); } } #removeEventListener(eventName: string, el?: EventTarget, callback?: TEventListener["fn"]) { const isExist = this.isEventExist(eventName); const remove = (el: EventTarget, data: TEventListenerData) => this.#removeDomListener(eventName, el, data); if (isExist) { const eventData = this.getMapByEventName(eventName); if (!eventData) { return; } eventData.forEach((value, elem) => { if (el) { if (elem === el) { if (callback) { value.callbacks.forEach(({ fn }: TEventListener) => { if (fn === callback) { remove(el, value); } }); } else { remove(el, value); } } } else { remove(elem, value); } }); } } /** * Подписывается на прослушивание события * @param eventName{String=} название события * @param el{Node|Element|HTMLElement} элемент * @param callback{Function=} обработчик */ subscribe(eventName = "", el?: EventTarget, callback?: TEventListener["fn"]) { if (eventName) { const isExist = this.isEventExist(eventName); if (isExist) { this.#addEventListener(eventName, el, callback); } } else { this.store.forEach((_value, eventName) => { this.#addEventListener(eventName, el, callback); }); } } #handleEvent(e: Event) { const currentEl = e.currentTarget || e.target; if (currentEl) { return this.publish(e.type, currentEl, undefined, e); } } /** * Возвращает карту данных для события их хранилища по его имени * @param eventName{String} название события * @returns {Map|undefined} */ getMapByEventName(eventName: string): Map | undefined { return this.store.get(eventName); } /** * Отменяет подписку на событие * @param eventName{String=} название события * @param el{Node|Element|HTMLElement=} элемент * @param callback{Function=} обработчик */ unsubscribe(eventName = "", el?: EventTarget, callback?: TEventListener["fn"]) { if (eventName) { const isExist = this.isEventExist(eventName); if (isExist) { this.#removeEventListener(eventName, el, callback); } } else { this.store.forEach((_value, eventName) => { this.#removeEventListener(eventName, el, callback); }); } } /** * Добавляет событие в хранилище * @param eventName{String} название события * @param data{{ fn: Function, el: Node|Element|HTMLElement, options: EventListenerOptions=, isListening: Boolean= }} данные */ add(eventName = "", data: { fn?: TEventListener["fn"]; el?: EventTarget; options?: AddEventListenerOptions | null; isListening?: boolean; } = {}) { const { fn, el, options = null, isListening = false, } = data; if (!fn || !el) { return; } if (this.isEventExist(eventName)) { const currentEvent = this.getMapByEventName(eventName); if (!currentEvent) { return; } if (currentEvent.has(el)) { const currentElListeners = currentEvent.get(el); if (currentElListeners) { if (!currentElListeners.callbacks.find((listener: TEventListener) => { return listener.fn === fn; })) { currentElListeners.callbacks.push({ fn }); } } } else { currentEvent.set(el, { options, isListening, callbacks: [ { fn } ], }); } } else { this.store.set(eventName, new Map([ [ el, { options, isListening, callbacks: [ { fn } ], } ] ])); } } #deleteFromStore(eventName: string) { this.store.delete(eventName); } /** * Возвращает результат проверки существования события в хранилище по имени * @param eventName{String} название события * @returns {boolean} */ isEventExist(eventName = "") { return this.store.has(eventName); } /** * Исполняет событие, вызывая его обработчик * @param eventName{String=} название события * @param el{Node|Element|HTMLElement=} элемент * @param callback{Function=} обработчик * @param args{Event=} объект события */ publish(eventName?: string, el?: EventTarget, callback?: TEventListener["fn"], args?: Event) { if (eventName) { if (this.isEventExist(eventName)) { const caller = ({ currentEl, callbacks = [], filter = (_item: TEventListener) => true, options, }: { currentEl: EventTarget; callbacks?: TEventListener[]; filter?: (item: TEventListener) => boolean; options?: TEventListenerOptions; }) => { const eventArg = args || new Event(eventName); const filteredCallbacks = callbacks.filter(filter); filteredCallbacks.forEach(({ fn }: TEventListener) => fn(eventArg)); if (options?.once && filteredCallbacks.length) { if (callback) { this.clear(eventName, currentEl, callback); } else { this.clear(eventName, currentEl); } } }; const eventData = this.getMapByEventName(eventName); if (!eventData) { return; } eventData.forEach(({ callbacks, options }, elem) => { if (el) { if (el === elem) { if (callback) { caller({ currentEl: elem, options, callbacks, filter: ({ fn }: TEventListener) => fn === callback, }); } else { caller({ currentEl: elem, options, callbacks }); } } } else { caller({ currentEl: elem, options, callbacks }); } }); } } } /** * Заполняет хранилище данными из объекта * @param el{Node|Element|HTMLElement=} элемент * @param listeners{Object} объект событий */ fill(el?: EventTarget, listeners: TEventListeners = {}) { if (!el) { return; } Object.entries(listeners) .forEach(([ eventName, params ]) => { if (eventName && params) { const handlerData = typeof params === "function" ? { fn: params, el } : { fn: params.fn, options: params.options, el, }; this.add(eventName, handlerData); } }); } /** * Очищает хранилище от событий * @param eventName{String=} название события * @param el{Node|Element|HTMLElement=} элемент * @param callback{Function=} обработчик */ clear(eventName?: string, el?: EventTarget, callback?: TEventListener["fn"]) { if (eventName) { if (this.isEventExist(eventName)) { const eventData = this.getMapByEventName(eventName); if (!eventData) { return; } if (el) { const currentEvent = eventData.get(el); if (!currentEvent) { return; } if (callback) { currentEvent.callbacks = currentEvent.callbacks.filter(({ fn }: TEventListener) => fn !== callback); if (!currentEvent.callbacks.length) { this.#removeDomListener(eventName, el, currentEvent); eventData.delete(el); } } else { this.#removeDomListener(eventName, el, currentEvent); eventData.delete(el); } } else { this.unsubscribe(eventName); eventData.clear(); } if (!eventData.size) { this.#deleteFromStore(eventName); } } } else { this.unsubscribe(); this.store.clear(); } } }