/* ============================================================================= * line_stack_drag.ts * 줄 쌓기 왼쪽 끌어 옮기기(PLAN 804-5 · 요구 682-4) — 손잡이(⋮⋮)를 누르고 끌면 놓을 자리에 선 표시 · 놓으면 `drop` * 줄 = 항목 안 · 항목 사이 어디든(빈 항목 = 그 안) → `k.p`(항목 k 의 p 자리) · 항목 = 항목 사이 → `p` * 포인터 끌기(마우스 · 터치 같음) · Esc = 그만 · 자리 표시 = 클래스(`data-drop`)만 — 끄는 동안 배치 안 바뀜 * ========================================================================== */ export interface DragOpts { /** 끄는 것 — 줄 카드 · 항목 상자 */ what: "line" | "item"; /** 끌기 영역(`.lstk-edit`) */ root: HTMLElement; /** 끄는 요소(반투명) */ self: HTMLElement; /** 놓음 — line `k.p` · item `p` */ drop: (to: string) => void; } const mid = (r: DOMRect): number => r.top + r.height / 2; /** 포인터 y 의 놓을 자리 + 표시할 요소 · 꼴 */ function spotOf(o: DragOpts, y: number): { to: string; at: HTMLElement; side: string } | null { const boxes = [...o.root.querySelectorAll(":scope > .lstk-item")]; if (!boxes.length) return null; if (o.what === "item") { const k = boxes.findIndex((b) => y < mid(b.getBoundingClientRect())); return k < 0 ? { to: String(boxes.length), at: boxes[boxes.length - 1], side: "after" } : { to: String(k), at: boxes[k], side: "before" }; } // 줄 — 포인터가 든 항목(위 · 아래 밖 = 처음 · 끝 항목) let k = boxes.findIndex((b) => y < b.getBoundingClientRect().bottom); if (k < 0) k = boxes.length - 1; const cards = [...boxes[k].querySelectorAll(":scope .lstk-lines > .lstk-line")]; if (!cards.length) return { to: `${k}.0`, at: boxes[k], side: "in" }; const p = cards.findIndex((c) => y < mid(c.getBoundingClientRect())); return p < 0 ? { to: `${k}.${cards.length}`, at: cards[cards.length - 1], side: "after" } : { to: `${k}.${p}`, at: cards[p], side: "before" }; } /** 손잡이에 끌기를 붙임 */ export function attachDrag(grip: HTMLElement, o: DragOpts): void { grip.addEventListener("pointerdown", (e) => { if (e.button !== 0) return; e.preventDefault(); e.stopPropagation(); grip.setPointerCapture(e.pointerId); let spot: ReturnType = null; const clear = (): void => { for (const x of o.root.querySelectorAll("[data-drop]")) x.removeAttribute("data-drop"); }; o.self.classList.add("is-dragging"); o.root.dataset.dragging = o.what; const move = (ev: PointerEvent): void => { clear(); spot = spotOf(o, ev.clientY); if (spot) spot.at.dataset.drop = spot.side; }; const end = (ok: boolean): void => { grip.removeEventListener("pointermove", move); grip.removeEventListener("pointerup", up); grip.removeEventListener("pointercancel", cancel); window.removeEventListener("keydown", esc); clear(); o.self.classList.remove("is-dragging"); delete o.root.dataset.dragging; if (ok && spot) o.drop(spot.to); }; const up = (): void => end(true); const cancel = (): void => end(false); const esc = (ev: KeyboardEvent): void => { if (ev.key === "Escape") end(false); }; grip.addEventListener("pointermove", move); grip.addEventListener("pointerup", up); grip.addEventListener("pointercancel", cancel); window.addEventListener("keydown", esc); }); }