- 항목 추가(나무 「항목 추가」 · 「새 항목」) — 구조물 종류(있는 것 고름 · 새 이름 · 폴더를 골라 두었으면 그 안) + 이름 + 일위대가(세 층 조합 고르기 창 · 찾기 AND · 비워도 됨) · 단위 칸 없앰 · 빈 칸 안내 · 서버 add-item 이 일위대가 받음 - 일위대가 고르기 창을 pickCombo 로 내보내 표 칸 고르기와 같이 씀 - 카드 넷(도면 정보 · 면적산출 · 재료산출 · 선택 입력) 2행 2열 · 높이 360px · 넘치면 카드 안 스크롤 · 상세 칸 전체 스크롤(좁으면 1열) - 「구조물 조합」 제목을 누르면 표 접힘 · 펴짐 - 시험 — 항목 추가 서버 · 창 · 격자 · 접기 · 옛 단위 필수 시험을 31 정함으로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
244 lines
9.3 KiB
TypeScript
244 lines
9.3 KiB
TypeScript
/* =============================================================================
|
|
* 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<Record<string, string> | null> {
|
|
return new Promise((resolve) => {
|
|
let done = false;
|
|
const finish = (value: Record<string, string> | 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<string, string> = {};
|
|
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<NewItem | null> {
|
|
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<string[] | null> {
|
|
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<Settle> {
|
|
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] }));
|
|
},
|
|
});
|
|
});
|
|
}
|