/* ============================================================================= * formula_doc_rows.ts * 수식 영역 줄 편집 흐름(PLAN 803-2a · 요구 667-4 · 6) — 어느 줄을 편집 중인지 · 줄 사이 옮김 · 새 줄 · 문서에 넣기 * 줄 누름 = 편집 중 글을 넣고 그 줄을 엶 · Enter = 아래 새 줄(금액 줄 뒤면 같은 비목 「노무비 」로 시작) · * ↑ ↓ = 옆 줄(가로 자리 그대로) · 빈 줄 맨 앞 지움 = 줄 걷고 윗줄 끝 · Esc · 밖 누름 = 나옴 · 빈 줄은 나올 때 걷음 * 글 → 문서 = `editRow`(식 줄 · 금액 줄 「노무비 형틀목공 = w × Q」 · 갈래가 바뀌면 옮김) * 그림 · 셈은 부른 쪽(`formula_doc_screen.ts` — `change` · `draw`) · 편집기는 `formula_doc_math.ts` * ========================================================================== */ import type { FormulaDoc, MoneyLine } from "../common_util/common_util_formula_doc"; import { editRow, splitLine, sweep } from "./formula_doc_edit"; import type { RowRef } from "./formula_doc_lines"; import { openMath, type Leave, type MathEditor, type MathHooks } from "./formula_doc_math"; import { tryParse } from "./formula_doc_render"; import { ft } from "./formula_doc_text"; interface Editing { ref: RowRef; at?: MathHooks["at"]; /** 처음 글(새 금액 줄 「노무비 」) */ seed?: string; } interface Current { ref: RowRef; first: string; ed: MathEditor | null; } export interface RowsHost { doc: () => FormulaDoc | null; /** 문서 고침 → 캐시 · 셈 · 그림 */ change: (fn: (d: FormulaDoc) => void) => void; /** 고침 없이 캐시 · 셈(그림 없음) */ settle: () => void; draw: () => void; root: HTMLElement; } export function createRows(host: RowsHost) { let editing: Editing | null = null; let current: Current | null = null; let order: RowRef[] = []; let live: { ref: RowRef; text: string; first: string } | null = null; // 다시 그림 사이 편집 중 글 const doc = (): FormulaDoc | null => host.doc(); const rowObj = (r: RowRef): object | undefined => (r.kind === "line" ? doc()?.식 : doc()?.금액)?.[r.i]; const refOf = (o: object | null | undefined): RowRef | null => { const a = (doc()?.식 ?? []).indexOf(o as never); if (a >= 0) return { kind: "line", i: a }; const b = (doc()?.금액 ?? []).indexOf(o as never); return b >= 0 ? { kind: "money", i: b } : null; }; /** 편집 중 글을 문서에 — 돌려줌 = 그 줄 객체 */ const commit = (d: FormulaDoc, c: Current, text: string): object | null => text === c.first ? (rowObj(c.ref) ?? null) : editRow(d, c.ref.kind, c.ref.i, text); /** 새 줄 — `after` 뒤(식 줄 = 식 줄 · 금액 줄 = 같은 비목 · 묶음) · null = 영역 맨 끝 */ const newRow = (d: FormulaDoc, after: object | null): { obj: object; seed: string } => { const lines = (d.식 ??= []); const money = (d.금액 ??= []); const li = after ? lines.indexOf(after as never) : -1; const mi = after ? money.indexOf(after as MoneyLine) : -1; const base = mi >= 0 ? money[mi] : li < 0 ? money[money.length - 1] : undefined; if (!base) { const o = { 기호: "", 식: "" }; lines.splice(li >= 0 ? li + 1 : lines.length, 0, o); return { obj: o, seed: "" }; } const kind = base.비목 ?? "노무비"; const o: MoneyLine = { 이름: "", 비목: kind, 단가: "", ...(base.묶음 ? { 묶음: base.묶음 } : {}), }; money.splice(mi >= 0 ? mi + 1 : money.length, 0, o); return { obj: o, seed: `${kind} ` }; }; const target = (obj: object | null, at?: MathHooks["at"], seed?: string): void => { const ref = refOf(obj); editing = ref ? { ref, at, ...(seed ? { seed } : {}) } : null; }; const take = (): Current | null => { const c = current; current = live = null; c?.ed?.close(); return c; }; /** 줄 누름 — 편집 중 글을 넣고 그 줄(또는 맨 끝 새 줄)을 엶 */ const editAt = (ref: RowRef | "end", at?: { x: number; y: number }): void => { const c = take(); const want = ref === "end" ? null : (rowObj(ref) ?? null); host.change((d) => { if (c) commit(d, c, c.ed?.text() ?? c.first); const made = ref === "end" ? newRow(d, null) : null; sweep(d, made?.obj ?? want); target(made?.obj ?? want, at ?? "end", made?.seed); }); }; /** 편집기에서 나옴 — Enter = 새 줄 · ↑ ↓ = 옆 줄 · 지움 = 빈 줄 걷고 윗줄 끝 */ const finish = (c: Current, text: string, how: Leave, x?: number): void => { if (current === c) current = null; live = null; const at = order.findIndex((r) => r.kind === c.ref.kind && r.i === c.ref.i); const near = how === "down" ? order[at + 1] : how === "up" || how === "back" ? order[at - 1] : undefined; const nearObj = near ? (rowObj(near) ?? null) : null; const d = doc(); if (how === "exit" && text === c.first && d) { editing = null; const before = JSON.stringify(d); sweep(d); if (JSON.stringify(d) !== before) host.settle(); // 값 식 숫자 등 다른 자리를 고치는 중이면 그대로(다음 그림에서 정리) if (!host.root.querySelector("[data-editing]:not(.fdoc-math)")) host.draw(); return; } host.change((dd) => { const mine = commit(dd, c, text); const made = how === "enter" ? newRow(dd, mine) : null; sweep(dd, made?.obj ?? nearObj); const pos: MathHooks["at"] = how === "up" || how === "down" ? { x: x ?? 0 } : how === "back" ? "end" : "start"; target(made?.obj ?? nearObj, pos, made?.seed); }); }; return { editing: (): RowRef | null => editing?.ref ?? null, editingMoney: (): boolean => editing?.ref.kind === "money", setOrder: (o: RowRef[]): void => void (order = o), editAt, /** 다시 그리기 앞 — 편집 중 글을 기억(같은 줄을 다시 열 때 이어 씀) */ beforeDraw: (): void => { const c = current; if (!c?.ed) return; live = { ref: c.ref, text: c.ed.text(), first: c.first }; c.ed.close(); current = null; }, /** 편집 중 글을 문서에 넣고 닫음(그림 없음 · 저장 · 나가기 앞) */ flush: (): void => { const c = take(); editing = null; const d = doc(); if (!c || !d) return; commit(d, c, c.ed?.text() ?? c.first); sweep(d); host.settle(); }, /** 편집 버림(문서 바뀜 · 고침 버림) */ reset: (): void => { take(); editing = null; }, mount: (el: HTMLElement, ref: RowRef, text: string): void => { const e = editing; if (!e) return; const again = live && live.ref.kind === ref.kind && live.ref.i === ref.i ? live : null; live = null; const first = again?.first ?? e.seed ?? text; const c: Current = { ref, first, ed: null }; current = c; queueMicrotask(() => { if (current !== c || !el.isConnected) return; c.ed = openMath(el, { text: again?.text ?? first, at: again ? "end" : (e.at ?? "end"), onInput: (t) => { const body = splitLine(t)[1]; el.toggleAttribute("data-bad", !!body.trim() && !tryParse(body)); }, onLeave: (t, how, x) => finish(c, t, how, x), }); }); }, /** 편집 중 금액 줄부터 그 묶음 끝까지 새 묶음(이름은 머리를 눌러 고침) */ splitGroup: (): void => { const c = current; if (!c || c.ref.kind !== "money") return; take(); host.change((d) => { const mine = commit(d, c, c.ed?.text() ?? c.first) as MoneyLine | null; const money = d.금액 ?? []; const at = mine ? money.indexOf(mine) : -1; if (at < 0) return void (editing = null); const old = mine!.묶음 ?? ""; const taken = new Set(money.map((l) => l.묶음 ?? "")); let name = ft("New_Group"); for (let n = 2; taken.has(name); n++) name = `${ft("New_Group")} ${n}`; for (let k = at; k < money.length && (money[k].묶음 ?? "") === old; k++) money[k].묶음 = name; target(mine, "end"); }); }, }; }