Files
Aislo/B09_Estimation/B09_Estimation_UI_Tabs_Bill_Level.ts
T

130 lines
5.6 KiB
TypeScript

/* =============================================================================
* B09_Estimation_UI_Tabs_Bill_Level.ts
* B09 설계내역서 레벨 도구 줄(PLAN 200-2) — 표 위 [→ 들여쓰기] [← 내어쓰기] [↑ 위로] [↓ 아래로] · 키 · 안내 한 줄.
*
* 동작 = 줄 우클릭 메뉴(`…_UI_Tabs_Bill_Edit` billMenu)의 같은 항목을 부름 — 메뉴에 없으면 단추 흐림 · 키 안 먹음
* 고른 줄 = 표 줄 누름(키로 기억 · 다시 그려도 남음) · 표시 = 줄 class is-focus(Shell 이 줄 class 곁값으로)
* 키 = Tab 들여쓰기 · Shift+Tab 내어쓰기 · Alt+↑↓ 옮기기 · Esc 편집 끝(편집 아니면 고름 풂)
* 글 칸 편집 중 · 초점이 표 · 도구 줄 밖이면 안 먹음 — 줄 누름이 다른 탭으로 가는 줄은 글 칸을 눌러 고름
* ========================================================================== */
import type { DataRow } from "@ui/ui_template_data_table";
import { createButton } from "@ui/ui_template_elements";
import { attachHoverInfo } from "@ui/ui_template_hover_info";
import type { BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit";
/** [단추 글, 메뉴 글] — 차례 = 들여 · 내어 · 위 · 아래 */
const ACTS: [string, string][] = [
["→ 들여쓰기", "들여쓰기 →"],
["← 내어쓰기", "내어쓰기 ←"],
["↑ 위로", "위로 ↑"],
["↓ 아래로", "아래로 ↓"],
];
const GUIDE = "레벨 — 줄을 누르고 단추 · 키 Tab 들여 · Shift+Tab 내어 · Alt+↑↓ 옮김";
const GUIDE_MORE =
"레벨 = 공종 · 명칭 칸 왼쪽 들여쓰기(레벨마다 한 칸) · 묶음 금액 = 아래 줄 합 · " +
"들여쓰기 = 바로 위 묶음 줄 아래로 · 내어쓰기 = 부모 묶음 밖으로(맨 위 공종은 [새 공종 추가]) · " +
"위로 · 아래로 = 같은 부모 안에서 아래 줄째 자리 바꿈 · 못 하는 동작 = 흐린 단추 · " +
"키는 글 칸 편집 중이 아닐 때(Esc = 편집 끝 · 한 번 더 Esc = 고름 풂) · " +
"묶음 줄로 바꾸기 · 더하기 · 지우기 = 줄 우클릭";
let picked: string | null = null;
/** 지금 그린 도구 줄 — 키가 부름 */
let live: {
root: HTMLElement;
bar: HTMLElement;
run: (label: string) => void;
sync: () => void;
} | null = null;
let keysOn = false;
/** 고른 줄 class — Shell 줄 class 곁값 */
export const pickedClass = (row: DataRow | undefined): string | null =>
row && picked !== null && String(row.key) === picked ? "is-focus" : null;
/** 그 줄 메뉴 항목(글 → 동작) — 메뉴에 없는 동작 = 못 함 */
export function menuActs(table: BillTable, index: number): Map<string, () => void> {
const items = index >= 0 && table.menu ? table.menu(index) : [];
return new Map(items.filter((x): x is [string, () => void] => Array.isArray(x)));
}
/** 키 → 메뉴 글 · 편집 중 · 다른 키 = null */
export function keyAct(
e: { key: string; shiftKey: boolean; altKey: boolean; ctrlKey?: boolean },
editing: boolean,
): string | null {
if (editing || e.ctrlKey) return null;
if (e.key === "Tab" && !e.altKey) return ACTS[e.shiftKey ? 1 : 0][1];
if (!e.altKey || e.shiftKey) return null;
return e.key === "ArrowUp" ? ACTS[2][1] : e.key === "ArrowDown" ? ACTS[3][1] : null;
}
const editing = (at: Element | null): boolean =>
/^(INPUT|TEXTAREA|SELECT)$/.test(at?.tagName ?? "") ||
!!(at as HTMLElement | null)?.isContentEditable;
function listenKeys(): void {
if (keysOn) return;
keysOn = true;
document.addEventListener("keydown", (event) => {
if (!live?.root.isConnected || picked === null) return;
const at = document.activeElement;
if (at && at !== document.body && !live.root.contains(at) && !live.bar.contains(at)) return;
// Esc = 편집 끝(표에 초점 · 키 받음) · 편집 아닐 때 = 고름 풂
if (event.key === "Escape") {
if (!editing(at)) {
picked = null;
return live.sync();
}
(at as HTMLElement).blur();
return live.root.focus();
}
const label = keyAct(event, editing(at));
if (!label) return;
event.preventDefault();
live.run(label);
});
}
/** 표 위 레벨 도구 줄 — 표 root 에 줄 누름(고름)을 붙임 */
export function levelBar(root: HTMLElement, table: BillTable): HTMLElement {
const index = (): number =>
picked === null ? -1 : table.rows.findIndex((r) => String(r.key) === picked);
const run = (label: string): void => menuActs(table, index()).get(label)?.();
const buttons = ACTS.map(([text, label]) => {
const button = createButton({
label: text,
variant: "ghost",
size: "sm",
onClick: () => run(label),
});
button.dataset.b09Level = label;
return button;
});
const guide = document.createElement("span");
guide.className = "b09-estimation__hint";
guide.textContent = GUIDE;
attachHoverInfo(guide, GUIDE_MORE);
const bar = document.createElement("div");
bar.className = "b09-estimation__level";
bar.append(...buttons, guide);
const sync = (): void => {
const acts = menuActs(table, index());
ACTS.forEach(([, label], k) => (buttons[k].disabled = !acts.has(label)));
for (const tr of root.querySelectorAll("tr.is-focus")) tr.classList.remove("is-focus");
root.querySelector(`tr[data-i="${index()}"]`)?.classList.add("is-focus");
};
root.addEventListener("click", (event) => {
const tr = (event.target as Element).closest<HTMLElement>("tr[data-i]");
const row = tr ? table.rows[Number(tr.dataset.i)] : undefined;
if (!row) return;
picked = String(row.key);
sync();
});
sync();
live = { root, bar, run, sync };
listenKeys();
return bar;
}