/* ============================================================================= * 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 편집 끝(편집 아니면 고름 풂) * 방향키(204) = → 들여쓰기 · ← 내어쓰기 · ↑↓ 옮기기(같은 메뉴 항목) · 끌기 = `…_Bill_Drag`(놓은 줄 = 고른 줄) * 글 칸 편집 중 · 초점이 표 · 도구 줄 밖이면 안 먹음 — 줄 누름이 다른 탭으로 가는 줄은 글 칸을 눌러 고름 * ========================================================================== */ 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 { attachDrag } from "./B09_Estimation_UI_Tabs_Bill_Drag"; 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 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.shiftKey) return null; const arrow = ["ArrowRight", "ArrowLeft", "ArrowUp", "ArrowDown"].indexOf(e.key); // 방향키(204) — 그냥 · Alt 같음(Alt+←→ = 브라우저 뒤로 · 앞으로라 안 받음) if (arrow < 0 || (e.altKey && arrow < 2)) return null; return ACTS[arrow][1]; } 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("tr[data-i]"); const row = tr ? table.rows[Number(tr.dataset.i)] : undefined; if (!row) return; picked = String(row.key); sync(); }); attachDrag(root, table, (key) => (picked = key)); sync(); live = { root, bar, run, sync }; listenKeys(); return bar; }