/* ============================================================================= * M02_MasterTemplete_TreePanel_Drag.ts * 공용 나무 끌어다 옮기기(PLAN 30-2 · 31-5) — 같은 층 안 · 폴더 줄 위 = 그 안 · 빈 자리 = 맨 위 · * 제 안으로는 안 됨 · 고른 것 안의 줄을 끌면 고른 것 전부. 옮기는 일은 부르는 쪽(`move`). * 놓을 자리 강조는 한 곳만 · 놓음 · 끝 · 칸 밖으로 나감에서 모두 지움(나무 칸 자신 포함). * ========================================================================== */ import type { Target } from "./M02_MasterTemplete_TreePanel_Pick"; import { isUnder } from "./M02_MasterTemplete_TreePanel_Model"; export interface TreeDragOptions { /** 나무 칸(줄들의 부모 · 빈 자리 = 맨 위) */ body: HTMLElement; targetOf: (id: string) => Target | undefined; /** 이 줄을 끌 때 옮길 것 — 고른 것 안이면 고른 것 전부 */ listFor: (id: string) => Target[] | null; move: (list: Target[], dest: string[]) => void; } /** 칸에 놓기를 달고 줄마다 끌기를 다는 손잡이(`arm`)를 돌려줌 */ export function attachTreeDrag(opt: TreeDragOptions): { arm: (row: HTMLElement, id: string) => void; } { const { body } = opt; let dragging: Target[] | null = null; const dropDest = (event: DragEvent): string[] | null => { const row = (event.target as HTMLElement).closest("[data-node]"); const target = row ? opt.targetOf(row.dataset.node ?? "") : undefined; const dest = !row ? [] : target?.kind === "folder" ? target.path : null; const blocked = dragging?.some((t) => t.kind === "folder" && dest && isUnder(dest, t.path)); return dest && !blocked ? dest : null; }; /** 놓을 자리 강조 — 한 곳만 · null = 모두 지움(나무 칸 자신 = 맨 위 자리 포함 · PLAN 31-5 흠) */ const dropMark = (row: HTMLElement | null): void => { body.classList.remove("is-drop"); body.querySelectorAll(".is-drop").forEach((b) => b.classList.remove("is-drop")); row?.classList.add("is-drop"); }; // 나무 칸 밖으로 나가면 강조를 지움 — 칸 안 자식 위로 옮겨 갈 때는 그대로(깜빡임 막음) body.addEventListener("dragleave", (event) => { const to = event.relatedTarget as Node | null; if (!to || !body.contains(to)) dropMark(null); }); body.addEventListener("dragover", (event) => { if (!dragging || !dropDest(event)) return; event.preventDefault(); dropMark((event.target as HTMLElement).closest("[data-node]") ?? body); }); body.addEventListener("drop", (event) => { const dest = dragging && dropDest(event); const list = dragging; dragging = null; dropMark(null); if (!dest || !list) return; event.preventDefault(); opt.move(list, dest); }); return { arm: (row, id) => { row.draggable = true; row.addEventListener("dragstart", (event) => { dragging = opt.listFor(id); event.dataTransfer?.setData("text/plain", id); if (event.dataTransfer) event.dataTransfer.effectAllowed = "move"; }); row.addEventListener("dragend", () => { dragging = null; dropMark(null); }); }, }; }