- 세 영역 가로 — 기호 식(줄바꿈으로 이어진 하나의 수식 영역) | 값 식(같은 높이 줄) | 기호 정의 - 맨 아래 합계 상자 = 단가산출 상세 탭 부품(노무비 · 재료비 · 경비 · 계) · 옵션 아래 계 줄 · 설명 글 · 표지 안내 줄 걷음(오류 = 그 기호 빨강) - 기호 식 줄 누름 = 그 자리 편집기(치는 대로 조판 — / 분수 · _ 아래첨자 · ^ 위첨자 · 괄호 · 화살표 구조 이동 · Enter 새 줄 · Esc 나옴 · 한글 조합) · 편집 중만 영역 위 수식 도구 줄 - 금액 줄 = 영역 안 식 줄 「노무비 형틀목공 = w × Q」(머리 첫 말 = 비목) · 「+ 식 줄」 「+ 금액 줄」 「+ 묶음」 · 비목 고르기 칸 걷음 · 묶음 = 도구 줄 [묶음] - 저장 전 새 로직 = 세 영역 빈 화면 · 끊은 단가산출 블록도 같은 편집기 - 새 라이브러리 없음 · 서버 길 · 계산기 · 문서 꼴 그대로 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
200 lines
8.1 KiB
TypeScript
200 lines
8.1 KiB
TypeScript
/* =============================================================================
|
|
* 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");
|
|
});
|
|
},
|
|
};
|
|
}
|