/* ============================================================================= * ui_template_loading.ts * 컨테이너 안 「불러오는 중」 — 화면 전체 잠금(`showLoadingOverlay`)과 달리 그 칸 안에만 작은 회전 표시 + * 글 · 0.3초 넘게 걸릴 때만 보임(빠른 응답은 깜빡임 없음) · 실패하면 그 자리에 까닭 + [다시 불러오기]. * * const stop = showLoading(host); * try { const got = await load(); stop(); draw(got); } * catch (e) { stop(); host.replaceChildren(loadingError(why(e), () => void again())); } * ========================================================================== */ import "./ui_template_loading.css"; import { createButton, el } from "./ui_template_elements"; import { t } from "./ui_template_locale"; export const LOADING_DELAY_MS = 300; /** `delay` 뒤에 `show` — 그 전에 돌려받은 함수를 부르면 안 보임 · 보인 뒤면 `hide` */ export function delayedShow( show: () => void, hide: () => void, delay: number = LOADING_DELAY_MS, // 브라우저 setTimeout 은 window 에서 불러야 함(객체 메서드로 부르면 Illegal invocation) — 감싸서 넘김 timer: { set: (fn: () => void, ms: number) => ReturnType; clear: (id: ReturnType) => void; } = { set: (fn, ms) => setTimeout(fn, ms), clear: (id) => clearTimeout(id), }, ): () => void { let shown = false; const id = timer.set(() => { shown = true; show(); }, delay); return () => { timer.clear(id); if (shown) hide(); }; } /** 칸 안 불러오는 중 — 돌려받은 함수 = 끝(보였으면 표시를 뗌 · 내용은 부르는 쪽이 그림) */ export function showLoading(host: HTMLElement, text = t("Common_Status_Loading")): () => void { const box = el("div", { className: "ui-loading", attrs: { role: "status", "aria-live": "polite" }, children: [ el("span", { className: "ui-loading__spin", attrs: { "aria-hidden": "true" } }), el("span", { text }), ], }); return delayedShow( () => host.replaceChildren(box), () => box.remove(), ); } /** 실패 글 — 까닭 그대로 · 없으면 「불러오지 못함」 */ export const loadingFailText = (reason: string): string => reason ? `${t("Common_Load_Failed")} — ${reason}` : t("Common_Load_Failed"); /** 실패 자리 — 까닭 글 + [다시 불러오기] */ export function loadingError(reason: string, retry: () => void): HTMLElement { const again = createButton({ label: t("Common_Reload"), variant: "ghost", size: "sm" }); again.addEventListener("click", retry); return el("div", { className: "ui-loading ui-loading--error", attrs: { role: "alert" }, children: [el("span", { text: loadingFailText(reason) }), again], }); }