4-26 树rust-2
This commit is contained in:
@@ -27,6 +27,8 @@ pub struct TreeShellQuery {
|
||||
pub root_node_id: Option<String>,
|
||||
pub depth: Option<u32>,
|
||||
pub active_document_id: Option<String>,
|
||||
pub focused_document_id: Option<String>,
|
||||
pub active_picker_item_key: Option<String>,
|
||||
pub actor_id: Option<String>,
|
||||
pub channel: Option<String>,
|
||||
pub host: Option<String>,
|
||||
@@ -162,6 +164,8 @@ fn build_tree_shell_html(
|
||||
workspace_id: &str,
|
||||
root_node_id: Option<&str>,
|
||||
active_document_id: Option<&str>,
|
||||
focused_document_id: Option<&str>,
|
||||
active_picker_item_key: Option<&str>,
|
||||
channel: &str,
|
||||
host: Option<&str>,
|
||||
context: &RequestContext,
|
||||
@@ -175,6 +179,8 @@ fn build_tree_shell_html(
|
||||
"workspaceId": workspace_id,
|
||||
"rootNodeId": root_node_id,
|
||||
"activeDocumentId": active_document_id,
|
||||
"focusedDocumentId": focused_document_id,
|
||||
"activePickerItemKey": active_picker_item_key,
|
||||
"actorId": context.auth.actor_id,
|
||||
"channel": channel,
|
||||
"host": host,
|
||||
@@ -663,6 +669,21 @@ fn build_tree_shell_html(
|
||||
.tree-kind-badge[data-kind="table"] {
|
||||
color: #b45309;
|
||||
}
|
||||
.tree-kind-badge[data-kind="pdf"] {
|
||||
color: #dc2626;
|
||||
}
|
||||
.tree-kind-badge[data-kind="book"] {
|
||||
color: #0f766e;
|
||||
}
|
||||
.tree-kind-badge[data-kind="image"] {
|
||||
color: #0891b2;
|
||||
}
|
||||
.tree-kind-badge[data-kind="video"] {
|
||||
color: #ea580c;
|
||||
}
|
||||
.tree-kind-badge[data-kind="audio"] {
|
||||
color: #16a34a;
|
||||
}
|
||||
.tree-kind-badge[data-kind="file"] {
|
||||
color: #64748b;
|
||||
}
|
||||
@@ -776,6 +797,14 @@ fn build_tree_shell_html(
|
||||
typeof state.activeDocumentId === "string" && state.activeDocumentId.trim()
|
||||
? state.activeDocumentId.trim()
|
||||
: "";
|
||||
const focusedDocumentId =
|
||||
typeof state.focusedDocumentId === "string" && state.focusedDocumentId.trim()
|
||||
? state.focusedDocumentId.trim()
|
||||
: "";
|
||||
const activePickerItemKey =
|
||||
typeof state.activePickerItemKey === "string" && state.activePickerItemKey.trim()
|
||||
? state.activePickerItemKey.trim()
|
||||
: "";
|
||||
const mode = (() => {
|
||||
const rawMode =
|
||||
typeof state.mode === "string" ? state.mode.trim() : "";
|
||||
@@ -938,19 +967,43 @@ fn build_tree_shell_html(
|
||||
.filter((item) => item.childCount > 0 && item.expandedByDefault)
|
||||
.map((item) => item.nodeId),
|
||||
);
|
||||
let focusedNodeId =
|
||||
activeDocumentId && itemById.has(activeDocumentId)
|
||||
? activeDocumentId
|
||||
: roots[0]?.nodeId || "";
|
||||
let selectedFileTreeRowIds = new Set(activeDocumentId ? [`doc:${activeDocumentId}`, `index:${activeDocumentId}`] : []);
|
||||
let fileTreeAnchorRowId = activeDocumentId ? `doc:${activeDocumentId}` : null;
|
||||
let fileTreeFocusedRowId = activeDocumentId ? `doc:${activeDocumentId}` : null;
|
||||
let currentActiveDocumentId = activeDocumentId;
|
||||
let currentFocusedDocumentId = focusedDocumentId;
|
||||
let currentActivePickerItemKey = activePickerItemKey;
|
||||
const resolvePickerRootFocused = () =>
|
||||
mode === "picker" && currentActivePickerItemKey === "__root__";
|
||||
const resolveFocusedNodeIdFromHostState = () => {
|
||||
const pickerRootFocused = resolvePickerRootFocused();
|
||||
return mode === "picker"
|
||||
? currentActivePickerItemKey &&
|
||||
currentActivePickerItemKey !== "__root__" &&
|
||||
itemById.has(currentActivePickerItemKey)
|
||||
? currentActivePickerItemKey
|
||||
: currentActiveDocumentId && itemById.has(currentActiveDocumentId)
|
||||
? currentActiveDocumentId
|
||||
: pickerRootFocused
|
||||
? ""
|
||||
: roots[0]?.nodeId || ""
|
||||
: currentFocusedDocumentId && itemById.has(currentFocusedDocumentId)
|
||||
? currentFocusedDocumentId
|
||||
: currentActiveDocumentId && itemById.has(currentActiveDocumentId)
|
||||
? currentActiveDocumentId
|
||||
: roots[0]?.nodeId || "";
|
||||
};
|
||||
let focusedNodeId = resolveFocusedNodeIdFromHostState();
|
||||
let selectedFileTreeRowIds = new Set(
|
||||
currentActiveDocumentId ? [`doc:${currentActiveDocumentId}`, `index:${currentActiveDocumentId}`] : []
|
||||
);
|
||||
let fileTreeAnchorRowId = currentActiveDocumentId ? `doc:${currentActiveDocumentId}` : null;
|
||||
let fileTreeFocusedRowId = currentActiveDocumentId ? `doc:${currentActiveDocumentId}` : null;
|
||||
let visibleFileTreeRowIds = [];
|
||||
let draggingPageNodeId = "";
|
||||
let activePageDropNodeId = null;
|
||||
let draggingFileTreeRowIds = [];
|
||||
let activeFileTreeDropRowId = null;
|
||||
let activeFileTreeRootDrop = false;
|
||||
|
||||
let activeCursor = itemById.get(activeDocumentId) || null;
|
||||
let activeCursor = itemById.get(currentActiveDocumentId) || null;
|
||||
while (activeCursor && activeCursor.parentNodeId && itemById.has(activeCursor.parentNodeId)) {
|
||||
expanded.add(activeCursor.parentNodeId);
|
||||
activeCursor = itemById.get(activeCursor.parentNodeId) || null;
|
||||
@@ -1400,6 +1453,36 @@ fn build_tree_shell_html(
|
||||
<path d="M3.8 6.6h8.4M6.6 3.8v8.4M9.4 3.8v8.4" stroke="currentColor" stroke-width="1.1"/>
|
||||
</svg>
|
||||
`,
|
||||
pdf: `
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M4 2.8h5.2l2.8 2.8v7.6H4V2.8Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<path d="M6 10.8V6.5h1.5a1.2 1.2 0 1 1 0 2.4H6m3.2-2.4v4.3m0 0c1.1 0 1.8-.8 1.8-2.1 0-1.3-.7-2.2-1.8-2.2m-1.7 4.3h1.7" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
`,
|
||||
book: `
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M4.2 3.2h6.2a1.6 1.6 0 0 1 1.6 1.6v7.4H5.4a1.2 1.2 0 0 0-1.2 1.2V4.4a1.2 1.2 0 0 1 1.2-1.2Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
<path d="M5.4 12.2V4.1M7 6h3.1M7 8.2h3.1" stroke="currentColor" stroke-width="1.1" stroke-linecap="round"/>
|
||||
</svg>
|
||||
`,
|
||||
image: `
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<rect x="3" y="3" width="10" height="10" rx="1.5" stroke="currentColor" stroke-width="1.2"/>
|
||||
<circle cx="6.2" cy="6.2" r="1.1" stroke="currentColor" stroke-width="1"/>
|
||||
<path d="M4.5 11 7.1 8.6l1.8 1.7 1.7-1.5L12 11" stroke="currentColor" stroke-width="1.1" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
`,
|
||||
video: `
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<rect x="3" y="3.4" width="7.8" height="9.2" rx="1.4" stroke="currentColor" stroke-width="1.2"/>
|
||||
<path d="m9.8 7 2.8-1.7v5.4L9.8 9" stroke="currentColor" stroke-width="1.1" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
`,
|
||||
audio: `
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M6.4 4.2v7.6a1.5 1.5 0 1 1-1-1.4V5.6l5.2-1.2v5.2a1.5 1.5 0 1 1-1-1.4V3.5L6.4 4.2Z" stroke="currentColor" stroke-width="1.1" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
`,
|
||||
file: `
|
||||
<svg viewBox="0 0 16 16" fill="none" aria-hidden="true">
|
||||
<path d="M4 2.8h5.2l2.8 2.8v7.6H4V2.8Z" stroke="currentColor" stroke-width="1.2" stroke-linejoin="round"/>
|
||||
@@ -1432,10 +1515,16 @@ fn build_tree_shell_html(
|
||||
throw new Error(await readErrorMessage(response));
|
||||
}
|
||||
const data = await response.json().catch(() => null);
|
||||
if (!data || data.ok !== true || !data.result) {
|
||||
if (!data || typeof data !== "object") {
|
||||
throw new Error("tree command 返回了无效响应");
|
||||
}
|
||||
return data.result;
|
||||
if (data.ok === true && data.result) {
|
||||
return data.result;
|
||||
}
|
||||
if (data.result && typeof data.result === "object") {
|
||||
return data.result;
|
||||
}
|
||||
return data;
|
||||
} finally {
|
||||
setBusy(false);
|
||||
}
|
||||
@@ -1453,9 +1542,101 @@ fn build_tree_shell_html(
|
||||
return (childrenByParentId.get(parentId) || []).slice();
|
||||
};
|
||||
|
||||
const PAGE_DRAG_MIME = "application/x-mnote-page-tree-node";
|
||||
|
||||
const clearPageDropFeedback = () => {
|
||||
if (!activePageDropNodeId) {
|
||||
return;
|
||||
}
|
||||
const previousRow = appElement.querySelector(
|
||||
`.tree-row[data-shell-mode="page"][data-node-id="${activePageDropNodeId}"]`,
|
||||
);
|
||||
if (previousRow instanceof HTMLElement) {
|
||||
previousRow.dataset.dropFeedback = "false";
|
||||
}
|
||||
activePageDropNodeId = null;
|
||||
};
|
||||
|
||||
const setPageDropFeedback = (nodeId) => {
|
||||
const nextNodeId = normalizeText(nodeId);
|
||||
if (activePageDropNodeId && activePageDropNodeId !== nextNodeId) {
|
||||
const previousRow = appElement.querySelector(
|
||||
`.tree-row[data-shell-mode="page"][data-node-id="${activePageDropNodeId}"]`,
|
||||
);
|
||||
if (previousRow instanceof HTMLElement) {
|
||||
previousRow.dataset.dropFeedback = "false";
|
||||
}
|
||||
}
|
||||
|
||||
if (!nextNodeId) {
|
||||
activePageDropNodeId = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const nextRow = appElement.querySelector(
|
||||
`.tree-row[data-shell-mode="page"][data-node-id="${nextNodeId}"]`,
|
||||
);
|
||||
if (nextRow instanceof HTMLElement) {
|
||||
nextRow.dataset.dropFeedback = "true";
|
||||
}
|
||||
activePageDropNodeId = nextNodeId;
|
||||
};
|
||||
|
||||
const resolvePageDropTargetNodeId = (element) => {
|
||||
const row = element instanceof Element
|
||||
? element.closest('.tree-row[data-shell-mode="page"]')
|
||||
: null;
|
||||
if (!(row instanceof HTMLElement)) {
|
||||
return "";
|
||||
}
|
||||
return normalizeText(row.dataset.nodeId);
|
||||
};
|
||||
|
||||
const readPageDragNodeId = (event) => {
|
||||
const raw =
|
||||
event.dataTransfer?.getData(PAGE_DRAG_MIME) ||
|
||||
event.dataTransfer?.getData("text/plain") ||
|
||||
draggingPageNodeId ||
|
||||
"";
|
||||
return normalizeText(raw);
|
||||
};
|
||||
|
||||
const canAcceptPageDrop = (sourceNodeId, targetNodeId) => {
|
||||
if (!sourceNodeId || !targetNodeId || sourceNodeId === targetNodeId) {
|
||||
return false;
|
||||
}
|
||||
const sourceItem = itemById.get(sourceNodeId);
|
||||
const targetItem = itemById.get(targetNodeId);
|
||||
if (!sourceItem || !targetItem) {
|
||||
return false;
|
||||
}
|
||||
return sourceItem.parentNodeId === targetItem.parentNodeId;
|
||||
};
|
||||
|
||||
const postPageExpandChange = (nodeId, nextExpanded) => {
|
||||
if (mode !== "page" || !nodeId) return;
|
||||
postToHost("tree.page.expand.changed", {
|
||||
documentId: nodeId,
|
||||
expanded: nextExpanded === true,
|
||||
target: { documentId: nodeId },
|
||||
payload: { documentId: nodeId, expanded: nextExpanded === true },
|
||||
});
|
||||
};
|
||||
|
||||
const postPageFocusChange = (nodeId) => {
|
||||
if (mode !== "page" || !nodeId) return;
|
||||
postToHost("tree.page.focus.changed", {
|
||||
documentId: nodeId,
|
||||
target: { documentId: nodeId },
|
||||
payload: { documentId: nodeId },
|
||||
});
|
||||
};
|
||||
|
||||
const toggleExpand = (nodeId) => {
|
||||
if (expanded.has(nodeId)) expanded.delete(nodeId);
|
||||
else expanded.add(nodeId);
|
||||
const nextExpanded = !expanded.has(nodeId);
|
||||
if (nextExpanded) expanded.add(nodeId);
|
||||
else expanded.delete(nodeId);
|
||||
postPageExpandChange(nodeId, nextExpanded);
|
||||
renderTree();
|
||||
};
|
||||
|
||||
@@ -1473,13 +1654,150 @@ fn build_tree_shell_html(
|
||||
return visible;
|
||||
};
|
||||
|
||||
const getVisiblePickerEntries = () => {
|
||||
if (mode !== "picker") {
|
||||
return [];
|
||||
}
|
||||
|
||||
const visible = [];
|
||||
if (allowRootPick) {
|
||||
visible.push({
|
||||
pickerItemKey: "__root__",
|
||||
item: null,
|
||||
});
|
||||
}
|
||||
|
||||
const walk = (entries) => {
|
||||
entries.forEach((item) => {
|
||||
visible.push({
|
||||
pickerItemKey: item.nodeId,
|
||||
item,
|
||||
});
|
||||
if (item.childCount > 0 && expanded.has(item.nodeId)) {
|
||||
walk(getSiblings(item.nodeId));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
walk(roots);
|
||||
return visible;
|
||||
};
|
||||
|
||||
const focusNode = (nodeId) => {
|
||||
if (!nodeId || !itemById.has(nodeId)) return;
|
||||
if (focusedNodeId === nodeId) {
|
||||
focusRowElement(nodeId);
|
||||
return;
|
||||
}
|
||||
focusedNodeId = nodeId;
|
||||
postPageFocusChange(nodeId);
|
||||
renderTree();
|
||||
focusRowElement(nodeId);
|
||||
};
|
||||
|
||||
const postPickerFocusChange = (pickerItemKey) => {
|
||||
if (mode !== "picker") return;
|
||||
const normalizedItemKey = normalizeText(pickerItemKey);
|
||||
const documentId =
|
||||
normalizedItemKey && normalizedItemKey !== "__root__"
|
||||
? normalizedItemKey
|
||||
: null;
|
||||
postToHost("tree.picker.focus.changed", {
|
||||
documentId,
|
||||
itemKey: normalizedItemKey || null,
|
||||
pickerItemKey: normalizedItemKey || null,
|
||||
target: { documentId },
|
||||
payload: {
|
||||
documentId,
|
||||
itemKey: normalizedItemKey || null,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const applyPickerFocusByItemKey = (pickerItemKey) => {
|
||||
if (mode !== "picker") return;
|
||||
|
||||
const normalizedItemKey = normalizeText(pickerItemKey);
|
||||
const nextPickerItemKey =
|
||||
normalizedItemKey === "__root__"
|
||||
? "__root__"
|
||||
: itemById.has(normalizedItemKey)
|
||||
? normalizedItemKey
|
||||
: "";
|
||||
const nextDocumentId =
|
||||
nextPickerItemKey && nextPickerItemKey !== "__root__"
|
||||
? nextPickerItemKey
|
||||
: null;
|
||||
|
||||
currentActivePickerItemKey = nextPickerItemKey;
|
||||
currentActiveDocumentId = nextDocumentId;
|
||||
focusedNodeId = nextDocumentId || "";
|
||||
renderTree();
|
||||
if (nextDocumentId) {
|
||||
focusRowElement(nextDocumentId);
|
||||
}
|
||||
postPickerFocusChange(nextPickerItemKey || null);
|
||||
};
|
||||
|
||||
const handlePickerCommand = (command) => {
|
||||
if (mode !== "picker") return;
|
||||
|
||||
const normalizedCommand = normalizeText(command);
|
||||
const visible = getVisiblePickerEntries();
|
||||
if (visible.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentPickerItemKey =
|
||||
currentActivePickerItemKey ||
|
||||
(currentActiveDocumentId && itemById.has(currentActiveDocumentId)
|
||||
? currentActiveDocumentId
|
||||
: allowRootPick
|
||||
? "__root__"
|
||||
: visible[0]?.pickerItemKey || "");
|
||||
const currentIndex = visible.findIndex(
|
||||
(entry) => entry.pickerItemKey === currentPickerItemKey,
|
||||
);
|
||||
const resolvedIndex = currentIndex >= 0 ? currentIndex : 0;
|
||||
|
||||
if (normalizedCommand === "pick") {
|
||||
const target = visible[resolvedIndex];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
if (target.pickerItemKey === "__root__") {
|
||||
setLastAction("已选择根目录");
|
||||
postToHost("tree.pick.root", {
|
||||
documentId: null,
|
||||
target: { documentId: null },
|
||||
payload: { documentId: null },
|
||||
});
|
||||
return;
|
||||
}
|
||||
handleNavigate(target.pickerItemKey);
|
||||
return;
|
||||
}
|
||||
|
||||
let nextIndex = resolvedIndex;
|
||||
if (normalizedCommand === "next") {
|
||||
nextIndex = Math.min(visible.length - 1, resolvedIndex + 1);
|
||||
} else if (normalizedCommand === "previous") {
|
||||
nextIndex = Math.max(0, resolvedIndex - 1);
|
||||
} else if (normalizedCommand === "home") {
|
||||
nextIndex = 0;
|
||||
} else if (normalizedCommand === "end") {
|
||||
nextIndex = visible.length - 1;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
const target = visible[nextIndex];
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
applyPickerFocusByItemKey(target.pickerItemKey);
|
||||
};
|
||||
|
||||
const openFileTreeContextMenu = ({
|
||||
documentId,
|
||||
assetId,
|
||||
@@ -1575,6 +1893,7 @@ fn build_tree_shell_html(
|
||||
event.preventDefault();
|
||||
if (item.childCount > 0 && !expanded.has(item.nodeId)) {
|
||||
expanded.add(item.nodeId);
|
||||
postPageExpandChange(item.nodeId, true);
|
||||
renderTree();
|
||||
focusRowElement(item.nodeId);
|
||||
return;
|
||||
@@ -1589,6 +1908,7 @@ fn build_tree_shell_html(
|
||||
event.preventDefault();
|
||||
if (item.childCount > 0 && expanded.has(item.nodeId)) {
|
||||
expanded.delete(item.nodeId);
|
||||
postPageExpandChange(item.nodeId, false);
|
||||
renderTree();
|
||||
focusRowElement(item.nodeId);
|
||||
return;
|
||||
@@ -1747,6 +2067,41 @@ fn build_tree_shell_html(
|
||||
}
|
||||
};
|
||||
|
||||
const handlePageDropMove = async (sourceNodeId, targetNodeId) => {
|
||||
const sourceItem = itemById.get(sourceNodeId);
|
||||
const targetItem = itemById.get(targetNodeId);
|
||||
if (!sourceItem || !targetItem) return;
|
||||
const siblings = getSiblings(targetItem.parentNodeId);
|
||||
const targetIndex = siblings.findIndex((entry) => entry.nodeId === targetNodeId);
|
||||
if (targetIndex < 0) return;
|
||||
try {
|
||||
const result = await sendCommand({
|
||||
action: "move",
|
||||
workspaceId,
|
||||
documentId: sourceNodeId,
|
||||
parentId: targetItem.parentNodeId,
|
||||
sortOrder: targetIndex,
|
||||
});
|
||||
const documentId =
|
||||
typeof result.documentId === "string" && result.documentId.trim()
|
||||
? result.documentId.trim()
|
||||
: sourceNodeId;
|
||||
setStatus("移动页面成功");
|
||||
setLastAction(`页面已拖放到 ${targetItem.title}`);
|
||||
postToHost("tree.subtree.moved", {
|
||||
documentId,
|
||||
target: { documentId },
|
||||
payload: { documentId },
|
||||
});
|
||||
scheduleRefresh();
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "拖拽移动失败";
|
||||
setStatus(message, "error");
|
||||
setLastAction("拖拽移动失败", "error");
|
||||
window.alert(message);
|
||||
}
|
||||
};
|
||||
|
||||
const createKindBadge = (kind) => {
|
||||
const badge = document.createElement("span");
|
||||
badge.className = "tree-kind-badge";
|
||||
@@ -1756,9 +2111,19 @@ fn build_tree_shell_html(
|
||||
? ICONS.mindmap
|
||||
: kind === "table"
|
||||
? ICONS.table
|
||||
: kind === "index"
|
||||
? ICONS.index
|
||||
: kind === "page"
|
||||
: kind === "pdf"
|
||||
? ICONS.pdf
|
||||
: kind === "book"
|
||||
? ICONS.book
|
||||
: kind === "image"
|
||||
? ICONS.image
|
||||
: kind === "video"
|
||||
? ICONS.video
|
||||
: kind === "audio"
|
||||
? ICONS.audio
|
||||
: kind === "index"
|
||||
? ICONS.index
|
||||
: kind === "page"
|
||||
? ICONS.page
|
||||
: ICONS.file;
|
||||
return badge;
|
||||
@@ -1784,17 +2149,21 @@ fn build_tree_shell_html(
|
||||
const hasChildren = item.childCount > 0;
|
||||
const row = document.createElement("div");
|
||||
row.className = "tree-row";
|
||||
row.dataset.active = String(item.nodeId === activeDocumentId);
|
||||
row.dataset.active = String(item.nodeId === currentActiveDocumentId);
|
||||
row.dataset.focused = String(item.nodeId === focusedNodeId);
|
||||
row.dataset.nodeId = item.nodeId;
|
||||
row.dataset.shellMode = mode;
|
||||
row.dataset.dropFeedback = String(activePageDropNodeId === item.nodeId);
|
||||
row.tabIndex = item.nodeId === focusedNodeId ? 0 : -1;
|
||||
row.setAttribute("role", "treeitem");
|
||||
row.setAttribute("aria-level", String(item.depth + 1));
|
||||
row.setAttribute("aria-expanded", hasChildren ? String(expanded.has(item.nodeId)) : "false");
|
||||
row.draggable = mode === "page";
|
||||
row.dataset.draggable = String(mode === "page");
|
||||
row.addEventListener("focus", () => {
|
||||
if (focusedNodeId !== item.nodeId) {
|
||||
focusedNodeId = item.nodeId;
|
||||
postPageFocusChange(item.nodeId);
|
||||
renderTree();
|
||||
}
|
||||
});
|
||||
@@ -1804,6 +2173,58 @@ fn build_tree_shell_html(
|
||||
event.preventDefault();
|
||||
openContextMenu(item.nodeId, event.clientX, event.clientY);
|
||||
});
|
||||
row.addEventListener("dragstart", (event) => {
|
||||
if (mode !== "page") return;
|
||||
draggingPageNodeId = item.nodeId;
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.effectAllowed = "move";
|
||||
event.dataTransfer.setData(PAGE_DRAG_MIME, item.nodeId);
|
||||
event.dataTransfer.setData("text/plain", item.nodeId);
|
||||
}
|
||||
setLastAction(`开始拖拽页面 ${item.title}`);
|
||||
});
|
||||
row.addEventListener("dragover", (event) => {
|
||||
if (mode !== "page") return;
|
||||
const sourceNodeId = readPageDragNodeId(event);
|
||||
const targetNodeId = resolvePageDropTargetNodeId(event.target);
|
||||
if (!canAcceptPageDrop(sourceNodeId, targetNodeId)) {
|
||||
clearPageDropFeedback();
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.dropEffect = "move";
|
||||
}
|
||||
setPageDropFeedback(targetNodeId);
|
||||
});
|
||||
row.addEventListener("dragleave", (event) => {
|
||||
if (mode !== "page") return;
|
||||
const relatedTarget =
|
||||
event.relatedTarget instanceof Node ? event.relatedTarget : null;
|
||||
if (relatedTarget && row.contains(relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
if (activePageDropNodeId === item.nodeId) {
|
||||
clearPageDropFeedback();
|
||||
}
|
||||
});
|
||||
row.addEventListener("drop", (event) => {
|
||||
if (mode !== "page") return;
|
||||
const sourceNodeId = readPageDragNodeId(event);
|
||||
const targetNodeId = resolvePageDropTargetNodeId(event.target);
|
||||
clearPageDropFeedback();
|
||||
draggingPageNodeId = "";
|
||||
if (!canAcceptPageDrop(sourceNodeId, targetNodeId)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
void handlePageDropMove(sourceNodeId, targetNodeId);
|
||||
});
|
||||
row.addEventListener("dragend", () => {
|
||||
if (mode !== "page") return;
|
||||
draggingPageNodeId = "";
|
||||
clearPageDropFeedback();
|
||||
});
|
||||
|
||||
if (hasChildren) {
|
||||
const toggleButton = document.createElement("button");
|
||||
@@ -2008,7 +2429,9 @@ fn build_tree_shell_html(
|
||||
const row = document.createElement("div");
|
||||
row.className = "tree-row";
|
||||
row.style.marginLeft = `${item.depth * 22}px`;
|
||||
row.dataset.active = String(item.rowKind === "document" && documentId === activeDocumentId);
|
||||
row.dataset.active = String(
|
||||
item.rowKind === "document" && documentId === currentActiveDocumentId
|
||||
);
|
||||
row.dataset.nodeId = item.nodeId;
|
||||
row.dataset.rowId = item.rowId;
|
||||
row.dataset.rowKind = item.rowKind;
|
||||
@@ -2195,6 +2618,7 @@ fn build_tree_shell_html(
|
||||
rootButton.type = "button";
|
||||
rootButton.className = "tree-row";
|
||||
rootButton.setAttribute("data-testid", "tree-picker-root");
|
||||
rootButton.dataset.focused = String(resolvePickerRootFocused());
|
||||
rootButton.addEventListener("click", () => {
|
||||
setLastAction("已选择根目录");
|
||||
postToHost("tree.pick.root", {
|
||||
@@ -2244,6 +2668,52 @@ fn build_tree_shell_html(
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener("message", (event) => {
|
||||
const payload = event.data;
|
||||
if (!payload || typeof payload !== "object") {
|
||||
return;
|
||||
}
|
||||
if (normalizeText(payload.channel) !== channel) {
|
||||
return;
|
||||
}
|
||||
const messageType = normalizeText(payload.type);
|
||||
if (messageType === "tree.picker.command") {
|
||||
handlePickerCommand(payload.command);
|
||||
return;
|
||||
}
|
||||
if (messageType !== "tree.shell.state.patch") {
|
||||
return;
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
const nextActiveDocumentId = normalizeText(payload.activeDocumentId);
|
||||
const nextFocusedDocumentId = normalizeText(payload.focusedDocumentId);
|
||||
const nextActivePickerItemKey = normalizeText(payload.activePickerItemKey);
|
||||
|
||||
if (nextActiveDocumentId !== currentActiveDocumentId) {
|
||||
currentActiveDocumentId = nextActiveDocumentId;
|
||||
changed = true;
|
||||
}
|
||||
if (nextFocusedDocumentId !== currentFocusedDocumentId) {
|
||||
currentFocusedDocumentId = nextFocusedDocumentId;
|
||||
changed = true;
|
||||
}
|
||||
if (nextActivePickerItemKey !== currentActivePickerItemKey) {
|
||||
currentActivePickerItemKey = nextActivePickerItemKey;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
return;
|
||||
}
|
||||
|
||||
focusedNodeId = resolveFocusedNodeIdFromHostState();
|
||||
renderTree();
|
||||
if (mode === "page" && focusedNodeId) {
|
||||
focusRowElement(focusedNodeId);
|
||||
}
|
||||
});
|
||||
|
||||
createRootButton.addEventListener("click", () => {
|
||||
if (mode === "picker") return;
|
||||
void handleCreate(null);
|
||||
@@ -2257,6 +2727,9 @@ fn build_tree_shell_html(
|
||||
};
|
||||
|
||||
renderTree();
|
||||
if (mode === "page" && focusedNodeId) {
|
||||
postPageFocusChange(focusedNodeId);
|
||||
}
|
||||
if (mode === "filetree") {
|
||||
emitFileTreeSelectionChange();
|
||||
}
|
||||
@@ -2328,6 +2801,8 @@ pub async fn tree_shell(
|
||||
&effective_workspace_id,
|
||||
query.root_node_id.as_deref(),
|
||||
query.active_document_id.as_deref(),
|
||||
query.focused_document_id.as_deref(),
|
||||
query.active_picker_item_key.as_deref(),
|
||||
&normalize_channel(query.channel),
|
||||
query.host.as_deref(),
|
||||
&effective_context,
|
||||
@@ -2389,6 +2864,7 @@ fn create_command_wire(
|
||||
"accessScope": access_scope,
|
||||
"content": content.unwrap_or_else(|| Value::Array(Vec::new())),
|
||||
}),
|
||||
preflight_data: None,
|
||||
reason: Some("tree-shell create".into()),
|
||||
refs: vec!["mnote-web-tree".into()],
|
||||
dry_run: false,
|
||||
@@ -2423,6 +2899,7 @@ fn create_command_wire(
|
||||
"documentId": document_id,
|
||||
"title": title,
|
||||
}),
|
||||
preflight_data: None,
|
||||
reason: Some("tree-shell rename".into()),
|
||||
refs: vec!["mnote-web-tree".into()],
|
||||
dry_run: false,
|
||||
@@ -2461,6 +2938,7 @@ fn create_command_wire(
|
||||
"parentId": parent_id,
|
||||
"sortOrder": sort_order,
|
||||
}),
|
||||
preflight_data: None,
|
||||
reason: Some("tree-shell move".into()),
|
||||
refs: vec!["mnote-web-tree".into()],
|
||||
dry_run: false,
|
||||
@@ -2645,6 +3123,11 @@ mod tests {
|
||||
assert!(html.contains("test-shell"));
|
||||
assert!(html.contains("tree-action-menu"));
|
||||
assert!(html.contains("tree.page.context-menu"));
|
||||
assert!(html.contains("tree.page.expand.changed"));
|
||||
assert!(html.contains("tree.page.focus.changed"));
|
||||
assert!(html.contains("tree.shell.state.patch"));
|
||||
assert!(html.contains("application/x-mnote-page-tree-node"));
|
||||
assert!(html.contains("页面已拖放到"));
|
||||
assert!(html.contains("setAttribute(\"role\", \"treeitem\")"));
|
||||
assert!(html.contains("setAttribute(\"aria-level\""));
|
||||
}
|
||||
@@ -2670,6 +3153,8 @@ mod tests {
|
||||
assert!(html.contains("\"allowRootPick\":true"));
|
||||
assert!(html.contains("\"excludeIds\":[\"page_child\"]"));
|
||||
assert!(html.contains("tree.pick.root"));
|
||||
assert!(html.contains("tree.picker.command"));
|
||||
assert!(html.contains("tree.picker.focus.changed"));
|
||||
assert!(html.contains("__MNOTE_TREE_SHELL_OVERRIDE__"));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user