/* ============================================================================= * B09_Estimation_UI_Tabs_Bill_Drag.ts * B09 설계내역서 줄 끌기(PLAN 204-1) — 줄을 잡아 위아래 = 자리 · 좌우 = 레벨(한 칸 = 1em) · 공용 표 위 덧그림. * * 놓을 자리 = 줄 사이 틈(포인터가 줄 가운데 위 = 그 줄 앞) · 끄는 줄 자손 = 함께 감 · 자기 자손 안 = 못 놓음 * 레벨 = 위 줄 + 1(위 줄 아래로 · 항목이면 저절로 묶음) ~ 아래 줄 레벨 · 맨 위(1) = 맨 위 줄만 * 놓기 = 표 drop(`…_Bill_Edit` dropMove — 메뉴와 같은 지킬 것) · 미리 보기 선(못 놓음 = is-bad · 커서 not-allowed) * 잡기 = 글 칸 · 단추 밖 아무 칸 · 6px 넘게 움직여야 끌기(그냥 누름 = 줄 고름) * ========================================================================== */ import type { DataRow } from "@ui/ui_template_data_table"; import type { BillTable } from "./B09_Estimation_UI_Tabs_Bill_Edit"; export interface DropAt { parent: string | null; before: string | null; level: number; } const holds = (row: DataRow): boolean => row.key !== "fee" && !String(row.key).startsWith("mat:"); /** 틈 gap(0 ~ 줄 수 · 그 줄 앞) · 바라는 레벨 → 놓을 자리 · 같은 자리 · 못 놓음 = null */ export function dropAt(rows: DataRow[], from: number, gap: number, want: number): DropAt | null { const row = rows[from]; if (!row) return null; const lv = (r: DataRow | undefined): number => Number(r?.level ?? 1); let end = from + 1; while (end < rows.length && lv(rows[end]) > lv(row)) end += 1; if (gap > from && gap < end) return null; // 자기 자손 안 const rest = [...rows.slice(0, from), ...rows.slice(end)]; const g = gap <= from ? gap : gap - (end - from); const up = rest[g - 1]; const down = rest[g]; const max = up ? lv(up) + (holds(up) ? 1 : 0) : 1; const min = Math.max(down ? lv(down) : 1, lv(row) === 1 ? 1 : 2); if (min > max) return null; const level = Math.min(max, Math.max(min, want)); if (level === lv(row) && (gap === from || gap === end)) return null; // 제자리 let parent: DataRow | undefined = up; const byKey = new Map(rest.map((r) => [String(r.key), r])); while (parent && lv(parent) >= level) parent = byKey.get(String(parent.parent ?? "")); return { parent: parent ? String(parent.key) : null, before: down && lv(down) === level ? String(down.key) : null, level, }; } /** 표 root 에 줄 끌기를 붙임 — 놓으면 done(끈 줄 키) 뒤 drop 실행 */ export function attachDrag(root: HTMLElement, table: BillTable, done: (key: string) => void): void { let endAt = 0; // 끌고 난 누름 = 줄 고름 아님 root.addEventListener( "click", (event) => { if (Date.now() - endAt < 300) event.stopPropagation(); }, true, ); root.addEventListener("pointerdown", (down) => { const target = down.target as Element; if (down.button !== 0 || !table.drop || target.closest("input,textarea,select,button,a")) return; const tr = target.closest("tr[data-i]"); if (!tr || !root.contains(tr)) return; const from = Number(tr.dataset.i); const step = parseFloat(getComputedStyle(tr).fontSize) || 14; const line = document.createElement("div"); line.className = "b09-estimation__drop"; let on = false; let run: (() => void) | null = null; let seen = ""; const move = (event: PointerEvent): void => { const dx = event.clientX - down.clientX; if (!on && Math.abs(dx) + Math.abs(event.clientY - down.clientY) < 6) return; if (!on) { on = true; document.body.append(line); document.body.style.userSelect = "none"; window.getSelection()?.removeAllRanges(); } const trs = table.rows.map((_r, i) => root.querySelector(`tr[data-i="${i}"]`)); let gap = trs.length; let y = trs.at(-1)?.getBoundingClientRect().bottom ?? 0; for (const [i, el] of trs.entries()) { const box = el?.getBoundingClientRect(); if (box && event.clientY < box.top + box.height / 2) { [gap, y] = [i, box.top]; break; } } const want = Number(table.rows[from]?.level ?? 1) + Math.round(dx / step); const at = dropAt(table.rows, from, gap, want); const key = `${gap}|${at?.level}`; if (key !== seen) { seen = key; run = at ? table.drop!(from, at.parent, at.before) : null; } const box = root.getBoundingClientRect(); const left = box.left + ((at?.level ?? want) - 1) * step + 8; Object.assign(line.style, { top: `${y - 1}px`, left: `${left}px`, width: `${Math.max(box.right - left, 40)}px`, }); line.classList.toggle("is-bad", !run); document.body.style.cursor = run ? "grabbing" : "not-allowed"; }; const stop = (event: PointerEvent): void => { window.removeEventListener("pointermove", move); window.removeEventListener("pointerup", stop); window.removeEventListener("pointercancel", stop); line.remove(); document.body.style.userSelect = ""; document.body.style.cursor = ""; if (!on) return; endAt = Date.now(); if (event.type === "pointerup" && run) { done(String(table.rows[from].key)); run(); } }; window.addEventListener("pointermove", move); window.addEventListener("pointerup", stop); window.addEventListener("pointercancel", stop); }); }