import type { FancyboxInstance, CarouselSlide, FancyboxOptions } from "@fancyapps/ui";
import { Fancybox } from "@fancyapps/ui/dist/fancybox/index.js";
import { isSelectorValid } from "@web3r/flowerkit/css";
import { isNode } from "@web3r/flowerkit/dom";
import { bubble } from "@web3r/flowerkit/evt";
import { wait } from "@web3r/flowerkit/fn";
import { getJSONFromStr } from "@web3r/flowerkit/json";
import { getMergedObj, isObjHasOwnProp } from "@web3r/flowerkit/obj";
import { getId } from "@web3r/flowerkit/str";
import { createElement } from "react";
import type { PropsWithChildren } from "react";
import { getDocument, getWindow } from "ssr-window";
import { locales } from "./config/locales";
import type { TLocales } from "./config/locales";
import type {
TCallbackArg, TControls, TFbOptions, TModalCfg, TPresets, TResult, TModalId, TLayout, TAsset, TActions,
} from "./config/types";
import {
getAsyncModalLayout, getTemplateLayout, getCfgProp, getLocaleCluster, isAssetsValid,
} from "./utils/index";
import type { IBtn } from "../../components/btn";
import { Btn } from "../../components/btn";
import { Icon } from "../../components/icon";
import { Locale } from "../../components/locale";
import { Modal } from "../../components/modal";
import type { IModal } from "../../components/modal/config/types";
import { getPresetPartials } from "../../components/modal/lib";
import { constants } from "../../constants";
import type { IComponentBaseProps } from "../../types";
import { dispatchContentLoaded } from "../../utils/dispatchContentLoaded";
import { getAttr } from "../../utils/getAttr";
import { getClassString } from "../../utils/getClassString";
import { getSanitizedHTML } from "../../utils/getSanitizedHTML";
import { getStaticMarkupForClient } from "../../utils/react/getStaticMarkup.client";
import { Localisation } from "../localisation";
/**
* Singleton. Плагин модальных окон.
* Асинхронно загружает внутренние ресурсы и предоставляет методы для работы с модальными окнами.
*/
export class Modals {
/**
* Статусы операций во время показа модального окна
*/
static reasons = {
open: "MODAL_IS_OPEN",
close: "MODAL_IS_CLOSE",
error: "MODAL_IS_ERROR",
};
/**
* Всплывающие события
*/
static bubbles = {
// открытие модального окна после анимации
open: "modals::open",
// модальное окно готово к закрытию
beforeClose: "modals::beforeClose",
// закрытие модального окна после анимации
close: "modals::close",
// подтверждение внутри модального окна
confirm: "modals::confirm",
// отмена внутри модального окна
cancel: "modals::cancel",
// загрузка асинхронного модального окна
loading: "modals::loading",
// ошибка открытия модального окна
error: "modals::error",
};
/**
* Singleton-экземпляр плагина модальных окон.
* @type {Modals|null}
*/
static instance: Modals | null = null;
/**
* Текущая ссылка на Fancybox API.
* Допускает замену реализации при интеграции.
* @type {typeof Fancybox}
*/
static fancybox: typeof Fancybox = Fancybox;
/**
* Базовые селекторы модальных блоков и интерактивных элементов.
*/
static selectors = {
modal: "[data-js-modal]",
handler: "[data-js-modal-handler]",
interaction: "input, textarea, select, form, label, a",
};
/**
* Стандартная конфигурация Fancybox
* @see https://fancyapps.com/fancybox/api/options
*/
static fancyboxDefaultCfg: TFbOptions = {
mainTpl: `
`,
mainClass: "modal",
animated: false,
hideScrollbar: true,
closeButton: false,
compact: true,
idle: false,
on: {
["Carousel.ready"]: (instance: FancyboxInstance, slide: CarouselSlide) => null,
close: (instance: FancyboxInstance, slide: CarouselSlide) => null,
},
trapFocus: false,
autoFocus: false,
placeFocusBack: false,
showClass: "isModalShow",
hideClass: "isModalHide",
triggerEl: null,
};
/**
* Конфигурация модального окна по умолчанию.
* Используется как база при открытии через `getOpen` и парсинге handler-конфига.
* @type {TModalCfg}
*/
static modalDefaultCfg: TModalCfg = {
type: "inline",
src: null,
async: null,
assets: null,
redirectAfterClose: null,
isReloadAfterClose: false,
onAction: null,
fancyboxCfg: {},
};
static #getModalElFromInstance(instance: FancyboxInstance) {
const container = instance.getContainer();
return container?.querySelector(Modals.selectors.modal) ?? null;
}
static #getCfg(node: HTMLElement, selector: string): TModalCfg {
const attrValue = node.getAttribute(getAttr(selector));
const parsedCfg: TModalCfg = attrValue ? getJSONFromStr(attrValue) : {};
if (selector === Modals.selectors.handler) {
if (parsedCfg.assign) {
return {
assign: parsedCfg.assign,
};
} else {
return {
...Modals.modalDefaultCfg,
...parsedCfg,
src: getCfgProp("src", node, parsedCfg) as TModalCfg["src"],
id: getCfgProp("id", node, parsedCfg) as TModalCfg["id"],
type: getCfgProp("type", node, parsedCfg) as TModalCfg["type"],
action: parsedCfg.action,
preset: parsedCfg.preset,
triggerEl: node,
fancyboxCfg: parsedCfg?.fancyboxCfg ?? {},
};
}
} else {
const { redirectAfterClose, isReloadAfterClose } = parsedCfg;
return {
redirectAfterClose,
isReloadAfterClose,
};
}
}
/**
* Генерирует HTML-кнопку управления Fancybox-панелью.
* @returns {Promise}
*/
static getControlsLayout = async (type: TControls): Promise => {
if (!type) {
return "";
}
const props: Partial<{
localeCode: string;
type: IBtn["type"];
icon: IBtn["icon"];
extraAttrs: IComponentBaseProps["extraAttrs"];
}> = {
type: "button" as const,
icon: type,
localeCode: type.toUpperCase(),
};
switch (type) {
case "zoomOut":
props.extraAttrs = {
"data-panzoom-action": "zoomOut",
};
break;
case "zoomIn":
props.extraAttrs = {
"data-panzoom-action": "zoomIn",
};
break;
case "download":
props.type = "link";
props.extraAttrs = {
"download": true,
"data-carousel-download": "",
};
break;
case "close":
props.extraAttrs = {
"data-fancybox-close": "",
};
break;
case "fullscreen":
props.extraAttrs = {
"data-fullscreen-action": "toggle",
};
break;
}
return getStaticMarkupForClient(createElement(Locale, {
code: `modals.${props.localeCode}`,
attr: "title",
}, createElement(Btn, {
extraAttrs: props.extraAttrs,
label: null,
type: props.type,
icon: props.icon,
iconSize: "medium",
})));
};
static #getCN(el?: string, mod?: IComponentBaseProps["extraClasses"], utilClasses?: string[]) {
return getClassString("modal", el, mod);
}
/**
* Получает HTML-разметку модального окна на основе готового шаблона
* @param preset{TPresets}
* @param props{Object=}
* @returns {IModal}
*/
static getLayoutPresetProps = (preset: TPresets, props?: PropsWithChildren): IModal => {
const id: string = props?.id ?? getId(9);
const cfg = props?.cfg ?? {};
const presetPartials = getPresetPartials(preset, id, Modals.#getCN);
const userPartials: TLayout = {
children: props?.children || presetPartials.contentSlot,
titleSlot: props?.titleSlot,
footerSlot: props?.footerSlot,
headerSlot: props?.headerSlot,
};
const layout: TLayout = getMergedObj(userPartials, presetPartials);
return {
isInline: true,
cfg,
id,
...layout,
};
};
constructor() {
if (Modals.instance) {
return Modals.instance;
}
Modals.instance = this;
Modals.locales = locales;
this.bindEvents();
}
static set locales(locales: TLocales) {
const setLocales = async (instance: Localisation) => {
const currentLang: string = instance.lang;
const langList: string[] = instance.langList;
Modals.fancyboxDefaultCfg.l10n = getLocaleCluster(locales, currentLang, langList);
// silent mode
await instance.getResourceAppend({
modals: locales,
}, true, true);
};
const instance: Localisation | null = Localisation.instance;
if (instance) {
void setLocales(instance);
} else {
void Localisation.create()
.then((created) => setLocales(created));
}
}
static getInstance(id?: TModalId) {
if (Modals.instance) {
return Modals.fancybox?.getInstance(id) ?? null;
} else {
return null;
}
}
static async getClose(id: TModalId | undefined): Promise<{
reason: string;
instance: FancyboxInstance | null;
}> {
return new Promise((resolve, reject) => {
const resolveClose = (instance: FancyboxInstance | null = null) => {
resolve({
reason: Modals.reasons.close,
instance,
});
};
const closeInstance = (instance: FancyboxInstance) => {
Modals.#onAfterAnimated(instance)
.finally(() => {
resolveClose(instance);
});
instance.close();
};
if (Modals.instance) {
if (typeof id === "undefined") {
Modals.fancybox?.close(true);
resolveClose(null);
} else {
const instance = Modals.getInstance(id);
if (instance) {
closeInstance(instance);
} else {
const currentInstance = Modals.getInstance();
if (currentInstance) {
console.warn(`[Modals] Instance with id "${id}" not found. The current modal will be closed as fallback`);
closeInstance(currentInstance);
} else {
resolveClose(null);
}
}
}
} else {
reject({
reason: Modals.reasons.error,
instance: null,
});
}
});
}
static #getDraggableCfg(cfg: TFbOptions, isDraggable: boolean): TFbOptions {
return {
...cfg,
dragToClose: isDraggable,
Carousel: {
...cfg?.Carousel ?? {},
gestures: isDraggable ? cfg?.Carousel?.gestures : false,
dragFree: isDraggable ? cfg?.Carousel?.dragFree : false,
singleClickAction: isDraggable ? cfg?.Carousel?.singleClickAction : false,
wheelAction: isDraggable ? cfg?.Carousel?.wheelAction : false,
},
};
}
static async #getInlineModal(src: string, cfg: TFbOptions) {
if (isSelectorValid(src)) {
const el = getDocument().querySelector(src);
if (el) {
const isDraggable = isObjHasOwnProp(cfg, "dragToClose") ? !!cfg.dragToClose : !Modals.#isHasInteraction(el);
const instanceCfg = Modals.#getDraggableCfg({
...cfg,
parentEl: isNode(cfg.parentEl) ? cfg.parentEl as HTMLElement : (el?.parentNode ?? getDocument()),
}, isDraggable);
if (el.tagName.toLowerCase() === "template") { // template element
const content = getTemplateLayout(el);
if (content.length) {
const instance = Modals.fancybox?.show([
{
html: content,
},
], instanceCfg) ?? null;
// with fallback
return instance || Modals.getInstance(cfg.id);
} else {
return null;
}
} else { // other elements
const instance = Modals.fancybox?.show([
{
src,
type: "inline",
},
], instanceCfg) ?? null;
// with fallback
return instance || Modals.getInstance(cfg.id);
}
} else {
return null;
}
} else {
return null;
}
}
static async #getAssetsModal(src: TAsset[], cfg: TFbOptions) {
const options: FancyboxOptions = {
Carousel: {
Thumbs: false,
enabled: false,
Toolbar: {
items: {
zoomPlus: {
tpl: await Modals.getControlsLayout("zoomOut"),
},
zoomMinus: {
tpl: await Modals.getControlsLayout("zoomIn"),
},
download: {
tpl: await Modals.getControlsLayout("download"),
},
prev: {
tpl: await Modals.getControlsLayout("prev"),
},
next: {
tpl: await Modals.getControlsLayout("next"),
},
fullscreen: {
tpl: await Modals.getControlsLayout("fullscreen"),
},
close: {
tpl: await Modals.getControlsLayout("close"),
},
},
display: {
left: [],
middle: [],
right: [
"fullscreen",
"download",
"zoomPlus",
"zoomMinus",
"close",
],
},
},
Arrows: {
prevTpl: await getStaticMarkupForClient(createElement("span", { className: "btn__icon" }, createElement(Icon, { size: "medium", src: "prev" }))),
nextTpl: await getStaticMarkupForClient(createElement("span", { className: "btn__icon" }, createElement(Icon, { size: "medium", src: "next" }))),
},
},
...cfg,
};
const items = src.map((item, index) => {
return {
...item,
caption: (typeof item.caption === "function") ? item.caption(index) : item.caption,
};
});
const instance = Modals.fancybox?.show(items, options) ?? null;
return instance || Modals.getInstance(cfg.id);
}
static async #getHTMLModal(src: string, cfg: TFbOptions) {
// replace `template` tags to `div`
const source = `${src.replace(/<(\/?)template\b([^>]*)>/g, "<$1div$2>")}
`;
const html = getSanitizedHTML(source);
const instance = Modals.fancybox?.show([
{
html,
},
], cfg) ?? null;
return instance || Modals.getInstance(cfg.id);
}
static onClick({ instance, event, onAction }: TCallbackArg) {
let action: TActions = "click";
const { target } = event!;
if (!!target && (target as HTMLElement).matches(Modals.selectors.handler)) {
const handlerCfg = Modals.#getCfg(target as HTMLElement, Modals.selectors.handler);
if (handlerCfg.action && handlerCfg.action !== "close") {
action = handlerCfg.action;
}
}
if (typeof onAction === "function") {
onAction({
instance,
action,
event,
});
}
}
static #isHasInteraction(el: Element) {
return !!el.querySelectorAll(Modals.selectors.interaction).length;
}
static onRender({
instance, actions, slide, onAction, cfg,
}: TCallbackArg) {
const container = instance.getContainer();
if (!container) {
return;
}
const arrows = container?.querySelectorAll(".is-arrow") ?? [];
arrows.forEach((arrow) => {
arrow.classList.add("btn");
arrow.classList.remove("f-button");
});
}
static onAfterOpen({
instance, actions, slide, onAction, cfg,
}: TCallbackArg) {
const options: FancyboxOptions = instance.getOptions();
const container = instance.getContainer();
if (options.dragToClose && container) {
container.classList.add("is-draggable");
}
const el = Modals.#getModalElFromInstance(instance);
if (el) {
el.removeAttribute("hidden");
el.setAttribute("aria-hidden", "false");
}
Modals.#onAfterAnimated(instance)
.finally(() => {
if (typeof onAction === "function") {
onAction({
instance,
action: "open",
});
}
const container = instance.getContainer();
if (container) {
dispatchContentLoaded({
content: container,
});
bubble(container || getDocument(), Modals.bubbles.open, { ...cfg, instance });
}
actions.resolve({
reason: Modals.reasons.open,
instance,
});
});
}
static async #onAfterAnimated(instance: FancyboxInstance) {
const container = instance.getContainer();
if (container) {
return Promise.race([
new Promise((resolve) => {
const onAnimationEnd = (e: AnimationEvent) => {
resolve(e);
};
container.addEventListener("animationend", onAnimationEnd, {
once: true,
});
}),
wait(5000).then(() => undefined),
])
.then((event: AnimationEvent | undefined) => {
if (!event) {
console.error(`[Modals] Force callback after 5000ms: can't catch "animationend" event. Please check animation CSS for this instance: `, instance);
}
return Promise.resolve();
});
} else {
return Promise.resolve();
}
}
static onBeforeClose({
instance, actions, onAction, cfg, event, customEvent,
}: TCallbackArg) {
if (event && event.type === "cancel" && customEvent) { // fix the bug of file dialog closing
customEvent.preventDefault();
} else {
const container = instance.getContainer();
bubble(container || getDocument(), Modals.bubbles.beforeClose, { ...cfg, instance });
}
}
static onAfterClose({
instance, actions, onAction, cfg, event,
}: TCallbackArg) {
const el = Modals.#getModalElFromInstance(instance);
if (el) {
const {
redirectAfterClose,
isReloadAfterClose,
} = Modals.#getCfg(el as HTMLElement, Modals.selectors.modal);
switch (true) {
case typeof redirectAfterClose === "string":
getWindow().location.href = redirectAfterClose.toString();
break;
case isReloadAfterClose:
getWindow().location.reload();
break;
default:
}
}
Modals.#onAfterAnimated(instance)
.finally(() => {
if (el) {
el.setAttribute("aria-hidden", "true");
el.setAttribute("hidden", "");
}
if (typeof onAction === "function") {
onAction({
instance,
action: "close",
});
}
const bubbleEl = instance.getContainer() || getDocument();
bubble(bubbleEl, Modals.bubbles.close, { ...cfg, instance });
actions.resolve({
reason: Modals.reasons.close,
instance,
});
});
}
static isOpen(id?: TModalId) {
const instance = Modals.getInstance(id);
if (instance) {
const state = instance.getState();
return ![ 2, 3, 4 ].includes(state);
} else {
return false;
}
}
static async getOpen(cfg: Omit) {
const {
src, type, preset, fancyboxCfg, id, async, assets, onAction,
} = {
...Modals.modalDefaultCfg,
...cfg,
};
return new Promise((resolve, reject) => {
// Действия с результатом
const actions = {
resolve: (result: TResult = { reason: Modals.reasons.open, instance: null }) => resolve(result),
reject: (result: TResult = { reason: Modals.reasons.error, instance: null }) => reject(result),
};
void (async () => {
// Адаптированная конфигурация для Fancybox
const fbCfg = getMergedObj(Modals.fancyboxDefaultCfg, fancyboxCfg || {});
// Уникальный ID
fbCfg.id = id;
// Элемент триггера
fbCfg.triggerEl = cfg.triggerEl;
// Переопредление предварительного закрытия
fbCfg.on.shouldClose = (...args: [ FancyboxInstance, CustomEvent, Event ]) => {
const [ instance, customEvent, event ] = args;
const callbackArg: TCallbackArg = {
instance, actions, onAction, cfg, event, customEvent,
};
Modals.onBeforeClose(callbackArg);
};
// Переопределение закрытия
fbCfg.on.close = (...args: [ FancyboxInstance, CarouselSlide, Event ]) => {
const [ instance, slide, event ] = args;
const callbackArg: TCallbackArg = {
instance, slide, actions, onAction, cfg, event,
};
Modals.onAfterClose(callbackArg);
};
fbCfg.on["Carousel.click"] = (...args: [ FancyboxInstance, CarouselSlide, Event ]) => {
const [ instance, slide, event ] = args;
const callbackArg: TCallbackArg = {
instance, slide, event, actions, onAction, cfg,
};
Modals.onClick(callbackArg);
};
// Переопределение рендера
fbCfg.on["Carousel.render"] = (...args: [ FancyboxInstance, CarouselSlide ]) => {
const [ instance, slide ] = args;
const callbackArg: TCallbackArg = {
instance, slide, actions, onAction, cfg,
};
Modals.onRender(callbackArg);
};
// Переопределение открытия
fbCfg.on["Carousel.ready"] = (...args: [ FancyboxInstance, CarouselSlide ]) => {
const [ instance, slide ] = args;
const callbackArg: TCallbackArg = {
instance, slide, actions, onAction, cfg,
};
Modals.onAfterOpen(callbackArg);
};
if (preset) { // готовый шаблон (только программное открытие через `getOpen`)
switch (preset) {
case "assets":
if (!!assets && isAssetsValid(assets)) {
fbCfg.id = "modalAssets_" + getId(9); // for multiple instances
await Modals.#getAssetsModal(assets, fbCfg);
} else {
console.error(`[Modals] Preset "assets" needs non-empty array in "assets" prop with "type" and "src" in every child`);
actions.reject({
reason: Modals.reasons.error,
instance: null,
});
}
break;
case "confirm": {
fbCfg.id = "modalConfirm_" + getId(9); // for multiple instances
fbCfg.dragToClose = true;
const props = Modals.getLayoutPresetProps("confirm", { id: fbCfg.id });
const html = await getStaticMarkupForClient(createElement(Modal, props));
await Modals.#getHTMLModal(html, fbCfg);
break;
}
case "success": {
fbCfg.id = "modalSuccess_" + getId(9); // for multiple instances
fbCfg.dragToClose = true;
const props = Modals.getLayoutPresetProps("success", { id: fbCfg.id });
const html = await getStaticMarkupForClient(createElement(Modal, props));
await Modals.#getHTMLModal(html, fbCfg);
break;
}
case "error": {
fbCfg.id = "modalError_" + getId(9); // for multiple instances
fbCfg.dragToClose = true;
const props = Modals.getLayoutPresetProps("error", { id: fbCfg.id });
const html = await getStaticMarkupForClient(createElement(Modal, props));
await Modals.#getHTMLModal(html, fbCfg);
break;
}
default: {
console.error(`[Modals] Preset "${preset}" not found. Only "confirm", "success", "error" are supported`);
actions.reject({
reason: Modals.reasons.error,
instance: null,
});
}
}
} else {
if (!!type && !!src) { // Открытие нужного типа окна
switch (type) {
case "async": {
bubble(getDocument(), Modals.bubbles.loading, { ...cfg, instance: null });
return await getAsyncModalLayout({
url: src,
...(async || {}),
})
.then((data) => {
return Modals.#getHTMLModal(data, fbCfg);
})
.catch((msg) => {
console.error(`[Modals] ${msg}`);
actions.reject({
reason: Modals.reasons.error,
instance: null,
});
});
}
case "inline": {
const instance = await Modals.#getInlineModal(src, fbCfg);
if (!instance) {
console.error(`[Modals] Element with anchor "${src}" not found or empty`);
actions.reject({
reason: Modals.reasons.error,
instance: null,
});
}
break;
}
case "html": {
const instance = await Modals.#getHTMLModal(src, fbCfg);
if (!instance) {
console.error(`[Modals] Can't open HTML modal`);
actions.reject({
reason: Modals.reasons.error,
instance: null,
});
}
break;
}
default: {
console.error(`[Modals] Type "${type}" not found. Only "async", "inline", "html" are supported`);
actions.reject({
reason: Modals.reasons.error,
instance: null,
});
}
}
} else {
console.error(`[Modals] Props "type" or "src" are missing`);
actions.reject({
reason: Modals.reasons.error,
instance: null,
});
}
}
})().catch((error: unknown) => {
console.error("[Modals] Unexpected error while opening modal", error);
actions.reject({ reason: Modals.reasons.error, instance: null });
});
});
}
#lockHandler(handlerNode: HTMLElement, isLock = true) {
if (handlerNode.matches("button, input")) {
(handlerNode as HTMLButtonElement | HTMLInputElement).disabled = isLock;
}
handlerNode.classList.toggle("isDisabled", isLock);
}
async #actionFromHandler(handlerNode: HTMLElement) {
// Cfg from clicked element
let parsedCfg: TModalCfg = Modals.#getCfg(handlerNode, Modals.selectors.handler);
// Labels or links with `assign` cfg
if (parsedCfg.assign) {
// Original element
const getHandler: ((id: string) => HTMLElement) = (id) => {
return !!id ? getDocument().getElementById(id) : null;
};
// Merge `assign` to cfg
const getMergedConfigs: ((handlerCfg: TModalCfg) => TModalCfg) = (handlerCfg) => {
const merged: TModalCfg = getMergedObj(parsedCfg.assign || {}, handlerCfg);
delete merged.assign;
return merged;
};
switch (handlerNode.tagName.toLowerCase()) {
case "label":
if (handlerNode.hasAttribute("for")) {
const id = (handlerNode as HTMLLabelElement)?.getAttribute("for") || "";
const handler = getHandler(id);
if (handler) {
const handlerCfg = Modals.#getCfg(handler, Modals.selectors.handler);
handlerNode = handler;
parsedCfg = getMergedConfigs(handlerCfg);
}
}
break;
case "a":
if (handlerNode.hasAttribute("href")) {
const id = (handlerNode as HTMLLinkElement).getAttribute("href")?.replace("#", "") ?? "";
const handler = getHandler(id);
if (handler) {
const handlerCfg = Modals.#getCfg(handler, Modals.selectors.handler);
handlerNode = handler;
parsedCfg = getMergedConfigs(handlerCfg);
}
}
break;
default:
console.error(`[Modals] Assign prop in cfg only applyed to "a" or "label" elements with non-empty "href" or "for" attributes`);
}
}
if (parsedCfg.id) {
switch (parsedCfg.action) {
case "open":
this.#lockHandler(handlerNode, true);
parsedCfg.onAction = ({ action }) => {
if (action === "close") {
this.#lockHandler(handlerNode, false);
}
};
await Modals.getOpen(parsedCfg)
.catch(async (err) => {
this.#lockHandler(handlerNode, false);
bubble(getDocument(), Modals.bubbles.error, { ...parsedCfg, instance: null });
if (err?.reason === Modals.reasons.error) {
await Modals.getOpen({
preset: "error",
});
}
});
break;
case "close":
await Modals.getClose(parsedCfg.id)
.catch((err) => {
bubble(getDocument(), Modals.bubbles.error, { ...parsedCfg, instance: null });
throw err;
});
break;
case "confirm":
bubble(handlerNode, Modals.bubbles.confirm, { ...parsedCfg, instance: Modals.getInstance(parsedCfg.id) });
break;
case "cancel":
bubble(handlerNode, Modals.bubbles.cancel, { ...parsedCfg, instance: Modals.getInstance(parsedCfg.id) });
break;
default:
console.error(`[Modals] Action for handler not found, only "open", "close", "confirm", "cancel" are supported`);
}
} else {
console.error(`[Modals] Prop "id" for handler not found`);
}
}
#onLangChange(e: CustomEvent) {
const instance: Localisation = e.detail.instance;
const locales = instance.getResource("modals");
if (locales) {
Modals.locales = locales;
}
}
#onClick(e: Event) {
const triggerEl = (e.target as HTMLElement).closest(Modals.selectors.handler);
if (triggerEl) {
e.preventDefault();
this.#actionFromHandler(triggerEl)
.catch((error) => {
console.error(`[Modals] Can't run action from handler`, error);
});
}
}
bindEvents() {
getDocument().addEventListener(constants.localisationBubbles.localisationChange, this.#onLangChange.bind(this));
getDocument().addEventListener("click", this.#onClick.bind(this));
}
}