- 좌측 「구조물 집계 양식」 아래 「항목 나무」 카드(세 층 공용 · 깊이 자유 · 검색 · 키보드 위아래 · 펼침 · F2 · Delete)
- 항목 누름 → 표 그 열 고름 + 카드 넷 · 표에서 열 고름 → 나무 표시
- 나무 고침 POST /api/m02/layers/{층}/tree · 판 409 다시 읽고 알림 · 저장 안 한 표 고침은 저장/버림/취소 물음
- 읽기만 층은 메뉴 · 키 없음 · 나무 메뉴 자리는 sub5 의 ui_template_tree 그대로
- 시험 test_m02_treepanel 8 · test_m02_layer_screen 문구 맞춤
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01D2nUMUMTsqm93PbQtipo6c
143 lines
4.8 KiB
TypeScript
143 lines
4.8 KiB
TypeScript
/* =============================================================================
|
|
* M02_MasterTemplete_TreePanel_Dialogs.ts
|
|
* 항목 나무 오른쪽 클릭 메뉴가 띄우는 작은 창 — 글 받기 · 옮길 폴더 고르기 · 저장/버림 물음.
|
|
* 모두 약속(Promise)으로 답함 — 닫기 · Esc · 바깥 누르기 = 취소(null / "cancel").
|
|
* ========================================================================== */
|
|
|
|
import {
|
|
createButton,
|
|
createInputField,
|
|
createSelectField,
|
|
el,
|
|
showToast,
|
|
} 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> = {};
|
|
for (const { f, field } of inputs) {
|
|
out[f.key] = field.input.value.trim();
|
|
if (!out[f.key] && !f.optional) return showToast(L("M02_NameNeeded"), "error");
|
|
}
|
|
finish(out);
|
|
close();
|
|
};
|
|
ok.addEventListener("click", submit);
|
|
for (const { field } of inputs) {
|
|
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();
|
|
},
|
|
});
|
|
});
|
|
}
|
|
|
|
/** 옮길 폴더 고르기 — `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] }));
|
|
},
|
|
});
|
|
});
|
|
}
|