feat(804-6): 옛 수식 탭 걷음 · 줄 쌓기 = 첫 「수식」 탭(요구 686-1)
- M01 단가산출 기초 · M02 단가산출 상세 보기 = 수식(줄 쌓기 · 기본) · 단가산출 상세 둘 - 줄 쌓기에 M02 사본 [단가 갱신] · [재생성](미리 셈 → 알림 → 저장 · 고침 있으면 막음) · 판 바뀜(409) 문서 그대로면 새 판으로 다시 저장 옮김 - 옛 수식 문서 화면 걷음(formula_doc_screen · defs · edit · lines · math · math_model · rows · sub · formula_doc.css) · 이미 안 부르던 796-2 수식 나무 화면(LogicLab_Equation · Defs · Var · css · M01_Logic_Equation_Model · sheet_formula_cell · ui_template_equation_defs · edit) 걷음 — 18 파일 4,860 줄 - formula_doc_render = 줄 쌓기가 쓰는 reach 만 · 옵션 가로 CSS 는 line_stack.css 로 - 남김 = 계산기 common_util_formula_doc* · 서버 길 · 옵션 · 사본 표지 · 합계 상자 · KaTeX · 시험 페이지 tools/line-stack-test - 시험: test_796_2 지움(걷은 화면 원본 검사뿐) · 799_1 · 800_2 걷은 화면 줄만 뺌 · 799_2 · 802_4b2 · 804_2 보기 · 화면 파일 대상 바꿈 · test_804_6 새로(걷은 파일 · 부르는 곳 없음 · 첫 탭 · 사본 할일) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,93 +0,0 @@
|
||||
/* =============================================================================
|
||||
* ui_template_equation_defs.ts
|
||||
* 부품 「수식」 넷째 파일(800-2 · 요구 660-2 · 3 · 6) — 오른쪽 「변수 정의 영역」.
|
||||
* 한 덩어리 목록 · 같은 기호는 한 줄(서버가 합침) · 줄 = 기호 : 국문 이름 — 연결 설명(입력 · 표 찾기 · 요소 · 로직 · 계산을 사람 말로)
|
||||
* 값 · 단위 · DB 열쇠 글은 안 그림(값은 왼쪽 값 식에 있음) · 고른 옵션 갈래의 변수만(서버 `정의`)
|
||||
* 상태 = 고침 열린 줄 `data-editing`(줄 아래 `editor` 칸) · 연결 안 한 새 기호 줄 `data-draft`(`drafts` 요소 — 부르는 쪽이 만듦) · 눌러 보인 줄 `data-flash`
|
||||
* 쓰는 법 = `createEqDefs(defs, { editing, onEdit, editor, drafts, head })` → 요소 · `flashEqDef(root, node)` = 식의 변수를 눌렀을 때 그 정의 줄로 이동
|
||||
* ========================================================================== */
|
||||
|
||||
import { symbolOf, type EqDef, type EqNode } from "./ui_template_equation";
|
||||
|
||||
export interface EqDefsOptions {
|
||||
/** 고침이 열린 정의의 기호 글(`defSign`) */
|
||||
editing?: string | null;
|
||||
/** 정의 줄을 누름(없으면 고칠 수 없음 — 고침 가능한 줄만 눌림) */
|
||||
onEdit?: (def: EqDef) => void;
|
||||
/** 고침이 열린 줄 아래 칸 */
|
||||
editor?: (def: EqDef) => HTMLElement;
|
||||
/** 연결을 지정할 새 기호 줄들 */
|
||||
drafts?: HTMLElement[];
|
||||
/** 머리 오른쪽 단추 자리 */
|
||||
head?: HTMLElement[];
|
||||
}
|
||||
|
||||
const node = (tag: "div" | "span", className: string, text = ""): HTMLElement => {
|
||||
const out = document.createElement(tag);
|
||||
out.className = className;
|
||||
if (text) out.textContent = text;
|
||||
return out;
|
||||
};
|
||||
|
||||
/** 정의의 기호 글(기호 · 아래 · 위) — 줄 이름표 · 식의 변수와 맞대는 열쇠 */
|
||||
export const defSign = (d: Pick<EqDef, "기호" | "위" | "아래">): string =>
|
||||
`${d.기호}${d.아래 ? `_${d.아래}` : ""}${d.위 ? `^${d.위}` : ""}`;
|
||||
|
||||
/** 식 변수 마디의 기호 글 — `defSign` 과 같은 꼴 */
|
||||
export const nodeSign = (n: Pick<EqNode, "기호" | "열쇠" | "위" | "아래">): string =>
|
||||
defSign({
|
||||
기호: n.기호 || n.열쇠 || "",
|
||||
위: typeof n.위 === "string" ? n.위 : "",
|
||||
아래: typeof n.아래 === "string" ? n.아래 : "",
|
||||
});
|
||||
|
||||
function defRow(d: EqDef, o: EqDefsOptions): HTMLElement {
|
||||
const sign = defSign(d);
|
||||
const row = node("div", "ui-eq__def");
|
||||
row.dataset.eqDef = sign;
|
||||
row.dataset.kind = d.갈래;
|
||||
const main = node("div", "ui-eq__def-main");
|
||||
main.append(symbolOf({ 열쇠: d.열쇠, 기호: d.기호, 위: d.위, 아래: d.아래 }));
|
||||
if (d.이름 && d.이름 !== d.기호)
|
||||
main.append(node("span", "ui-eq__op", ":"), node("span", "ui-eq__def-name", d.이름));
|
||||
if (d.안씀) main.append(node("span", "ui-eq__def-tag", "쓰는 식 없음"));
|
||||
const links = node("div", "ui-eq__def-links");
|
||||
for (const text of d.연결) links.append(node("div", "ui-eq__def-link", text));
|
||||
const top = node("div", "ui-eq__def-top", "");
|
||||
top.append(main, links);
|
||||
if (o.onEdit && d.고침) {
|
||||
row.classList.add("is-pick");
|
||||
top.title = "눌러서 이름 · 기호 · 첨자 고침 · 지움";
|
||||
top.addEventListener("click", () => o.onEdit?.(d));
|
||||
}
|
||||
row.append(top);
|
||||
if (o.editing === sign && o.editor) {
|
||||
row.dataset.editing = "1";
|
||||
row.append(o.editor(d));
|
||||
}
|
||||
return row;
|
||||
}
|
||||
|
||||
export function createEqDefs(defs: EqDef[], o: EqDefsOptions = {}): HTMLElement {
|
||||
const root = node("div", "ui-eq__defs");
|
||||
root.dataset.eqDefs = "";
|
||||
const head = node("div", "ui-eq__defs-head");
|
||||
head.append(node("span", "ui-eq__defs-title", "변수 정의"), ...(o.head ?? []));
|
||||
root.append(head);
|
||||
for (const draft of o.drafts ?? []) root.append(draft); // 새 기호는 맨 위 — 식을 고치는 곳 옆에서 바로 보이게
|
||||
for (const d of defs) root.append(defRow(d, o));
|
||||
if (!defs.length && !o.drafts?.length) root.append(node("div", "ui-eq__def-empty", "—"));
|
||||
return root;
|
||||
}
|
||||
|
||||
/** 식의 변수를 눌렀을 때 — 그 정의 줄로 이동 · 잠깐 표시 */
|
||||
export function flashEqDef(root: HTMLElement, n: EqNode): void {
|
||||
const sign = nodeSign(n);
|
||||
const row = [...root.querySelectorAll<HTMLElement>("[data-eq-def]")].find(
|
||||
(r) => r.dataset.eqDef === sign,
|
||||
);
|
||||
if (!row) return;
|
||||
row.scrollIntoView({ block: "nearest", behavior: "smooth" });
|
||||
row.dataset.flash = "1";
|
||||
window.setTimeout(() => delete row.dataset.flash, 1200);
|
||||
}
|
||||
@@ -1,136 +0,0 @@
|
||||
/* =============================================================================
|
||||
* ui_template_equation_edit.ts
|
||||
* 부품 「수식」 바로 고침(801-1 · 요구 662-1 · 2) — 보이는 조판 수식 그 자리에서 고침(따로 열리는 칸 · 창 없음).
|
||||
* 기호 식 = `editEq(host, …)` — 그린 식이 그대로 글 고침 자리(커서 · 지움 · + − × ÷ / ( ) · `_` 아래첨자 · `^` 위첨자)
|
||||
* · 변수 · 표 찾기 · 요소 · 로직 부름 · 숨긴 감싸개 = 원자(통째 지움 · 누르면 연결) · Enter 넣음 · Esc 버림
|
||||
* · 분수 · 첨자 꼴은 넣은 뒤 서버 나무로 다시 그림(`/` = 분수 · `Q_d` = 첨자)
|
||||
* 값 식 = `editNumber(span, …)` — 숫자 하나를 그 자리에서(Enter · 다른 데 누름 = 넣음 · Esc 버림)
|
||||
* 글 꼴 = `serializeEq` — 그림 DOM → 키보드 한 줄 식(원자 `data-tok` · 감싸개 `data-pre` · `data-post` · 분수 · 괄호)
|
||||
* 상태 = 고치는 자리 `data-editing` · 틀린 식 `data-bad`(빨강 밑줄)
|
||||
* ========================================================================== */
|
||||
|
||||
const OP = /^[×÷−+\-*/=<>≤≥≠,]$|^(그리고|또는)$/;
|
||||
|
||||
/** 숫자 글 → 키보드 수(천 단위 쉼표 걷음) */
|
||||
const plain = (text: string): string =>
|
||||
text.replace(/ /g, " ").replace(/(\d),(?=\d{3}(?!\d))/g, "$1");
|
||||
|
||||
/** 그림 DOM → 키보드 한 줄 식 */
|
||||
export function serializeEq(node: Node): string {
|
||||
if (node.nodeType === Node.TEXT_NODE) return plain(node.textContent ?? "");
|
||||
if (!(node instanceof HTMLElement)) return "";
|
||||
const kids = (el: Element | null): string =>
|
||||
el ? [...el.childNodes].map(serializeEq).join("") : "";
|
||||
const d = node.dataset;
|
||||
if (d.tok !== undefined) return ` ${d.tok} `;
|
||||
if (d.pre !== undefined) return `${d.pre}${kids(node)}${d.post ?? ""}`;
|
||||
const cls = node.classList;
|
||||
if (cls.contains("ui-eq__unit") || node.tagName === "BR") return "";
|
||||
if (cls.contains("ui-eq__frac")) {
|
||||
const [top, low] = [node.children[0] ?? null, node.children[1] ?? null];
|
||||
return `(${kids(top)}) / (${kids(low)})`;
|
||||
}
|
||||
if (cls.contains("ui-eq__paren")) return `(${kids(node)})`;
|
||||
if (cls.contains("ui-eq__op")) {
|
||||
const op = node.textContent ?? "";
|
||||
return OP.test(op) ? ` ${op} ` : op;
|
||||
}
|
||||
return kids(node);
|
||||
}
|
||||
|
||||
/** 한 줄 식 글 다듬기(빈칸 하나로) */
|
||||
export const tidyEq = (text: string): string => text.replace(/\s+/g, " ").trim();
|
||||
|
||||
export interface EqEditHooks {
|
||||
/** 칠 때마다(쉼 뒤) — 검사 · 미리 셈 */
|
||||
onInput: (text: string) => void;
|
||||
/** Enter — 넣음 */
|
||||
onCommit: (text: string) => void;
|
||||
/** Esc · 안 바꾸고 다른 데 누름 — 닫음 */
|
||||
onCancel: () => void;
|
||||
/** 누른 자리(그 자리에 커서) */
|
||||
at?: { x: number; y: number } | null;
|
||||
}
|
||||
|
||||
/** 그린 식(`host` 안)을 그 자리 고침으로 — 처음 글 돌려줌 */
|
||||
export function editEq(host: HTMLElement, h: EqEditHooks): string {
|
||||
host.contentEditable = "true";
|
||||
host.spellcheck = false;
|
||||
host.dataset.editing = "1";
|
||||
host.querySelectorAll<HTMLElement>("[data-tok]").forEach((a) => {
|
||||
a.contentEditable = "false";
|
||||
});
|
||||
const first = tidyEq(serializeEq(host));
|
||||
const now = (): string => tidyEq(serializeEq(host));
|
||||
let timer = 0;
|
||||
host.addEventListener("beforeinput", (ev) => {
|
||||
if (ev.inputType === "insertText" && ev.data === "*") {
|
||||
ev.preventDefault();
|
||||
document.execCommand("insertText", false, "×");
|
||||
}
|
||||
if (ev.inputType === "insertParagraph" || ev.inputType === "insertLineBreak")
|
||||
ev.preventDefault();
|
||||
});
|
||||
host.addEventListener("input", () => {
|
||||
window.clearTimeout(timer);
|
||||
timer = window.setTimeout(() => h.onInput(now()), 250);
|
||||
});
|
||||
host.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Enter") {
|
||||
ev.preventDefault();
|
||||
window.clearTimeout(timer);
|
||||
h.onCommit(now());
|
||||
} else if (ev.key === "Escape") {
|
||||
ev.preventDefault();
|
||||
h.onCancel();
|
||||
}
|
||||
});
|
||||
host.addEventListener("focusout", () =>
|
||||
window.setTimeout(() => {
|
||||
if (!host.isConnected || host.contains(document.activeElement)) return;
|
||||
if (now() === first) h.onCancel();
|
||||
}, 150),
|
||||
);
|
||||
queueMicrotask(() => {
|
||||
host.focus();
|
||||
const sel = window.getSelection();
|
||||
const pt = h.at;
|
||||
const range =
|
||||
pt && document.caretRangeFromPoint ? document.caretRangeFromPoint(pt.x, pt.y) : null;
|
||||
if (range && host.contains(range.startContainer)) {
|
||||
sel?.removeAllRanges();
|
||||
sel?.addRange(range);
|
||||
} else {
|
||||
sel?.selectAllChildren(host);
|
||||
sel?.collapseToEnd();
|
||||
}
|
||||
});
|
||||
return first;
|
||||
}
|
||||
|
||||
/** 값 식 숫자 하나를 그 자리에서 고침 — 바뀐 글이면 `done(새 글)` */
|
||||
export function editNumber(num: HTMLElement, done: (text: string) => void): void {
|
||||
const before = plain(num.textContent ?? "");
|
||||
num.textContent = before;
|
||||
num.contentEditable = "true";
|
||||
num.dataset.editing = "1";
|
||||
let over = false;
|
||||
const finish = (keep: boolean): void => {
|
||||
if (over) return;
|
||||
over = true;
|
||||
num.contentEditable = "false";
|
||||
delete num.dataset.editing;
|
||||
const text = plain(num.textContent ?? "").trim();
|
||||
if (keep && text && text !== before) done(text);
|
||||
else num.textContent = before;
|
||||
};
|
||||
num.addEventListener("keydown", (ev) => {
|
||||
if (ev.key === "Enter" || ev.key === "Escape") {
|
||||
ev.preventDefault();
|
||||
finish(ev.key === "Enter");
|
||||
}
|
||||
});
|
||||
num.addEventListener("blur", () => finish(true), { once: true });
|
||||
num.focus();
|
||||
window.getSelection()?.selectAllChildren(num);
|
||||
}
|
||||
Reference in New Issue
Block a user