- 걷음: 43784a4c0 의 옛 수식 나무 위 고침(서버 · 화면 · css) = 그 앞 판으로 되돌림
- 남김: master_equation_sign.py · ui_template_equation_text.ts · ui_template_equation_edit.ts(다시 쓸 조각 · 지금 안 부름) · text 는 `입력글` 을 제 타입에
- 지움: test_801_1_equation_inline.py — 걷은 옛 나무 고침을 재는 시험
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
217 lines
8.0 KiB
TypeScript
217 lines
8.0 KiB
TypeScript
/* =============================================================================
|
|
* ui_template_equation_line.ts
|
|
* 부품 「수식」 둘째 파일(796-2 · 800-2) — 수식 영역 줄 · 하위 로직 블록 · 여러 줄 이음. 식 그림 = `ui_template_equation.ts`.
|
|
* 줄 = 결과 변수명 = 이름 식 = 값 식 = 결과 값(단위) — 네 칸(PPT 꼴) · 줄 번호 · 구분선 · 줄마다 풀이 없음(변수 정의 = `ui_template_equation_defs.ts`)
|
|
* 값 식이 결과와 같으면 값 식 칸 생략 · 식을 못 읽은 줄 = 원문 + 까닭(붉은 글)
|
|
* 이음 = 여러 줄 위에서 아래로(`createEqChain`) · 하위 로직 부르기 = 줄 아래 접힘 블록(노 · 재 · 경 · 계 소계 · 펼치면 그 로직 줄 이음)
|
|
* 크기 = 글 `--text-body` · 접힘 단추 22
|
|
* 상태 = 바로 고침 — 이름 식 칸을 누르면 그 자리가 식 칸으로 바뀜(`onEdit` → 부르는 쪽이 `editing` 을 정해 다시 그림 · `editor` 가 칸을 줌) · 펼친 블록 `data-open`
|
|
* 쓰는 법 = `createEqChain(tree.줄, { path: "", open: new Set(), onVar, editing, onEdit, editor })`
|
|
* 대신한 옛 변종 = 시트 줄(이름 | 식 | 값 한 줄 · 분수 · 풀이 없음 · 799-2 에 걷음) · 줄 눌러 여는 아래 편집 칸(800-3 에 걷음)
|
|
* ========================================================================== */
|
|
|
|
import { createBadge } from "./ui_template_badge";
|
|
import { createButton } from "./ui_template_button";
|
|
import {
|
|
eqText,
|
|
plainEq,
|
|
renderEq,
|
|
symbolOf,
|
|
type EqHandlers,
|
|
type EqLine,
|
|
type EqNode,
|
|
} from "./ui_template_equation";
|
|
|
|
export interface EqLineOptions {
|
|
/** 블록 접힘 이름표 앞머리(블록 안 이음이 겹치지 않게) */
|
|
path: string;
|
|
/** 펼친 블록 이름표 모음 — 다시 그려도 유지 */
|
|
open: Set<string>;
|
|
/** 변수 누름(없으면 눌리지 않음) */
|
|
onVar?: (node: EqNode) => void;
|
|
/** 바로 고치는 줄 이름표(`lineId`) — 이 줄의 이름 식 칸 자리에 `editor` 칸이 들어감 */
|
|
editing?: string | null;
|
|
/** 이름 식 칸을 누름(없으면 고칠 수 없음) */
|
|
onEdit?: (line: EqLine, id: string) => void;
|
|
/** 고치는 줄의 식 칸 */
|
|
editor?: (line: EqLine, id: string) => HTMLElement;
|
|
/** 줄 바로 아래에 끼울 것(하위 로직 블록 소계 묶음 · 800-4) */
|
|
after?: (line: EqLine) => HTMLElement | null;
|
|
/** 결과 줄 값 뒤 단위 */
|
|
resultUnit?: string;
|
|
}
|
|
|
|
const BUNDLE: [string, string][] = [
|
|
["노무비", "노"],
|
|
["재료비", "재"],
|
|
["경비", "경"],
|
|
["계", "계"],
|
|
];
|
|
|
|
const span = (className: string, ...kids: (Node | string)[]): HTMLSpanElement => {
|
|
const node = document.createElement("span");
|
|
node.className = className;
|
|
node.append(...kids);
|
|
return node;
|
|
};
|
|
const div = (className: string, ...kids: Node[]): HTMLDivElement => {
|
|
const node = document.createElement("div");
|
|
node.className = className;
|
|
node.append(...kids);
|
|
return node;
|
|
};
|
|
|
|
/** 줄 이름표 — 호표 · 덧줄은 차례 · 그 밖은 열쇠 */
|
|
export const lineId = (line: EqLine): string =>
|
|
line.갈래 === "호표" || line.갈래 === "덧줄"
|
|
? `${line.갈래}:${line.차례 ?? 0}`
|
|
: `${line.갈래}:${line.열쇠}`;
|
|
|
|
function bundle(node: EqNode): HTMLElement {
|
|
const box = span("ui-eq__bundle");
|
|
const got = node.묶음;
|
|
if (got && typeof got === "object")
|
|
for (const [key, label] of BUNDLE)
|
|
if (got[key] !== undefined && got[key] !== null)
|
|
box.append(span("ui-eq__bundle-item", `${label} ${eqText(got[key])}`));
|
|
if (!box.childElementCount && node.값 !== undefined && node.값 !== null)
|
|
box.append(span("ui-eq__bundle-item", eqText(node.값)));
|
|
return box;
|
|
}
|
|
|
|
function blockPanel(node: EqNode, at: number, o: EqLineOptions): HTMLElement {
|
|
const id = `${o.path}/${node.열쇠 ?? at}`;
|
|
const body = div("ui-eq__block-body");
|
|
const box = div("ui-eq__blockpanel");
|
|
box.dataset.eqBlockPanel = node.열쇠 ?? "";
|
|
box.dataset.blockId = id;
|
|
const head = createButton({
|
|
label: `${String.fromCodePoint(0x245f + at)} ${node.이름 || node.열쇠 || ""}`,
|
|
variant: "ghost",
|
|
size: 22,
|
|
iconStart: "chevronRight",
|
|
onClick: () => toggle(),
|
|
});
|
|
head.classList.add("ui-eq__block-head");
|
|
const toggle = (force?: boolean): void => {
|
|
const on = force ?? !o.open.has(id);
|
|
if (on) {
|
|
o.open.add(id);
|
|
if (!body.childElementCount && node.나무)
|
|
body.append(
|
|
createEqChain(node.나무.줄, {
|
|
path: id,
|
|
open: o.open,
|
|
resultUnit: node.결과단위 ?? node.나무.결과단위,
|
|
}),
|
|
);
|
|
} else o.open.delete(id);
|
|
box.dataset.open = on ? "1" : "";
|
|
body.hidden = !on;
|
|
};
|
|
body.hidden = true;
|
|
box.append(div("ui-eq__block-row", head, bundle(node)), body);
|
|
if (o.open.has(id)) toggle(true);
|
|
return box;
|
|
}
|
|
|
|
/** M02 SK 고친 칸(796-2c) — 갈래 알약 + 원래 → 지금 · 뺀줄 알약 */
|
|
function markOf(line: EqLine): HTMLElement | null {
|
|
const kinds = line.표지 ?? [];
|
|
if (!kinds.length && !line.뺌) return null;
|
|
const box = span("ui-eq__mark");
|
|
if (kinds.length) box.append(createBadge(`고침 · ${kinds.join(" · ")}`, "warning", { size: 18 }));
|
|
if (line.뺌) box.append(createBadge("뺌", "neutral", { size: 18 }));
|
|
if (line.원래 !== undefined && line.원래 !== null)
|
|
box.append(
|
|
span(
|
|
"ui-eq__mark-was",
|
|
`원래 ${eqText(line.원래)}${line.뺌 ? "" : ` → 지금 ${eqText(line.지금) || "—"}`}`,
|
|
),
|
|
);
|
|
return box;
|
|
}
|
|
|
|
/** 줄 하나 */
|
|
export function createEqLine(line: EqLine, o: EqLineOptions): HTMLElement {
|
|
const id = lineId(line);
|
|
const root = div("ui-eq__line");
|
|
root.dataset.eqLine = id;
|
|
root.dataset.kind = line.갈래;
|
|
const blocks: EqNode[] = [];
|
|
const hand: EqHandlers = {
|
|
onVar: o.onVar,
|
|
blocks,
|
|
onBlock: (node) => {
|
|
root
|
|
.querySelector<HTMLElement>(
|
|
`[data-block-id="${CSS.escape(`${o.path}/${node.열쇠 ?? ""}`)}"] .ui-eq__block-head`,
|
|
)
|
|
?.click();
|
|
},
|
|
};
|
|
const body = div("ui-eq__body");
|
|
const head = line.결과;
|
|
if (head)
|
|
body.append(
|
|
span(
|
|
"ui-eq__box ui-eq__head",
|
|
head.꼴 !== "변수"
|
|
? plainEq(head)
|
|
: line.갈래 === "중간"
|
|
? renderEq(head, hand) // 누르면 변수 창
|
|
: symbolOf(head),
|
|
),
|
|
);
|
|
const resUnit =
|
|
line.갈래 === "호표" || line.갈래 === "덧줄"
|
|
? "원"
|
|
: line.갈래 === "결과"
|
|
? (o.resultUnit ?? "")
|
|
: (head?.단위 ?? "");
|
|
const result = span(
|
|
"ui-eq__box ui-eq__res",
|
|
line.글 || "—",
|
|
...(resUnit ? [span("ui-eq__unit", resUnit)] : []),
|
|
);
|
|
if (line.이름식) {
|
|
const name = span("ui-eq__box ui-eq__name");
|
|
if (o.editing === id && o.editor) {
|
|
name.classList.add("is-open");
|
|
name.append(o.editor(line, id));
|
|
} else {
|
|
name.append(renderEq(line.이름식, hand));
|
|
if (o.onEdit) {
|
|
name.classList.add("is-edit");
|
|
name.title = "눌러서 바로 고침";
|
|
name.addEventListener("click", () => o.onEdit?.(line, id));
|
|
}
|
|
}
|
|
body.append(span("ui-eq__op", "="), name);
|
|
const same =
|
|
plainEq(line.값식) === (line.글 ?? "") || plainEq(line.값식) === plainEq(line.이름식);
|
|
if (line.값식 && !same)
|
|
body.append(span("ui-eq__op", "="), span("ui-eq__box ui-eq__val", renderEq(line.값식)));
|
|
body.append(span("ui-eq__op", "="), result);
|
|
} else {
|
|
body.append(
|
|
span("ui-eq__op", "="),
|
|
span("ui-eq__box ui-eq__raw", line.식),
|
|
span("ui-eq__bad", line.까닭 ?? ""),
|
|
);
|
|
}
|
|
const mark = markOf(line);
|
|
if (mark) body.append(mark);
|
|
if (line.뺌) root.dataset.removed = "1";
|
|
root.append(div("ui-eq__main", body));
|
|
blocks.forEach((b, i) => root.append(blockPanel(b, i + 1, o)));
|
|
const extra = o.after?.(line);
|
|
if (extra) root.append(extra);
|
|
return root;
|
|
}
|
|
|
|
/** 여러 줄 위에서 아래로 이음 */
|
|
export function createEqChain(lines: EqLine[], o: EqLineOptions): HTMLElement {
|
|
return div("ui-eq", ...lines.map((l) => createEqLine(l, o)));
|
|
}
|