Merge remote-tracking branch 'origin/sub_laptop_7' into main_laptop_1

This commit is contained in:
2026-09-30 20:28:11 +09:00
17 changed files with 367 additions and 90 deletions
@@ -35,7 +35,8 @@ import {
type TreeRef,
type TreeView,
} from "./M02_MasterTemplete_Api_Fetch";
import { askFolder, askText } from "./M02_MasterTemplete_TreePanel_Dialogs";
import { askFolder, askNewItem, askText } from "./M02_MasterTemplete_TreePanel_Dialogs";
import { attachTreeDrag } from "./M02_MasterTemplete_TreePanel_Drag";
import {
clipOf,
keepClip,
@@ -82,6 +83,8 @@ export interface TreePanelOptions {
};
/** 복사해 둔 것의 나무 종류 — 같은 종류 나무끼리만 붙임(기본 「구조물」) */
clipKind?: string;
/** 항목 추가 창의 일위대가 고르기(PLAN 31-2 · 세 층 조합 · 찾기 AND) — 없으면 그 줄 없음 */
pickCombo?: () => Promise<{ key: string; label: string } | null>;
/** 「〇〇로 복사」 — 고른 것을 그 층 맨 위에 바로(층을 안 바꿈 · `label` 의 {n} = 개수 · `api.paste` 로) */
copyTo?: { layer: Layer; owner: string | null; label: string }[];
/** 이 층을 고칠 수 있나 — 아니면 메뉴 · 키 없음 */
@@ -286,49 +289,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),
});
/** 놓은 자리로 옮김 — 이미 그 자리인 것은 뺌 · 하나면 한 개 옮기기 · 여럿이면 한 번에 */
@@ -495,16 +467,20 @@ export function createTreePanel(opt: TreePanelOptions): TreePanelHandle {
async function addItem(parent: string[]): Promise<void> {
if (!(await ready())) return;
if (opt.item?.onAdd) return opt.item.onAdd(parent);
const got = await askText(L("M02_TreeAddItem"), [
{ key: "name", label: L("M02_TreeItemName") },
{ key: "unit", label: L("M02_TreeUnit") },
]);
// 종류(맨 위 폴더 · 고른 폴더면 그 안) + 이름 + 일위대가 · 단위 안 받음(PLAN 31-2)
const kinds = view ? buildFolders(view).folders.map((f) => f.path[0]) : [];
const got = await askNewItem({
title: L("M02_TreeAddItem"),
kinds,
at: parent,
pickCombo: opt.pickCombo,
});
if (got) {
await edit({
op: "add-item",
경로: parent,
경로: got.path,
이름: got.name,
...(got.unit ? { 단위: got.unit } : {}),
...(got.combo ? { 일위대가: got.combo } : {}),
});
}
}