/* ============================================================================= * B09_Estimation_UI_Tabs_Bill_Level.ts * B09 설계내역서 레벨 단추(PLAN 200-2 · 229-1) — 보조 탭 줄 오른쪽 [→ 들여쓰기] [← 내어쓰기] [↑ 위로] [↓ 아래로] · * 키 · 안내 한 줄(표 아래). * * 단추 = 보조 탭 줄 도구 자리(`…_UI_TabBar` setTools) · 안내 글 = 표 아래(202-1) * 동작 = 줄 우클릭 메뉴(`…_UI_Tabs_Bill_Edit` billMenu)의 같은 항목을 부름 — 메뉴에 없으면 단추 흐림 · 키 안 먹음 * 고른 줄 = 표 줄 누름(키로 기억 · 다시 그려도 남음) · 표시 = 줄 class is-focus(Shell 이 줄 class 곁값으로) · * 꼴 = 줄 전체 상자 테두리(235-1 · 페이지 CSS) · 글 칸도 한 번 누름 = 고름 · 더블클릭 · Enter · F2 = 입력(공용 표) * 키 = Tab 들여쓰기 · Shift+Tab 내어쓰기 · Alt+↑↓ 옮기기 · Esc 편집 끝(편집 아니면 고름 풂) * 방향키(204) = → 들여쓰기 · ← 내어쓰기 · ↑↓ 옮기기(같은 메뉴 항목) · 끌기 = `…_Bill_Drag`(놓은 줄 = 고른 줄) * 글 칸 편집 중 · 초점이 표 · 도구 줄 밖이면 안 먹음 — 줄 누름이 다른 탭으로 가는 줄은 글 칸을 눌러 고름 * 242-1 이동해 온 범위(총괄 등 → 그 줄 + 자손 · 빨강 is-pick) = 고름과 한때 하나 — 오면 고름 풂 · * 줄 누름 · 방향키 · 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 { attachDrag } from "./B09_Estimation_UI_Tabs_Bill_Drag"; import type { BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit"; /** [단추 글, 메뉴 글] — 차례 = 들여 · 내어 · 위 · 아래 */ const ACTS: [string, string][] = [ ["→ 들여쓰기", "들여쓰기 →"], ["← 내어쓰기", "내어쓰기 ←"], ["↑ 위로", "위로 ↑"], ["↓ 아래로", "아래로 ↓"], ]; const GUIDE = "줄 누르고 단추 · 방향키 ←→ 레벨 · ↑↓ 자리 · 줄 끌기 · 더블클릭 · Enter · F2 입력"; let picked: string | null = null; /** 242-1 이동해 온 범위 [첫 줄, 끝 줄] — 고름과 한때 하나 */ let jump: [number, number] | null = null; /** 지금 그린 도구 줄 — 키가 부름 */ let live: { root: HTMLElement; tools: HTMLElement[]; run: (label: string) => void; sync: () => void; edit: (index: number) => void; index: () => number; } | 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 subtreeEnd(rows: DataRow[], at: number): number { const lv = Number(rows[at]?.level ?? 1); let end = at; while (end + 1 < rows.length && Number(rows[end + 1]?.level ?? 1) > lv) end++; return end; } /** 242-1 이동해 온 줄(-1 = 없음) — 범위 = 그 줄 + 자손 · 오면 고름 풂(Shell 이 표 그리기 전에) */ export function jumpTo(rows: DataRow[], at: number): void { jump = at >= 0 ? [at, subtreeEnd(rows, at)] : null; if (jump) picked = null; } /** 지금 빨강 범위 — Shell 줄 class 곁값(pickClass) */ export const jumpRange = (): [number, number] | null => jump; /** 그 줄 메뉴 항목(글 → 동작) — 메뉴에 없는 동작 = 못 함 */ 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 && !jump)) return; const at = document.activeElement; if ( at && at !== document.body && !live.root.contains(at) && !live.tools.includes(at as HTMLElement) ) return; // 242-1 빨강 중 = 방향키 · Esc 가 빨강만 풂(고른 줄 없음) if (picked === null) { if (event.key !== "Escape" && !keyAct(event, editing(at))) return; jump = null; return live.sync(); } // Esc = 편집 끝(표에 초점 · 키 받음) · 편집 아닐 때 = 고름 풂 if (event.key === "Escape") { if (!editing(at)) { picked = null; return live.sync(); } (at as HTMLElement).blur(); return live.root.focus(); } // Enter · F2 = 고른 줄 입력 시작(235-1) — 도구 단추 위 Enter 는 단추 몫 if ((event.key === "Enter" || event.key === "F2") && !editing(at)) { if (live.tools.includes(at as HTMLElement) || event.isComposing) return; event.preventDefault(); return live.edit(live.index()); } const label = keyAct(event, editing(at)); if (!label) return; event.preventDefault(); live.run(label); }); } /** 레벨 단추 넷(보조 탭 줄로) · 안내 글(표 아래로) — 표 root 에 줄 누름(고름)을 붙임 */ export function levelBar( root: HTMLElement, table: BillTable, edit: (index: number) => void = () => {}, ): { tools: HTMLElement[]; guide: 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("p"); guide.className = "ui-general-block__empty b09-estimation__line-hint"; guide.textContent = GUIDE; attachHoverInfo(guide, (el) => (el.scrollWidth > el.clientWidth ? el.textContent : null)); // 236-2 잘릴 때만 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"); if (!jump) for (const tr of root.querySelectorAll("tr.is-pick")) tr.classList.remove("is-pick", "is-pick-top", "is-pick-end"); 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); jump = null; sync(); }); attachDrag(root, table, (key) => { picked = key; jump = null; }); sync(); live = { root, tools: buttons, run, sync, edit, index }; listenKeys(); return { tools: buttons, guide }; }