- ui_template_pager.ts: M01 buildPager 이동 + 화면 높이 줄 수 셈(fitRows) · 첫 줄 유지(pageKeepFirst) · 화면 크기 알림(watchFit) - ui_template_divider.ts + overlay.css .ui-divider: 수평 구분선 - M01 buildPager = 새 부품을 부르는 옛 이름(PickTable · M02 목록 그대로) - 부품 모음 견본 두 줄 · 지킴 시험 test_709_pager_fit.py
114 lines
4.4 KiB
TypeScript
114 lines
4.4 KiB
TypeScript
/* =============================================================================
|
||
* M01_MasterData_UI_Cells.ts
|
||
* 칸 다루기 — 값 → 글 · 글 → 값 · 눌러 고치기 (요소·표 화면 공용) · 쪽 단추 옛 이름
|
||
* ========================================================================== */
|
||
|
||
import { el } from "@ui/ui_template_elements";
|
||
import { createPager } from "@ui/ui_template_pager";
|
||
import type { Row } from "./M01_MasterData_Api_Fetch";
|
||
|
||
/** 묶음 값(`값 = {가격: …}`)을 「값›가격」 칸으로 펼칠 때의 구분자. */
|
||
export const SEP = "›";
|
||
|
||
export const isScalar = (v: unknown): boolean =>
|
||
v === null || v === undefined || ["string", "number", "boolean"].includes(typeof v);
|
||
|
||
/** 줄 → 칸 이름별 값. 묶음(dict) 칸은 한 단계 펼침. */
|
||
export function flatten(row: Row): Record<string, unknown> {
|
||
const out: Record<string, unknown> = {};
|
||
for (const [k, v] of Object.entries(row)) {
|
||
if (v && typeof v === "object" && !Array.isArray(v)) {
|
||
for (const [k2, v2] of Object.entries(v)) out[`${k}${SEP}${k2}`] = v2;
|
||
} else {
|
||
out[k] = v;
|
||
}
|
||
}
|
||
return out;
|
||
}
|
||
|
||
/** 칸 이름(펼친 것 포함)에 값을 넣은 새 줄. */
|
||
export function withCell(row: Row, path: string, value: unknown): Row {
|
||
const [head, sub] = path.split(SEP);
|
||
if (sub === undefined) return { ...row, [head]: value };
|
||
return { ...row, [head]: { ...(row[head] as Row), [sub]: value } };
|
||
}
|
||
|
||
/** 줄 안 깊은 자리 읽기 · 넣은 새 줄(중간 묶음 없으면 만듦). */
|
||
export const deep = (row: Row, path: string[]): unknown =>
|
||
path.reduce<unknown>((o, k) => (o as Row | null | undefined)?.[k], row);
|
||
|
||
export function withDeep(row: Row, path: string[], value: unknown): Row {
|
||
const [head, ...rest] = path;
|
||
if (!rest.length) return { ...row, [head]: value };
|
||
return { ...row, [head]: withDeep((row[head] ?? {}) as Row, rest, value) };
|
||
}
|
||
|
||
export function show(v: unknown): string {
|
||
if (v === null || v === undefined) return "";
|
||
if (Array.isArray(v)) {
|
||
const sep = v.every((x) => typeof x === "string") ? " · " : " ~ "; // 글 목록(옛이름) · 수 범위
|
||
return v.map((x) => (x === null ? "" : String(x))).join(sep);
|
||
}
|
||
return typeof v === "object" ? JSON.stringify(v) : String(v);
|
||
}
|
||
|
||
const NUMBER = /^-?\d+(\.\d+)?$/;
|
||
|
||
/** 글 → 수(수 꼴이면) 아니면 글. 빈 글 = null. */
|
||
export function toNumberish(text: string): number | string | null {
|
||
const s = text.replace(/,/g, "").trim();
|
||
if (s === "") return null;
|
||
return NUMBER.test(s) ? Number(s) : text.trim();
|
||
}
|
||
|
||
/** 원래 값의 꼴을 따라 글을 값으로 — 글이었으면 글 · 수·빈 칸이었으면 수 꼴이면 수. */
|
||
export const coerce = (text: string, orig: unknown): unknown =>
|
||
typeof orig === "string" ? text : toNumberish(text);
|
||
|
||
/** 「아래 ~ 위」 → [아래, 위] (빈 쪽 = null · 「~」 없으면 한 값). */
|
||
export function toRange(text: string): unknown {
|
||
if (!text.includes("~")) return toNumberish(text);
|
||
return text.split("~").map((s) => (s.trim() === "" ? null : toNumberish(s)));
|
||
}
|
||
|
||
/** 칸을 눌러 고침 — Enter·칸 밖 누름 = 확정 · Esc = 취소. */
|
||
export function startEdit(td: HTMLElement, text: string, commit: (value: string) => void): void {
|
||
if (td.querySelector("input")) return;
|
||
const input = el("input", { className: "m01-master__cell-input", attrs: { type: "text" } });
|
||
input.value = text;
|
||
td.textContent = "";
|
||
td.append(input);
|
||
input.focus();
|
||
input.select();
|
||
let done = false;
|
||
const finish = (ok: boolean): void => {
|
||
if (done) return;
|
||
done = true;
|
||
if (ok && input.value !== text) commit(input.value);
|
||
else td.textContent = text;
|
||
};
|
||
input.addEventListener("keydown", (e) => {
|
||
if (e.key === "Enter") finish(true);
|
||
else if (e.key === "Escape") finish(false);
|
||
});
|
||
input.addEventListener("blur", () => finish(true));
|
||
}
|
||
|
||
/** 쪽 나눔 띠 — 부품 `createPager` 를 부르는 옛 이름(PickTable · M02 목록이 씀). */
|
||
export const buildPager = createPager;
|
||
|
||
/** 칸 하나 — 노랑(고침) · 눌러 고침. */
|
||
export function buildCell(
|
||
text: string,
|
||
opts: { changed: boolean; onEdit?: (value: string) => void },
|
||
): HTMLTableCellElement {
|
||
const td = el("td", { text, attrs: { title: text } });
|
||
td.classList.toggle("is-changed", opts.changed);
|
||
if (opts.onEdit) {
|
||
const onEdit = opts.onEdit;
|
||
td.classList.add("is-editable");
|
||
td.addEventListener("click", () => startEdit(td, text, onEdit));
|
||
}
|
||
return td;
|
||
}
|