import { getDocument } from "ssr-window"; import { onContentLoaded } from "../../utils/onContentLoaded"; /** * Конфигурация диспетчера. */ export type TDispatcherCfg = { eventContentProp: string; isInitiatorValid: (params: TInitiatorValidationArg) => boolean; isDebug: boolean; }; /** * Базовая структура элемента коллекции для диспетчеризации. */ export type TDispatcherCollectionItem = { instance: Element; }; /** * Контекст, в пределах которого выполняется инициатор. */ export type TDispatcherContext = Document | HTMLElement; /** * Функция получения коллекции экземпляров. */ export type TDispatcherGetCollection = () => TDispatcherCollectionItem[]; /** * Функция-инициатор, запускаемая для контекста. */ export type TDispatcherInitiatorFn = (context: TDispatcherContext) => void; /** * Значение записи инициатора в карте диспетчера. */ export type TDispatcherInitiatorMapValue = { initiator: TDispatcherInitiatorFn; getCollection?: TDispatcherGetCollection; }; /** * Тип динамически вызываемого метода экземпляра. */ export type TDispatcherMethod = (...args: never[]) => unknown; /** * Аргументы валидации инициатора перед запуском. */ export type TInitiatorValidationArg = { initiatorFn: TDispatcherInitiatorFn; eventContentField: TDispatcherContext; event: CustomEvent>; initiators: Map; }; function hasMethod( value: T, key: K ): value is T & Record { const method = (value as Record)[key]; return typeof method === "function"; } /** * Диспетчер для управления инициаторами плагинов и их вызовом при загрузке контента. Singleton. * @module Dispatcher */ class Dispatcher { static #instance: Dispatcher | null = null; eventContentProp: string; isInitiatorValid: TDispatcherCfg["isInitiatorValid"]; isDebug: boolean; /** * Реестр инициаторов по селектору. * Ключ — селектор, значение — функция инициализации и опциональная коллекция. * @type {Map} */ static initiators = new Map(); /** * Признак регистрации плагина в глобальном namespace Core. * @type {boolean} */ static isApp = true; /** * Конфигурация Dispatcher по умолчанию. * @type {TDispatcherCfg} */ static defaultCfg: TDispatcherCfg = { eventContentProp: "content", isInitiatorValid: ({ initiatorFn, eventContentField }: TInitiatorValidationArg) => typeof initiatorFn === "function" && !!eventContentField, isDebug: false, }; constructor(cfg: Partial = {}) { if (Dispatcher.#instance) { return Dispatcher.#instance; } const { eventContentProp, isInitiatorValid, isDebug } = { ...Dispatcher.defaultCfg, ...cfg }; this.eventContentProp = eventContentProp; this.isInitiatorValid = isInitiatorValid.bind(this); this.isDebug = isDebug; this.#bindEvents(); Dispatcher.#instance = this; } #bindEvents() { onContentLoaded>((e: CustomEvent>) => this.#onContentLoadedCallback(e)); } #onContentLoadedCallback(e: CustomEvent>) { const eventContentField = e?.detail?.[this.eventContentProp]; if (!eventContentField) { console.error(`[Dispatcher] OnContentLoaded event problem: prop '${this.eventContentProp}' in 'eventContentField' is undefined`, e); return; } Dispatcher.initiators.forEach(({ initiator }) => { if (eventContentField instanceof Document || eventContentField instanceof HTMLElement) { if (this.isInitiatorValid({ event: e, initiatorFn: initiator, eventContentField, initiators: Dispatcher.initiators, })) { initiator(eventContentField); if (this.isDebug) { console.debug("[Dispatcher] Run initiator:", initiator, eventContentField); } } else { console.debug("[Dispatcher] Initiator is rejected by isInitiatorValid filter:", initiator); } } }); } /** * Проверяет существование инициатора по селектору * @param selector{String} селектор инициатора * @returns {Boolean} результат проверки */ static isInitiatorExist(selector: string) { return Dispatcher.initiators.has(selector); } /** * Устанавливает инициатор для селектора * @param params{Object} параметры инициатора * @param params.selector{String} селектор для идентификации * @param params.initiator{Function} функция-инициатор * @param params.getCollection{Function=} функция для получения коллекции экземпляров */ static set initiator({ selector, initiator, getCollection = () => [] }: { selector: string; initiator: TDispatcherInitiatorFn; getCollection?: TDispatcherGetCollection; }) { if (!Dispatcher.isInitiatorExist(selector)) { Dispatcher.initiators.set(selector, { initiator, getCollection }); } } /** * Выполняет действие для всех экземпляров коллекций в указанном элементе * @param el{HTMLElement|null=} корневой элемент для поиска, по умолчанию body * @param action{String} название метода для вызова у экземпляров * @param filterFn{Function=} функция-фильтр для отбора экземпляров */ static dispatchAction( el: HTMLElement | null = null, action: string, filterFn: (instance: TDispatcherCollectionItem) => boolean = (_instance: TDispatcherCollectionItem) => true ) { const root = el || getDocument().body; if (root) { Dispatcher.initiators.forEach(({ getCollection = () => [] }) => { getCollection().forEach((instance: TDispatcherCollectionItem) => { if (root.contains(instance.instance) && hasMethod(instance, action) && filterFn(instance)) { instance[action](); } }); }); } } /** * Удаляет инициатор по селектору * @param selector{String} селектор инициатора для удаления */ static removeInitiator(selector: string) { if (Dispatcher.isInitiatorExist(selector)) { Dispatcher.initiators.delete(selector); } } /** * Очищает все инициаторы */ static clear() { Dispatcher.initiators.clear(); } } export { Dispatcher, };