/* ============================================================================= * M02_SO_Def_Ui.ts * 구조물 정의 편집 화면(PLAN 48-2)의 작은 입력 부품 — 글 · 숫자 · 체크 · 고르기 · 위아래 단추. * 값은 받은 손잡이로 사본만 고침 · 글은 칸을 떠날 때(`change`) 다시 그림(초점 안 끊김). * ========================================================================== */ import { createButton, el } from "@ui/ui_template_elements"; import { step } from "./M02_SO_Def_Model"; import { to } from "./M02_SO_Text"; /** 글 칸 — 글자마다 `set` · 떠날 때 `done` */ export function textBox( value: string, set: (v: string) => void, done: () => void, opt: { edit: boolean; attr?: string; place?: string; cls?: string; max?: number }, ): HTMLInputElement { const box = el("input", { className: `ui-input ui-input--sm ${opt.cls ?? ""}`, attrs: { type: "text", maxlength: String(opt.max ?? 40), ...(opt.attr ? { [opt.attr]: "" } : {}), }, }) as HTMLInputElement; box.value = value; box.placeholder = opt.place ?? ""; box.disabled = !opt.edit; box.addEventListener("input", () => { set(box.value); box.classList.toggle("is-invalid", !box.value.trim() && !opt.place); }); box.addEventListener("change", done); return box; } /** 숫자 칸 — 범위 밖 · 빈 칸은 무름 */ export function numberBox( value: number, set: (v: number) => void, opt: { edit: boolean; min: number; max: number; attr?: string }, ): HTMLInputElement { const box = el("input", { className: "ui-input ui-input--sm m02-sod__num", attrs: { type: "number", min: String(opt.min), max: String(opt.max), ...(opt.attr ? { [opt.attr]: "" } : {}), }, }) as HTMLInputElement; box.value = String(value); box.disabled = !opt.edit; box.addEventListener("change", () => { const n = Math.round(Number(box.value)); if (box.value === "" || !Number.isFinite(n) || n < opt.min || n > opt.max) { box.value = String(value); return; } set(n); }); return box; } /** 체크 + 글 */ export function checkBox( label: string, on: boolean, set: (v: boolean) => void, opt: { edit: boolean; attr?: string }, ): HTMLLabelElement { const box = el("input", { attrs: { type: "checkbox", ...(opt.attr ? { [opt.attr]: "" } : {}) }, }) as HTMLInputElement; box.checked = on; box.disabled = !opt.edit; box.addEventListener("change", () => set(box.checked)); return el("label", { className: "m02-sod__check", children: [box, el("span", { text: label })] }); } /** 고르기 — [값, 글] 짝 */ export function pickBox( pairs: readonly (readonly [string, string])[], value: string, set: (v: string) => void, opt: { edit: boolean; attr?: string; cls?: string }, ): HTMLSelectElement { const box = el("select", { className: `ui-input ui-input--sm ${opt.cls ?? ""}`, attrs: opt.attr ? { [opt.attr]: "" } : {}, }) as HTMLSelectElement; box.append(...pairs.map(([v, t]) => new Option(t, v))); box.value = value; box.disabled = !opt.edit; box.addEventListener("change", () => set(box.value)); return box; } /** 위로 · 아래로 단추 둘 — 고치는 사람만 · 옮겼으면 `changed` */ export function moveButtons( list: T[], item: T, changed: () => void, edit: boolean, ): HTMLElement[] { if (!edit) return []; const index = list.indexOf(item); const make = (label: string, delta: number, off: boolean): HTMLButtonElement => { const b = createButton({ label, variant: "ghost", size: "sm" }); b.disabled = off; b.addEventListener("click", () => step(list, list.indexOf(item), delta) && changed()); return b; }; return [make(to("GroupUp"), -1, index <= 0), make(to("GroupDown"), 1, index >= list.length - 1)]; } /** 절 제목 + 안내 한 줄 */ export const head = (title: string, note?: string): HTMLElement[] => [ el("h4", { className: "m02-opts__group", text: title }), ...(note ? [el("p", { className: "m02-side__empty", text: note })] : []), ];