4-26 树rust-2

This commit is contained in:
lix-2026
2026-04-26 04:29:23 +08:00
parent 94631f3636
commit 338bb2e20f
58 changed files with 11718 additions and 1256 deletions
+502 -17
View File
@@ -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__"));
}