import { CLOSING_CLASS, ESCAPE_KEY, OVERLAY_CLOSE_MS, SCROLL_LOCK_CLASS, } from "#configuration/constants/overlay.constants"; import { OVERLAY_DISMISSED, OVERLAY_REQUESTED } from "#core/ids/overlay.ids"; import { emitEvent, subscribeEvent } from "#core/buses/base.bus"; import type { Disposer } from "#types/base.types"; import { OPEN_CLASS } from "#configuration/constants/element.constants"; import type { OverlayRequestedEvent } from "#types/event.types"; import { createCloseButton } from "#presentation/components/overlay.component"; import { createElement } from "#core/factories/element.factory"; const createDialog = function createDialog(event: OverlayRequestedEvent): HTMLElement { const header = createElement("div", { children: [createElement("span", { className: "overlay-title", text: event.title }), createCloseButton()], className: "overlay-header", }); const body = createElement("div", { children: [event.content], className: "overlay-body" }); return createElement("div", { attributes: { "aria-label": event.title, "aria-modal": "true", role: "dialog" }, children: [header, body], className: "overlay-dialog", }); }; const createBackdrop = function createBackdrop(dialog: HTMLElement): HTMLElement { const backdrop = createElement("div", { children: [dialog], className: "overlay-backdrop" }); backdrop.addEventListener("click", (click) => { if (click.target === backdrop) { emitEvent({ name: OVERLAY_DISMISSED }); } }); return backdrop; }; const onEscape = function onEscape(key: KeyboardEvent): void { if (key.key === ESCAPE_KEY) { emitEvent({ name: OVERLAY_DISMISSED }); } }; const reveal = function reveal(overlay: HTMLElement): void { requestAnimationFrame(() => { requestAnimationFrame(() => { overlay.classList.add(OPEN_CLASS); }); }); }; const teardown = function teardown(overlay: HTMLElement): void { setTimeout(() => { overlay.remove(); document.body.classList.remove(SCROLL_LOCK_CLASS); }, OVERLAY_CLOSE_MS); }; const open = function open(event: OverlayRequestedEvent): HTMLElement { const overlay = createElement("div", { children: [createBackdrop(createDialog(event))], className: "overlay" }); document.body.classList.add(SCROLL_LOCK_CLASS); document.body.append(overlay); document.addEventListener("keydown", onEscape); reveal(overlay); return overlay; }; const close = function close(overlay: HTMLElement): void { overlay.classList.remove(OPEN_CLASS); overlay.classList.add(CLOSING_CLASS); document.removeEventListener("keydown", onEscape); teardown(overlay); }; export const mountOverlay = function mountOverlay(): Disposer { let current: HTMLElement | null = null; const disposers = [ subscribeEvent(OVERLAY_REQUESTED, (event) => { if (current !== null) { close(current); } current = open(event); }), subscribeEvent(OVERLAY_DISMISSED, () => { if (current !== null) { close(current); current = null; } }), ]; return () => { for (const dispose of disposers) { dispose(); } }; };