Files
Aislo/ui_template/ui_template_loading.ts
T
eomsangdonandClaude Opus 5.5 25cf716a0d feat(M02): 컨테이너 안 「불러오는 중」 — 0.3초 넘을 때만 · 실패 = 까닭 + 다시 불러오기 (PLAN 30-6)
- 공용 부품 ui_template_loading(showLoading · loadingError · delayedShow) — 칸 안 작은 회전 표시 + 「불러오는 중...」 · 화면 전체 잠금 아님 · 다른 페이지도 씀
- 붙인 자리 — 좌측 나무(구조물 · 일위대가 · 사람 나무) · 상세 칸(구조물 표 · 도면 · 면적 · 재료) · 조합 목록 · 조합 상세 · 항목 카드
- 공용 글 Common_Load_Failed · Common_Reload
- 시험 test_ui_loading(0.3초 규칙 가짜 시계 · 실패 글 · 붙은 자리)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-30 20:02:36 +09:00

72 lines
2.8 KiB
TypeScript

/* =============================================================================
* 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<typeof setTimeout>;
clear: (id: ReturnType<typeof setTimeout>) => 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],
});
}