/* ============================================================================= * M02_SO_Def_Options.ts * 구조물 정의 편집 화면(PLAN 48-2) — 고른 종류의 **옵션 목록** * (이름 · 꼴 · 단위 · 기본값 · 보이기 · 들어갈 자리 · 안 씀 · 순서 · [옵션 더함]) + * 고르기 꼴의 **선택지 목록**(이름 바꿈 · 빼기 · 순서 · [선택지 더함]). * 옵션 키는 더할 때 자동 · 안 바뀜 · 선택지는 글이 곧 값(이름 바꿈 = 기본값 · 규칙도 같이) · * 저장은 화면 [저장](`M02_SO_Def.ts`). * ========================================================================== */ import { createButton, createTag, el, showConfirmDialog } from "@ui/ui_template_elements"; import { dropChoice, newKey, renameChoice, type OptionDef, type OptionForm, type StructDef, type TypeDef, } from "./M02_SO_Def_Model"; import { checkBox, head, moveButtons, pickBox, textBox } from "./M02_SO_Def_Ui"; import { buildPlaceSelect } from "./M02_SO_Groups"; import { to } from "./M02_SO_Text"; export interface OptionsBlockOptions { def: StructDef; type: TypeDef; edit: boolean; /** 선택지를 펼친 옵션 키 — 다시 그려도 남음 */ open: Set; changed: () => void; } const FORMS: readonly OptionForm[] = ["select", "number", "text", "bool"]; const FORM_TEXT = { select: "FormSelect", number: "FormNumber", text: "FormText", bool: "FormBool", } as const; /** 기본값 칸 — 꼴에 맞춤(고르기 = 선택지 · 예/아니오 · 숫자 · 글) · 비우면 기본값 없음 */ function defaultBox(o: OptionDef, edit: boolean, changed: () => void): HTMLElement { const attr = "data-sod-default"; const now = o.default == null ? "" : String(o.default); if (o.input === "select" || o.input === "bool") { const pairs: [string, string][] = o.input === "select" ? (o.choices ?? []).map((c) => [c, c]) : [ ["true", to("Yes")], ["false", to("No")], ]; return pickBox( [["", to("NoDefault")], ...pairs], now, (v) => { o.default = v === "" ? null : o.input === "bool" ? v === "true" : v; changed(); }, { edit, attr, cls: "m02-sod__default" }, ); } return textBox( now, (v) => { const n = Number(v); o.default = v.trim() === "" ? null : o.input === "number" && Number.isFinite(n) ? n : v; }, changed, { edit, attr, cls: "m02-sod__default", place: to("NoDefault") }, ); } /** 선택지 목록 — 이름(떠날 때 바꿈) · 빼기 · 순서 · [선택지 더함] */ function choicesBlock(opt: OptionsBlockOptions, o: OptionDef): HTMLElement { const { def, type, edit, changed } = opt; const list = (o.choices ??= []); const rows = list.map((c) => { let next = c; const del = createButton({ label: to("ChoiceDrop"), variant: "danger", size: "sm" }); del.addEventListener("click", async () => { if (!(await showConfirmDialog(to("ChoiceDropAsk", c), to("ChoiceDrop")))) return; dropChoice(def, type.type_id, o, c); changed(); }); return el("div", { className: "m02-sod__choice", attrs: { "data-sod-choice": c }, children: [ textBox( c, (v) => (next = v.trim()), () => { if (next && next !== c && !list.includes(next)) renameChoice(def, type.type_id, o, c, next); changed(); }, { edit, attr: "data-sod-choice-name" }, ), ...moveButtons(list, c, changed, edit), ...(edit ? [del] : []), ], }); }); const add = createButton({ label: to("ChoiceAdd"), variant: "ghost", size: "sm" }); add.setAttribute("data-sod-choice-add", ""); add.addEventListener("click", () => { let n = list.length + 1; while (list.includes(to("ChoiceNew", String(n)))) n += 1; list.push(to("ChoiceNew", String(n))); changed(); }); return el("div", { className: "m02-sod__choices", children: [ el("p", { className: "m02-side__empty", text: to("ChoiceNote") }), ...(rows.length ? rows : [el("p", { className: "m02-sod__bad", text: to("ChoiceNone") })]), ...(edit ? [add] : []), ], }); } /** 옵션 한 줄(+ 펼친 선택지) */ function optionRow(opt: OptionsBlockOptions, o: OptionDef): HTMLElement { const { def, type, edit, open, changed } = opt; const off = o.enabled === false; const hidden = def.표시[type.type_id]?.[o.key] === false; const form = pickBox( FORMS.map((f) => [f, to(FORM_TEXT[f])] as const), o.input, (v) => { o.input = v as OptionForm; o.default = null; // 꼴이 바뀌면 옛 기본값은 뜻이 없음 if (o.input === "select") open.add(o.key); changed(); }, { edit, attr: "data-sod-form", cls: "m02-sod__form" }, ); const choices = o.input === "select" ? createButton({ label: to("Choices", String(o.choices?.length ?? 0)), variant: open.has(o.key) ? "filled" : "ghost", size: "sm", }) : null; choices?.setAttribute("data-sod-choices", o.key); choices?.addEventListener("click", () => { if (open.has(o.key)) open.delete(o.key); else open.add(o.key); changed(); }); const show = checkBox( to("Shown"), !hidden, (v) => { const row = (def.표시[type.type_id] ??= {}); if (v) delete row[o.key]; else row[o.key] = false; if (!Object.keys(row).length) delete def.표시[type.type_id]; changed(); }, { edit, attr: "data-sod-shown" }, ); const row = el("div", { className: `m02-sod__option${off ? " is-off" : ""}`, attrs: { "data-sod-option": o.key }, children: [ textBox(o.label, (v) => (o.label = v), changed, { edit, attr: "data-sod-name", cls: "m02-sod__name", }), el("small", { className: "m02-opts__key", text: o.key }), ...(o.phase === "detail" ? [createTag(to("Detail"), "accent")] : []), form, textBox(o.unit ?? "", (v) => (o.unit = v.trim() || null), changed, { edit, attr: "data-sod-unit", cls: "m02-sod__unit", place: to("Unit"), max: 12, }), defaultBox(o, edit, changed), show, buildPlaceSelect((def.그룹[type.type_id] ??= []), o.key, edit, changed), checkBox(to("Unused"), off, (v) => ((o.enabled = !v), changed()), { edit, attr: "data-sod-off", }), ...moveButtons(type.options, o, changed, edit), ...(choices ? [choices] : []), ], }); return o.input === "select" && open.has(o.key) ? el("div", { className: "m02-sod__optwrap", children: [row, choicesBlock(opt, o)] }) : row; } /** 옵션 목록 칸 — 제목 · 안내 · 옵션 줄 · [옵션 더함] */ export function buildOptionsBlock(opt: OptionsBlockOptions): HTMLElement { const { type, edit, changed } = opt; const add = createButton({ label: to("OptionAdd"), variant: "ghost", size: "sm" }); add.setAttribute("data-sod-option-add", ""); add.addEventListener("click", () => { const n = type.options.length + 1; const o: OptionDef = { key: newKey( "opt_", type.options.map((x) => x.key), ), label: to("OptionNew", String(n)), input: "select", choices: [to("ChoiceNew", "1")], default: null, }; type.options.push(o); opt.open.add(o.key); changed(); }); return el("div", { className: "m02-sod__block", children: [ ...head(to("OptionHead"), to("OptionNote")), ...(type.options.length ? type.options.map((o) => optionRow(opt, o)) : [el("p", { className: "m02-side__empty", text: to("NoOptionKind") })]), ...(edit ? [el("div", { className: "m02-side__row", children: [add] })] : []), ], }); }