/* ============================================================================= * ui_template_modal.ts * 공용 모달 겉틀 — 배경 · 제목줄 · 닫기 · Esc · 포커스. 몸은 `mount` 가 채움. * M01 · M02 가 함께 씀 — `dialogClass` · `backdropClass` 로 화면별 겉모양을 덧붙임. * 곁값 — `actions`(머리 [닫기] 바로 왼쪽 단추 · mount 셋째 인자 자리에 넣어도 됨) · `wide`(큰 창 96vw × 94vh). * 안 넘긴 창은 지금 꼴 그대로. * ========================================================================== */ import "./ui_template_modal.css"; import { createButton, el } from "./ui_template_elements"; export interface ModalOptions { title: string; closeLabel: string; /** `actions` = 머리 단추 자리 — 몸에서 만든 단추([확인] 등)를 넣음 */ mount: (body: HTMLElement, close: () => void, actions: HTMLElement) => void; dialogClass?: string; /** 머리 [닫기] 바로 왼쪽에 둘 단추(넘긴 창만) */ actions?: HTMLElement[]; /** 큰 창 — 96vw × 94vh · 몸 넘침만 스크롤 */ wide?: boolean; backdropClass?: string; /** 닫기 단추 · Esc · 바깥 누르기 전에 물음 — false 면 안 닫음(몸의 `close` 는 묻지 않고 닫음) */ beforeClose?: () => boolean | Promise; } export function openModal(opts: ModalOptions): { close: () => void } { const body = el("div", { className: "ui-modal__body" }); const actions = el("div", { className: "ui-modal__actions", children: opts.actions ?? [] }); const closeButton = createButton({ label: opts.closeLabel, variant: "ghost", onClick: () => void ask(), }); const top = el("div", { className: "ui-modal__top", children: [el("h3", { text: opts.title }), closeButton], }); const close = (): void => backdrop.remove(); const ask = async (): Promise => { if (!opts.beforeClose || (await opts.beforeClose())) close(); }; const dialog = el("div", { className: `ui-modal ${opts.wide ? "ui-modal--wide " : ""}${opts.dialogClass ?? ""}`.trim(), attrs: { role: "dialog", "aria-label": opts.title }, children: [top, body], }); const backdrop = el("div", { className: `ui-modal__backdrop ${opts.backdropClass ?? ""}`.trim(), children: [dialog], }); backdrop.addEventListener("click", (ev) => ev.target === backdrop && void ask()); backdrop.addEventListener("keydown", (ev) => ev.key === "Escape" && void ask()); opts.mount(body, close, actions); // 단추가 있을 때만 머리에 — [닫기] 는 머리 직속 그대로(B05 추천의 `.ui-modal__top > button` 갈아 끼우기) if (actions.childElementCount) closeButton.before(actions); document.body.append(backdrop); dialog.tabIndex = -1; // 몸의 첫 입력 칸에 커서 — mount 안의 focus() 는 붙기 전이라 안 먹음 (body.querySelector("input, textarea, select") ?? dialog).focus(); return { close }; }