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>
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
/* =============================================================================
|
||||
* 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],
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user