/* ============================================================================= * M02_MasterTemplete_TreePanel_Dialogs.ts * 구조물 라이브러리 오른쪽 클릭 메뉴가 띄우는 작은 창 — 글 받기 · 옮길 폴더 고르기 · 저장/버림 물음. * 모두 약속(Promise)으로 답함 — 닫기 · Esc · 바깥 누르기 = 취소(null / "cancel"). * ========================================================================== */ import { createButton, createInputField, createSelectField, el } from "@ui/ui_template_elements"; import { t as L } from "@ui/ui_template_locale"; import { openModal } from "@ui/ui_template_modal"; export interface TextField { key: string; label: string; value?: string; /** 비워도 됨 — 아니면 비운 채 [확인] 하면 그 칸에 안내 글 */ optional?: boolean; } /** 글 칸 하나 이상을 받음 — 확인이면 칸 이름 → 글(앞뒤 공백 뺌) */ export function askText( title: string, fields: TextField[], okLabel = L("M02_TreeOk"), ): Promise | null> { return new Promise((resolve) => { let done = false; const finish = (value: Record | null): void => { if (done) return; done = true; resolve(value); }; openModal({ title, closeLabel: L("M02_Close"), dialogClass: "m02-modal m02-treedlg", beforeClose: () => (finish(null), true), mount: (body, close) => { const inputs = fields.map((f) => ({ f, field: createInputField({ label: f.label, value: f.value }), })); const ok = createButton({ label: okLabel, variant: "filled" }); const submit = (): void => { // 빈 칸은 그 칸에 빨간 테두리 + 한 줄(칸마다) — 토스트는 창 뒤에 가려 안 보였음 const out: Record = {}; let bad = false; for (const { f, field } of inputs) { out[f.key] = field.input.value.trim(); const empty = !out[f.key] && !f.optional; field.setError(empty ? L("M02_FieldNeeded").replace("{label}", f.label) : undefined); bad ||= empty; } if (bad) return; finish(out); close(); }; ok.addEventListener("click", submit); for (const { field } of inputs) { field.input.addEventListener("input", () => field.setError(undefined)); field.input.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.isComposing) submit(); }); } body.append(...inputs.map(({ field }) => field.root), ok); inputs[0]?.field.input.select(); }, }); }); } /** 항목 추가 창이 돌려줌 — `path` = 넣을 폴더(종류 · 고른 폴더) · `combo` = 고른 일위대가 키(없으면 null) */ export interface NewItem { path: string[]; name: string; combo: string | null; } /** 항목 추가(PLAN 31-2) — 구조물 종류(맨 위 폴더 · 있는 것 고르거나 새 이름 · 나무에서 폴더를 골라 두었으면 * 그 안이라 안 물음) + 이름 + 일위대가(`pickCombo` 고르기 창 · 비워도 됨 — 표에서 나중에) · 단위는 안 받음 */ export function askNewItem(opt: { title: string; /** 있는 종류(맨 위 폴더 이름) — 칸에 글을 치면 목록에서 고름 */ kinds: string[]; /** 나무에서 골라 둔 폴더 — 있으면 종류 칸 없음 */ at: string[]; pickCombo?: () => Promise<{ key: string; label: string } | null>; }): Promise { return new Promise((resolve) => { let done = false; const finish = (value: NewItem | null): void => { if (done) return; done = true; resolve(value); }; openModal({ title: opt.title, closeLabel: L("M02_Close"), dialogClass: "m02-modal m02-treedlg", beforeClose: () => (finish(null), true), mount: (body, close) => { const kind = opt.at.length ? null : createInputField({ label: L("M02_NewItemKind") }); if (kind) { const list = el("datalist", { attrs: { id: `m02-kinds-${Date.now()}` }, children: opt.kinds.map((k) => el("option", { attrs: { value: k } })), }); kind.input.setAttribute("list", list.id); kind.root.append(list); } const where = opt.at.length ? el("p", { className: "m02-treedlg__at", text: L("M02_NewItemAt").replace("{value}", opt.at.join(" / ")), }) : null; const name = createInputField({ label: L("M02_TreeItemName") }); let combo: { key: string; label: string } | null = null; const shown = el("span", { className: "m02-treedlg__combo", text: L("M02_NewItemNoCombo"), }); const pick = createButton({ label: L("M02_NewItemPickCombo"), variant: "ghost", size: "sm", }); pick.addEventListener("click", async () => { const got = await opt.pickCombo?.(); if (got === null || got === undefined) return; // 닫음 = 그대로 combo = got.key ? got : null; // 「비움」 = 없앰 shown.textContent = combo ? `${combo.key} ${combo.label}` : L("M02_NewItemNoCombo"); }); const comboRow = opt.pickCombo ? el("div", { className: "m02-treedlg__row", children: [el("span", { text: L("M02_NewItemCombo") }), shown, pick], }) : null; const ok = createButton({ label: L("M02_TreeOk"), variant: "filled" }); const need = (field: typeof name, label: string): boolean => { const empty = !field.input.value.trim(); field.setError(empty ? L("M02_FieldNeeded").replace("{label}", label) : undefined); return empty; }; const submit = (): void => { // 빈 칸은 그 칸에 빨간 테두리 + 한 줄(칸마다) const badKind = kind ? need(kind, L("M02_NewItemKind")) : false; const badName = need(name, L("M02_TreeItemName")); if (badKind || badName) return; const path = kind ? [kind.input.value.trim()] : opt.at; finish({ path, name: name.input.value.trim(), combo: combo?.key ?? null }); close(); }; ok.addEventListener("click", submit); for (const field of [kind, name]) { if (!field) continue; field.input.addEventListener("input", () => field.setError(undefined)); field.input.addEventListener("keydown", (event) => { if (event.key === "Enter" && !event.isComposing) submit(); }); } body.append( ...[where, kind?.root, name.root, comboRow].filter((n): n is HTMLDivElement => !!n), ok, ); (kind ?? name).input.focus(); }, }); }); } /** 옮길 폴더 고르기 — `folders` 에서 `blocked` 인 것은 뺌 · 맨 위(폴더 밖)는 늘 있음 */ export function askFolder( title: string, folders: string[][], blocked: (path: string[]) => boolean, current: string[], ): Promise { return new Promise((resolve) => { let done = false; const finish = (value: string[] | null): void => { if (done) return; done = true; resolve(value); }; const choices = [[], ...folders.filter((path) => !blocked(path))]; openModal({ title, closeLabel: L("M02_Close"), dialogClass: "m02-modal m02-treedlg", beforeClose: () => (finish(null), true), mount: (body, close) => { const field = createSelectField({ label: L("M02_TreeMoveTo"), options: choices.map((path, i) => ({ value: String(i), text: path.length ? path.join(" / ") : L("M02_TreeTop"), })), value: String(choices.findIndex((path) => path.join("|") === current.join("|"))), }); const ok = createButton({ label: L("M02_TreeOk"), variant: "filled" }); ok.addEventListener("click", () => { finish(choices[Number(field.select.value)] ?? []); close(); }); body.append(field.root, ok); }, }); }); } export type Settle = "save" | "discard" | "cancel"; /** 저장 안 한 표 고침 — 저장하고 계속 · 버리고 계속 · 취소 */ export function askSettle(): Promise { return new Promise((resolve) => { let done = false; const finish = (value: Settle): void => { if (done) return; done = true; resolve(value); }; openModal({ title: L("M02_TreeUnsaved"), closeLabel: L("M02_TreeCancel"), dialogClass: "m02-modal m02-treedlg", beforeClose: () => (finish("cancel"), true), mount: (body, close) => { const choose = (value: Settle) => () => { finish(value); close(); }; const save = createButton({ label: L("M02_TreeSaveGo"), variant: "filled" }); const discard = createButton({ label: L("M02_TreeDiscardGo"), variant: "danger" }); const cancel = createButton({ label: L("M02_TreeCancel"), variant: "ghost" }); save.addEventListener("click", choose("save")); discard.addEventListener("click", choose("discard")); cancel.addEventListener("click", choose("cancel")); body.append(el("div", { className: "m02-side__row", children: [save, discard, cancel] })); }, }); }); }