fix(M02): 좌측 두 컨테이너 펼친 채 진입 · 끌기 뒤 놓을 자리 강조 지움 (PLAN 31-5)
- 「일위대가 조합」 · 「구조물 조합」 둘 다 펼친 채 들어옴 - 끌어 놓은 뒤 나무 칸 자신(맨 위 자리) 강조가 남던 흠 — 놓음 · 끝 · 칸 밖 나감에서 모두 지움(칸 안 자식 위로는 그대로) - 끌기 코드를 TreePanel_Drag 로 나눔(700줄) - 시험 한 건 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -36,6 +36,7 @@ import {
|
||||
type TreeView,
|
||||
} from "./M02_MasterTemplete_Api_Fetch";
|
||||
import { askFolder, askNewItem, askText } from "./M02_MasterTemplete_TreePanel_Dialogs";
|
||||
import { attachTreeDrag } from "./M02_MasterTemplete_TreePanel_Drag";
|
||||
import {
|
||||
clipOf,
|
||||
keepClip,
|
||||
@@ -286,49 +287,18 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
|
||||
},
|
||||
{ capture: true },
|
||||
);
|
||||
// 끌어다 옮기기(30-2) — 쓸 수 있는 층만 · 고른 것 안의 줄을 끌면 고른 것 전부
|
||||
if (opt.canEdit) {
|
||||
button.draggable = true;
|
||||
button.addEventListener("dragstart", (event) => {
|
||||
const own = targets.get(one.id);
|
||||
dragging = picked.size > 1 && picked.has(one.id) ? pickedTargets() : own ? [own] : null;
|
||||
event.dataTransfer?.setData("text/plain", one.id);
|
||||
if (event.dataTransfer) event.dataTransfer.effectAllowed = "move";
|
||||
});
|
||||
button.addEventListener("dragend", () => {
|
||||
dragging = null;
|
||||
dropMark(null);
|
||||
});
|
||||
}
|
||||
if (opt.canEdit) drag.arm(button, one.id); // 끌어다 옮기기(30-2) — 쓸 수 있는 층만
|
||||
return button;
|
||||
};
|
||||
|
||||
/* --- 끌어다 놓기: 폴더 줄 위 = 그 안 · 빈 자리 = 맨 위 · 제 안으로는 안 됨 --- */
|
||||
let dragging: Target[] | null = null;
|
||||
const dropDest = (event: DragEvent): string[] | null => {
|
||||
const row = (event.target as HTMLElement).closest<HTMLElement>("[data-node]");
|
||||
const target = row ? targets.get(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;
|
||||
};
|
||||
const dropMark = (row: HTMLElement | null): void => {
|
||||
body.querySelectorAll(".is-drop").forEach((b) => b.classList.remove("is-drop"));
|
||||
row?.classList.add("is-drop");
|
||||
};
|
||||
body.addEventListener("dragover", (event) => {
|
||||
if (!dragging || !dropDest(event)) return;
|
||||
event.preventDefault();
|
||||
dropMark((event.target as HTMLElement).closest<HTMLElement>("[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();
|
||||
void dropMove(list, dest);
|
||||
const drag = attachTreeDrag({
|
||||
body,
|
||||
targetOf: (id) => targets.get(id),
|
||||
listFor: (id) => {
|
||||
const own = targets.get(id);
|
||||
return picked.size > 1 && picked.has(id) ? pickedTargets() : own ? [own] : null;
|
||||
},
|
||||
move: (list, dest) => void dropMove(list, dest),
|
||||
});
|
||||
|
||||
/** 놓은 자리로 옮김 — 이미 그 자리인 것은 뺌 · 하나면 한 개 옮기기 · 여럿이면 한 번에 */
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/* =============================================================================
|
||||
* 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<HTMLElement>("[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<HTMLElement>("[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);
|
||||
});
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -188,6 +188,8 @@ export function buildSide(opt: SideOptions): SideHandle {
|
||||
],
|
||||
});
|
||||
attachCollapsible(listHost);
|
||||
// 들어오면 「일위대가 조합」 · 「구조물 조합」 둘 다 펼친 채(PLAN 31-5 · 구조물 조합은 위에서 펼침)
|
||||
labor?.body.closest("section")?.classList.remove("is-collapsed");
|
||||
|
||||
const root = el("div", { className: "m02-side ui-sidebar-panel", children: [listHost] });
|
||||
|
||||
|
||||
@@ -231,8 +231,9 @@ def test_켜진_층_단추는_목록으로_복사_끌기():
|
||||
labor = _text(M02 / "M02_MasterTemplete_Labor.ts")
|
||||
assert 'if (editor.isDirty() && !(await showConfirmDialog(tc("BackAsk")))) return;' in labor
|
||||
panel = _text(M02 / "M02_MasterTemplete_TreePanel.ts")
|
||||
assert "if (opt.canEdit) {\n button.draggable = true;" in panel
|
||||
assert 'dragging?.some((t) => t.kind === "folder" && dest && isUnder(dest, t.path))' in panel
|
||||
assert "if (opt.canEdit) drag.arm(button, one.id);" in panel
|
||||
drag = _text(M02 / "M02_MasterTemplete_TreePanel_Drag.ts")
|
||||
assert 'dragging?.some((t) => t.kind === "folder" && dest && isUnder(dest, t.path))' in drag
|
||||
|
||||
|
||||
def test_구조물_조합_제목_줄_새_항목():
|
||||
@@ -255,3 +256,15 @@ def test_구조물_조합_제목_줄_새_항목():
|
||||
assert ".m02-main__new {\n margin-left: auto;" in _text(
|
||||
M02 / "M02_MasterTemplete_UI_Style.css"
|
||||
)
|
||||
|
||||
|
||||
def test_들어오면_두_컨테이너_펼침_끌기_뒤_강조_지움():
|
||||
"""31-5 — 좌측 「일위대가 조합」 · 「구조물 조합」 둘 다 펼친 채 · 끌어 놓은 뒤 강조(나무 칸
|
||||
자신 = 맨 위 자리 포함)를 모두 지움 · 칸 밖으로 나가면 지움(칸 안 자식 위로는 그대로)."""
|
||||
side = _text(M02 / "M02_MasterTemplete_UI_Side.ts")
|
||||
assert 'labor?.body.closest("section")?.classList.remove("is-collapsed");' in side
|
||||
assert 'summary.classList.remove("is-collapsed");' in side
|
||||
drag = _text(M02 / "M02_MasterTemplete_TreePanel_Drag.ts")
|
||||
mark = drag.split("const dropMark = ")[1].split("};")[0]
|
||||
assert 'body.classList.remove("is-drop");' in mark
|
||||
assert "if (!to || !body.contains(to)) dropMark(null);" in drag
|
||||
|
||||
Reference in New Issue
Block a user