// MNote debug /tree shell 浏览器运行时外置模块。 // 该文件由 tree.rs 原 inline runtime 同构迁出,语义仍由 Rust tree_shell reducer 主导。 import { reconcilePageRuntimeResult, reducePageActionWithRuntime, } from "./tree-shell-page-runtime.js"; import { buildFileTreeRuntimeEnvironment, compareTreeShellItems, getFileTreeRowAssetId, getFileTreeRowDocumentId, getFileTreeRowIconKind, getFileTreeRowMetaLabel, getFileTreeRowOwnerDocumentId, normalizeTreeShellTreeItems, } from "./tree-shell-filetree-runtime.js"; import { createTreeShellFileTreeMenuRuntime, } from "./tree-shell-filetree-menu-runtime.js"; import { createTreeShellFileTreeDndRuntime, TREE_SHELL_FILETREE_DRAG_MIME as FILETREE_DRAG_MIME, } from "./tree-shell-filetree-dnd-runtime.js"; import { createTreeShellRenderer, } from "./tree-shell-render-runtime.js"; import { focusTreeShellPickerRowElement, hydrateTreeShellInitialPageTree, hydrateTreeShellInitialPickerTree, patchTreeShellPageActiveDom, patchTreeShellPageExpansionDom, patchTreeShellPickerActiveDom, } from "./tree-shell-dom-runtime.js"; import { computeTreeShellPickerStateActionResult, getTreeShellVisiblePickerEntries, isTreeShellPickerEntryPickable, normalizeTreeShellPickerItemKey, resolveTreeShellCurrentPickerItemKey, } from "./tree-shell-picker-runtime.js"; import { normalizeTreeShellNumber as normalizeNumber, normalizeTreeShellStringArray as normalizeStringArray, normalizeTreeShellText as normalizeText, parseTreeShellState, parseTreeShellStateFromHtml, resolveTreeShellMode, resolveTreeShellTargetOrigin, } from "./tree-shell-state-runtime.js"; function startTreeShellRuntime() { const stateElement = document.getElementById("tree-shell-state"); const appElement = document.getElementById("tree-shell-app"); const statusElement = document.getElementById("tree-shell-status"); const lastActionElement = document.getElementById("tree-shell-last-action"); const createRootButton = document.getElementById("tree-create-root"); if (!stateElement || !appElement || !statusElement || !lastActionElement || !createRootButton) { return; } const state = parseTreeShellState(stateElement); const rendererInput = state.rendererInput && typeof state.rendererInput === "object" ? state.rendererInput : {}; const rendererFiletreeSelection = rendererInput.filetreeSelection && typeof rendererInput.filetreeSelection === "object" ? rendererInput.filetreeSelection : {}; const filetreeSelectionReducer = rendererInput.filetreeSelectionReducer && typeof rendererInput.filetreeSelectionReducer === "object" ? rendererInput.filetreeSelectionReducer : {}; const pickerStateReducer = rendererInput.pickerStateReducer && typeof rendererInput.pickerStateReducer === "object" ? rendererInput.pickerStateReducer : {}; const pageFocusKeyboardReducer = rendererInput.pageFocusKeyboardReducer && typeof rendererInput.pageFocusKeyboardReducer === "object" ? rendererInput.pageFocusKeyboardReducer : {}; const runtimeArtifact = rendererInput.runtimeArtifact && typeof rendererInput.runtimeArtifact === "object" ? rendererInput.runtimeArtifact : {}; const runtimeApi = runtimeArtifact.runtimeApi && typeof runtimeArtifact.runtimeApi === "object" ? runtimeArtifact.runtimeApi : {}; const hostOverride = window.__MNOTE_TREE_SHELL_OVERRIDE__ && typeof window.__MNOTE_TREE_SHELL_OVERRIDE__ === "object" ? window.__MNOTE_TREE_SHELL_OVERRIDE__ : {}; const channel = typeof state.channel === "string" && state.channel.trim() ? state.channel.trim() : "mnote-tree-shell-v1"; const workspaceId = typeof state.workspaceId === "string" && state.workspaceId.trim() ? state.workspaceId.trim() : ""; const sourceKind = typeof state.sourceKind === "string" && state.sourceKind.trim() ? state.sourceKind.trim() : "local_folder"; const rootUri = typeof state.rootUri === "string" && state.rootUri.trim() ? state.rootUri.trim() : ""; const initialLocalWatchRevision = state.localWatchRevision && typeof state.localWatchRevision === "object" && typeof state.localWatchRevision.revision === "string" ? state.localWatchRevision.revision : ""; const actorId = typeof state.actorId === "string" && state.actorId.trim() ? state.actorId.trim() : ""; const activeDocumentId = 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 rendererInput.activePickerItem === "string" && rendererInput.activePickerItem.trim() ? rendererInput.activePickerItem.trim() : typeof state.activePickerItemKey === "string" && state.activePickerItemKey.trim() ? state.activePickerItemKey.trim() : ""; const mode = resolveTreeShellMode(state); const allowRootPick = state.allowRootPick === true; const excludedIds = new Set( normalizeStringArray(rendererInput.excludedPickerIds).length > 0 ? normalizeStringArray(rendererInput.excludedPickerIds) : normalizeStringArray(state.excludeIds), ); const pickerStateReducerContractName = typeof pickerStateReducer.contractName === "string" && pickerStateReducer.contractName.trim() ? pickerStateReducer.contractName.trim() : ""; const pickerStateReducerActions = new Set( normalizeStringArray(pickerStateReducer.actions), ); const pageFocusKeyboardReducerContractName = typeof pageFocusKeyboardReducer.contractName === "string" && pageFocusKeyboardReducer.contractName.trim() ? pageFocusKeyboardReducer.contractName.trim() : ""; const pageFocusKeyboardReducerActions = new Set( normalizeStringArray(pageFocusKeyboardReducer.actions), ); const commandPath = typeof state.commandPath === "string" && state.commandPath.trim() ? state.commandPath.trim() : "/api/tree/commands"; let mediaAssets = Array.isArray(state.mediaAssets) ? state.mediaAssets : []; let mindmapAssets = Array.isArray(state.mindmapAssets) ? state.mindmapAssets : []; let tableAssets = Array.isArray(state.tableAssets) ? state.tableAssets : []; const mindmapAssetChildren = state.mindmapAssetChildren && typeof state.mindmapAssetChildren === "object" ? state.mindmapAssetChildren : {}; const titleElement = document.getElementById("tree-shell-title"); const summaryElement = document.getElementById("tree-shell-summary"); const toolbarElement = document.getElementById("tree-shell-toolbar"); const targetOrigin = resolveTreeShellTargetOrigin(document); const initialRenameRowId = (() => { try { return normalizeText(new URL(window.location.href).searchParams.get("renameRowId")); } catch { return ""; } })(); const runtimeReduceEndpoint = normalizeText( runtimeApi.reduceEndpoint, "/api/tree/runtime/reduce", ); let rawItems = Array.isArray(hostOverride.items) ? hostOverride.items : state.items; let normalizedItems = normalizeTreeShellTreeItems(rawItems, { excludedIds }); let itemById = new Map(); let fileTreeRowById = new Map(); let childrenByParentId = new Map(); let roots = []; let assetsByDocId = new Map(); const rebuildTreeIndexes = () => { itemById = new Map(normalizedItems.map((item) => [item.nodeId, item])); fileTreeRowById = new Map(normalizedItems.map((item) => [item.rowId, item])); childrenByParentId = new Map(); roots = []; assetsByDocId = new Map(); [...mediaAssets, ...mindmapAssets, ...tableAssets].forEach((asset) => { const documentId = normalizeText(asset?.document_id); const assetId = normalizeText(asset?.id); if (!documentId || !assetId) return; const bucket = assetsByDocId.get(documentId) || []; bucket.push({ id: assetId, documentId, assetType: normalizeText(asset?.asset_type, "file"), fileName: normalizeText(asset?.file_name, "附件"), storagePath: normalizeText(asset?.storage_path), }); assetsByDocId.set(documentId, bucket); }); normalizedItems.forEach((item) => { const parentId = item.parentNodeId && itemById.has(item.parentNodeId) ? item.parentNodeId : null; if (!parentId) { roots.push(item); return; } const bucket = childrenByParentId.get(parentId) || []; bucket.push(item); childrenByParentId.set(parentId, bucket); }); roots.sort(compareTreeShellItems); childrenByParentId.forEach((bucket) => bucket.sort(compareTreeShellItems)); }; rebuildTreeIndexes(); const rendererExpandedIds = normalizeStringArray(rendererInput.expandedIds); const expanded = new Set( rendererExpandedIds.length > 0 ? rendererExpandedIds : normalizedItems .filter((item) => item.childCount > 0 && item.expandedByDefault) .map((item) => item.nodeId), ); 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(); const rendererSelectedFileTreeRowIds = normalizeStringArray( rendererFiletreeSelection.selectedRowIds, ); const rendererAnchorRowId = typeof rendererFiletreeSelection.anchorRowId === "string" && rendererFiletreeSelection.anchorRowId.trim() ? rendererFiletreeSelection.anchorRowId.trim() : null; const rendererFocusedRowId = typeof rendererFiletreeSelection.focusedRowId === "string" && rendererFiletreeSelection.focusedRowId.trim() ? rendererFiletreeSelection.focusedRowId.trim() : null; const filetreeSelectionReducerContractName = typeof filetreeSelectionReducer.contractName === "string" && filetreeSelectionReducer.contractName.trim() ? filetreeSelectionReducer.contractName.trim() : ""; const filetreeSelectionReducerActions = new Set( normalizeStringArray(filetreeSelectionReducer.actions), ); let selectedFileTreeRowIds = new Set( rendererSelectedFileTreeRowIds.length > 0 ? rendererSelectedFileTreeRowIds : currentActiveDocumentId ? [`doc:${currentActiveDocumentId}`] : [] ); let fileTreeAnchorRowId = rendererAnchorRowId || (currentActiveDocumentId ? `doc:${currentActiveDocumentId}` : null); let fileTreeFocusedRowId = rendererFocusedRowId || (currentActiveDocumentId ? `doc:${currentActiveDocumentId}` : null); let visibleFileTreeRowIds = []; let draggingPageNodeId = ""; let draggingFileTreeRowIds = []; let activePageDropNodeId = null; let fileTreeClipboard = { action: null, rowIds: [] }; let inlineRenameState = { mode: null, id: null, committing: false }; let renderNode = () => { throw new Error("tree shell renderer is not ready"); }; let renderTree = () => { throw new Error("tree shell renderer is not ready"); }; let syncFileTreeSelectionDom = () => {}; let openHydratedFileTreeItem = () => {}; let hydrateInitialFileTree = () => false; let activeCursor = itemById.get(currentActiveDocumentId) || null; while (activeCursor && activeCursor.parentNodeId && itemById.has(activeCursor.parentNodeId)) { expanded.add(activeCursor.parentNodeId); activeCursor = itemById.get(activeCursor.parentNodeId) || null; } assetsByDocId.forEach((_, documentId) => { if (itemById.has(documentId)) { expanded.add(documentId); } }); if (titleElement) { titleElement.textContent = mode === "picker" ? "页面选择" : mode === "filetree" ? "资源管理器" : "页面树"; } if (summaryElement) { summaryElement.textContent = mode === "picker" ? "这个页面复用统一 projection 协议,以轻量选择器模式承载 move / embed picker。当前阶段只负责树浏览与目标选择,不承接命令写链。" : mode === "filetree" ? "这个页面以文件树模式消费 Rust 侧 projection,并承载页面、index 与附件浏览。当前阶段仍是过渡验证壳,但 filetree 协议与渲染分支必须保持闭环。" : "这个页面直接消费 Rust 侧 projection,并通过统一 command route 回写树操作。当前阶段先交付最小可交互壳,用于主 Sidebar 页面树切流与真实网页验证。"; } if (toolbarElement && mode === "picker") { toolbarElement.style.display = "none"; } let busy = false; const setBusy = (nextBusy) => { busy = nextBusy; createRootButton.disabled = nextBusy; appElement.querySelectorAll("button").forEach((button) => { button.disabled = nextBusy; }); }; const setStatus = (message, tone = "normal") => { statusElement.textContent = message; statusElement.dataset.tone = tone; }; const setLastAction = (message, tone = "normal") => { lastActionElement.textContent = message; lastActionElement.dataset.tone = tone; }; const focusRowElement = (nodeId) => { if (!nodeId) return; window.requestAnimationFrame(() => { const row = appElement.querySelector(`.tree-row[data-node-id="${nodeId}"]`); if (!(row instanceof HTMLElement)) return; row.focus({ preventScroll: true }); row.scrollIntoView({ block: "nearest" }); }); }; const postToHost = (type, extra = {}) => { if (window.parent === window) return; const payload = Object.assign({ channel, type }, extra); window.parent.postMessage(payload, targetOrigin); }; const canExpandFileTreeRow = (item) => { if (!item) return false; return item.childCount > 0 || item.capabilities.includes("expand"); }; const fileTreeDndRuntime = createTreeShellFileTreeDndRuntime({ appElement, expanded, sourceKind, workspaceId, canExpandFileTreeRow, getFileTreeAnchorRowId: () => fileTreeAnchorRowId, getFileTreeFocusedRowId: () => fileTreeFocusedRowId, getFileTreeRowById: () => fileTreeRowById, getItemById: () => itemById, getSelectedFileTreeRowIds: () => selectedFileTreeRowIds, getVisibleFileTreeRowIds: () => visibleFileTreeRowIds, renderTree: () => renderTree(), scheduleRefresh: (...args) => scheduleRefresh(...args), sendCommand: (...args) => sendCommand(...args), setLastAction, setStatus, }); const clearFileTreeDropFeedback = fileTreeDndRuntime.clearFileTreeDropFeedback; const executeLocalFileTreeExternalDrop = fileTreeDndRuntime.executeLocalFileTreeExternalDrop; const executeLocalFileTreeInternalDrop = fileTreeDndRuntime.executeLocalFileTreeInternalDrop; const filterRedundantFileTreeRowIds = fileTreeDndRuntime.filterRedundantFileTreeRowIds; const getActiveFileTreeDropPosition = fileTreeDndRuntime.getActiveFileTreeDropPosition; const getActiveFileTreeDropRowId = fileTreeDndRuntime.getActiveFileTreeDropRowId; const getActiveFileTreeRootDrop = fileTreeDndRuntime.getActiveFileTreeRootDrop; const getFileTreeDropTargetFromEvent = fileTreeDndRuntime.getFileTreeDropTargetFromEvent; const inferDefaultFileTreeDropDocumentId = fileTreeDndRuntime.inferDefaultFileTreeDropDocumentId; const runFileTreePreflight = fileTreeDndRuntime.runFileTreePreflight; const setFileTreeDropFeedback = fileTreeDndRuntime.setFileTreeDropFeedback; const updateFileTreeDropFeedback = fileTreeDndRuntime.updateFileTreeDropFeedback; const validateFileTreeInternalDrop = fileTreeDndRuntime.validateFileTreeInternalDrop; const isExternalFileDrag = (event) => { const types = event.dataTransfer?.types; return Array.isArray(types) ? types.includes("Files") : types instanceof DOMStringList ? types.contains("Files") : false; }; const isInternalFileTreeDrag = (event) => { const types = event.dataTransfer?.types; return Array.isArray(types) ? types.includes(FILETREE_DRAG_MIME) : types instanceof DOMStringList ? types.contains(FILETREE_DRAG_MIME) : false; }; const emitFileTreeSelectionChange = () => { if (mode !== "filetree") return; postToHost("tree.filetree.selection.changed", { selectedRowIds: Array.from(selectedFileTreeRowIds), anchorRowId: fileTreeAnchorRowId, focusedRowId: fileTreeFocusedRowId, payload: { selectedRowIds: Array.from(selectedFileTreeRowIds), anchorRowId: fileTreeAnchorRowId, focusedRowId: fileTreeFocusedRowId, }, }); }; const getFileTreeRangeRowIds = (fromId, toId) => { const fromIndex = visibleFileTreeRowIds.indexOf(fromId); const toIndex = visibleFileTreeRowIds.indexOf(toId); if (fromIndex < 0 || toIndex < 0) { return [toId]; } const lo = Math.min(fromIndex, toIndex); const hi = Math.max(fromIndex, toIndex); return visibleFileTreeRowIds.slice(lo, hi + 1); }; const normalizeFileTreeSelectionState = (selection) => { const selectedRowIds = selection?.selectedRowIds instanceof Set ? new Set( Array.from(selection.selectedRowIds) .map((rowId) => normalizeText(rowId)) .filter(Boolean), ) : new Set(normalizeStringArray(selection?.selectedRowIds)); const anchorRowId = normalizeText(selection?.anchorRowId) || null; const focusedRowId = normalizeText(selection?.focusedRowId) || null; return { selectedRowIds, anchorRowId, focusedRowId, }; }; const readFileTreeSelectionState = () => normalizeFileTreeSelectionState({ selectedRowIds: Array.from(selectedFileTreeRowIds), anchorRowId: fileTreeAnchorRowId, focusedRowId: fileTreeFocusedRowId, }); const commitFileTreeSelection = (nextSelection) => { const normalizedSelection = normalizeFileTreeSelectionState(nextSelection); selectedFileTreeRowIds = normalizedSelection.selectedRowIds; fileTreeAnchorRowId = normalizedSelection.anchorRowId; fileTreeFocusedRowId = normalizedSelection.focusedRowId; emitFileTreeSelectionChange(); return normalizedSelection; }; const computeFileTreeSelectionActionResult = (action) => { const currentSelection = readFileTreeSelectionState(); if ( mode !== "filetree" || filetreeSelectionReducerContractName !== "rust_filetree_selection_reducer_v1" || !filetreeSelectionReducerActions.has(action?.kind || "") ) { return { nextSelection: currentSelection, dragRowIds: action?.kind === "resolve_drag_rows" ? [normalizeText(action?.rowId)].filter(Boolean) : null, }; } if (action.kind === "select_row") { const rowId = normalizeText(action.rowId); if (!rowId) { return { nextSelection: currentSelection, dragRowIds: null }; } const shiftKey = action.modifiers?.shiftKey === true; const metaKey = action.modifiers?.metaKey === true; const ctrlKey = action.modifiers?.ctrlKey === true; const toggleSelection = metaKey || ctrlKey; if (shiftKey) { const anchor = currentSelection.anchorRowId || currentSelection.focusedRowId || rowId; const nextSelection = toggleSelection ? new Set(currentSelection.selectedRowIds) : new Set(); getFileTreeRangeRowIds(anchor, rowId).forEach((id) => { nextSelection.add(id); }); return { nextSelection: { selectedRowIds: nextSelection, anchorRowId: currentSelection.anchorRowId || anchor, focusedRowId: rowId, }, dragRowIds: null, }; } if (toggleSelection) { const nextSelection = new Set(currentSelection.selectedRowIds); if (nextSelection.has(rowId)) { nextSelection.delete(rowId); } else { nextSelection.add(rowId); } return { nextSelection: { selectedRowIds: nextSelection, anchorRowId: rowId, focusedRowId: rowId, }, dragRowIds: null, }; } return { nextSelection: { selectedRowIds: new Set([rowId]), anchorRowId: rowId, focusedRowId: rowId, }, dragRowIds: null, }; } if (action.kind === "select_context_row") { const rowId = normalizeText(action.rowId); if (!rowId) { return { nextSelection: currentSelection, dragRowIds: null }; } if (currentSelection.selectedRowIds.has(rowId)) { return { nextSelection: { selectedRowIds: new Set(currentSelection.selectedRowIds), anchorRowId: currentSelection.anchorRowId, focusedRowId: rowId, }, dragRowIds: null, }; } return { nextSelection: { selectedRowIds: new Set([rowId]), anchorRowId: rowId, focusedRowId: rowId, }, dragRowIds: null, }; } if (action.kind === "normalize_visible_rows") { const visibleSet = new Set(normalizeStringArray(action.visibleRowIds)); const nextSelectedRowIds = Array.from(currentSelection.selectedRowIds).filter((rowId) => visibleSet.has(rowId), ); return { nextSelection: { selectedRowIds: new Set(nextSelectedRowIds), anchorRowId: currentSelection.anchorRowId && visibleSet.has(currentSelection.anchorRowId) ? currentSelection.anchorRowId : null, focusedRowId: currentSelection.focusedRowId && visibleSet.has(currentSelection.focusedRowId) ? currentSelection.focusedRowId : null, }, dragRowIds: null, }; } if (action.kind === "clear") { return { nextSelection: { selectedRowIds: new Set(), anchorRowId: null, focusedRowId: null, }, dragRowIds: null, }; } if (action.kind === "resolve_drag_rows") { const rowId = normalizeText(action.rowId); if (!rowId) { return { nextSelection: currentSelection, dragRowIds: [] }; } return { nextSelection: currentSelection, dragRowIds: currentSelection.selectedRowIds.has(rowId) ? Array.from(currentSelection.selectedRowIds) : [rowId], }; } return { nextSelection: currentSelection, dragRowIds: null }; }; const applyFileTreeSelectionAction = (action) => { const result = computeFileTreeSelectionActionResult(action); if (action?.kind !== "resolve_drag_rows") { commitFileTreeSelection(result.nextSelection); } return result; }; const selectFileTreeRow = (rowId, modifiers = {}) => { applyFileTreeSelectionAction({ kind: "select_row", rowId, modifiers: { shiftKey: modifiers.shiftKey === true, ctrlKey: modifiers.ctrlKey === true, metaKey: modifiers.metaKey === true, }, }); }; const selectFileTreeContextRow = (rowId) => { applyFileTreeSelectionAction({ kind: "select_context_row", rowId, }); }; const clearFileTreeSelection = () => { if (mode !== "filetree") return; if ( selectedFileTreeRowIds.size === 0 && !fileTreeAnchorRowId && !fileTreeFocusedRowId ) { return; } applyFileTreeSelectionAction({ kind: "clear" }); renderTree(); }; const normalizeFileTreeSelectionForVisibleRows = () => { if (mode !== "filetree") return; const currentSelection = readFileTreeSelectionState(); const nextSelection = computeFileTreeSelectionActionResult({ kind: "normalize_visible_rows", visibleRowIds: visibleFileTreeRowIds, }).nextSelection; if ( nextSelection.selectedRowIds.size === currentSelection.selectedRowIds.size && Array.from(nextSelection.selectedRowIds).every((rowId) => currentSelection.selectedRowIds.has(rowId), ) && nextSelection.anchorRowId === currentSelection.anchorRowId && nextSelection.focusedRowId === currentSelection.focusedRowId ) { return; } commitFileTreeSelection(nextSelection); }; const resolveFileTreeDraggedRowIds = (rowId) => computeFileTreeSelectionActionResult({ kind: "resolve_drag_rows", rowId, }).dragRowIds || []; const patchFileTreeCutDecoration = () => { appElement.querySelectorAll('[data-rust-rendered-row="filetree"], .tree-row[data-shell-mode="filetree"]').forEach((row) => { if (!(row instanceof HTMLElement)) return; row.dataset.cut = String( fileTreeClipboard.action === "cut" && fileTreeClipboard.rowIds.includes(normalizeText(row.dataset.rowId)), ); }); }; const focusFileTreeRowByOffset = (offset) => { if (visibleFileTreeRowIds.length === 0) return; const currentIndex = Math.max(0, visibleFileTreeRowIds.indexOf(fileTreeFocusedRowId)); const nextIndex = Math.max(0, Math.min(visibleFileTreeRowIds.length - 1, currentIndex + offset)); const rowId = visibleFileTreeRowIds[nextIndex]; if (!rowId) return; commitFileTreeSelection({ selectedRowIds: [rowId], anchorRowId: rowId, focusedRowId: rowId }); const row = appElement.querySelector(`.tree-row[data-shell-mode="filetree"][data-row-id="${CSS.escape(rowId)}"]`); if (row instanceof HTMLElement) row.focus({ preventScroll: true }); }; const setFileTreeClipboard = (action) => { const rowIds = selectedFileTreeRowIds.size > 0 ? Array.from(selectedFileTreeRowIds) : fileTreeFocusedRowId ? [fileTreeFocusedRowId] : []; fileTreeClipboard = { action, rowIds }; patchFileTreeCutDecoration(); setLastAction(action === "cut" ? "已剪切选中文件树节点" : "已复制选中文件树节点"); }; const resolveFileTreeActionRowIds = (rowId) => { const normalizedRowId = normalizeText(rowId); if (normalizedRowId && selectedFileTreeRowIds.has(normalizedRowId)) { return Array.from(selectedFileTreeRowIds); } if (normalizedRowId) return [normalizedRowId]; if (selectedFileTreeRowIds.size > 0) return Array.from(selectedFileTreeRowIds); return fileTreeFocusedRowId ? [fileTreeFocusedRowId] : []; }; const runFileTreeDelete = async (rowId) => { const rowIds = resolveFileTreeActionRowIds(rowId); const deletableItems = rowIds .map((id) => fileTreeRowById.get(id)) .filter((item) => Boolean(getFileTreeRowDocumentId(item)) || (sourceKind === "local_folder" && item?.rowKind === "asset")); if (deletableItems.length === 0) { setLastAction("当前选择没有可删除的页面或 Markdown 文件", "error"); return false; } const accepted = await runFileTreePreflight("delete", { rowIds: deletableItems.map((item) => item.rowId), }); if (!accepted) return false; if (sourceKind === "local_folder") { for (const item of deletableItems) { await sendCommand({ action: "delete", workspaceId, documentId: getFileTreeRowDocumentId(item) || item.rowId, }); } scheduleRefresh(); return true; } if (sourceKind === "convex_workspace") { for (const item of deletableItems) { const documentId = getFileTreeRowDocumentId(item); await sendCommand({ action: "delete", workspaceId, documentId, }); applyRemovedDocumentLocally(documentId); } if (!deletableItems.every((item) => !fileTreeRowById.has(item.rowId))) { scheduleRefresh(); } return true; } postToHost("tree.filetree.delete", { workspaceId, rowIds: deletableItems.map((item) => item.rowId), payload: { workspaceId, rowIds: deletableItems.map((item) => item.rowId), documentIds: deletableItems.map(getFileTreeRowDocumentId), }, }); return true; }; const pasteFileTreeClipboardInto = async (targetRowId) => { if (!fileTreeClipboard.action || fileTreeClipboard.rowIds.length === 0) return; const targetItem = targetRowId ? fileTreeRowById.get(targetRowId) : null; const target = targetItem ? { rowId: targetItem.rowId, rowKind: targetItem.rowKind, nodeId: targetItem.nodeId, documentId: getFileTreeRowDocumentId(targetItem) || null, ownerDocumentId: getFileTreeRowOwnerDocumentId(targetItem) || null, assetId: getFileTreeRowAssetId(targetItem) || null, } : { rowId: null, rowKind: "root", nodeId: null, documentId: null, ownerDocumentId: null, assetId: null }; if (sourceKind === "local_folder") { const pasted = await executeLocalFileTreeInternalDrop( target, fileTreeClipboard.rowIds, fileTreeClipboard.action === "copy", "paste", ); if (pasted && fileTreeClipboard.action === "cut") { fileTreeClipboard = { action: null, rowIds: [] }; } patchFileTreeCutDecoration(); } else { const accepted = await runFileTreePreflight("paste", { target, rowIds: fileTreeClipboard.rowIds, copy: fileTreeClipboard.action === "copy", }); if (!accepted) return; if (sourceKind === "convex_workspace") { const sourceRowIds = filterRedundantFileTreeRowIds(fileTreeClipboard.rowIds); for (const rowId of sourceRowIds) { const sourceItem = fileTreeRowById.get(rowId); const documentId = getFileTreeRowDocumentId(sourceItem); if (!documentId) continue; await sendCommand({ action: fileTreeClipboard.action === "copy" ? "copy" : "move", workspaceId, documentId, parentId: target.documentId || null, targetParentId: target.documentId || null, sortOrder: 0, }); } if (fileTreeClipboard.action === "cut") { fileTreeClipboard = { action: null, rowIds: [] }; } patchFileTreeCutDecoration(); scheduleRefresh(); } else { postToHost("tree.filetree.paste", { workspaceId, rowIds: fileTreeClipboard.rowIds, action: fileTreeClipboard.action, targetRowId: target.rowId, targetRowKind: target.rowKind, documentId: target.documentId, payload: { workspaceId, rowIds: fileTreeClipboard.rowIds, action: fileTreeClipboard.action, targetRowId: target.rowId, targetRowKind: target.rowKind, documentId: target.documentId, }, }); } } }; const handleFileTreeKeyDown = (event, item) => { if (mode !== "filetree") return; if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "c") { event.preventDefault(); setFileTreeClipboard("copy"); return; } if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "x") { event.preventDefault(); setFileTreeClipboard("cut"); return; } if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "v") { event.preventDefault(); void pasteFileTreeClipboardInto(item?.rowId || fileTreeFocusedRowId); return; } if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a") { event.preventDefault(); commitFileTreeSelection({ selectedRowIds: visibleFileTreeRowIds, anchorRowId: visibleFileTreeRowIds[0] || null, focusedRowId: visibleFileTreeRowIds[visibleFileTreeRowIds.length - 1] || null, }); renderTree(); return; } if (event.key === "F2") { event.preventDefault(); const rowId = item?.rowId || fileTreeFocusedRowId; const renameItem = rowId ? fileTreeRowById.get(rowId) : null; if (rowId && getFileTreeRowDocumentId(renameItem)) { beginInlineRename("filetree", rowId); } else { setLastAction("当前资源暂不支持重命名", "error"); } return; } if (event.key === "ArrowDown") { event.preventDefault(); focusFileTreeRowByOffset(1); return; } if (event.key === "ArrowUp") { event.preventDefault(); focusFileTreeRowByOffset(-1); return; } if (event.key === "Home") { event.preventDefault(); focusFileTreeRowByOffset(-visibleFileTreeRowIds.length); return; } if (event.key === "End") { event.preventDefault(); focusFileTreeRowByOffset(visibleFileTreeRowIds.length); return; } if (event.key === "Enter") { event.preventDefault(); if (item) { const documentId = getFileTreeRowDocumentId(item); if (documentId) { handleNavigate(documentId); } else { openHydratedFileTreeItem(item); } } return; } if (event.key === "Delete" || event.key === "Backspace") { event.preventDefault(); void runFileTreeDelete(item?.rowId || fileTreeFocusedRowId); } }; const applyTreeShellStateSnapshot = (nextState, options = {}) => { if (!nextState || typeof nextState !== "object") return false; mediaAssets = Array.isArray(nextState.mediaAssets) ? nextState.mediaAssets : []; mindmapAssets = Array.isArray(nextState.mindmapAssets) ? nextState.mindmapAssets : []; tableAssets = Array.isArray(nextState.tableAssets) ? nextState.tableAssets : []; rawItems = Array.isArray(nextState.items) ? nextState.items : []; normalizedItems = normalizeTreeShellTreeItems(rawItems, { excludedIds }); rebuildTreeIndexes(); if (currentActiveDocumentId && !itemById.has(currentActiveDocumentId)) { currentActiveDocumentId = roots[0]?.nodeId || ""; } if (currentFocusedDocumentId && !itemById.has(currentFocusedDocumentId)) { currentFocusedDocumentId = currentActiveDocumentId; } normalizedItems .filter((item) => item.childCount > 0 && item.expandedByDefault) .forEach((item) => expanded.add(item.nodeId)); focusedNodeId = resolveFocusedNodeIdFromHostState(); renderTree(); if (mode === "filetree") { emitFileTreeSelectionChange(); } const renameRowId = normalizeText(options.renameRowId); if (renameRowId) { window.setTimeout(() => { if (fileTreeRowById.has(renameRowId)) { beginInlineRename("filetree", renameRowId); } }, 80); } return true; }; const refreshLocalFolderSnapshot = async (options = {}) => { const response = await fetch(window.location.href, { headers: { "accept": "text/html" }, }); if (!response.ok) return false; const nextState = parseTreeShellStateFromHtml(await response.text()); document.documentElement.setAttribute( "data-mnote-local-folder-watch-applied", options.appliedValue || "projection", ); document.documentElement.removeAttribute("data-mnote-local-folder-watch-disabled"); return applyTreeShellStateSnapshot(nextState, options); }; const scheduleRefresh = (options = {}) => { window.setTimeout(() => { void refreshLocalFolderSnapshot(options); }, 80); }; let localFolderEventRefreshPending = false; const scheduleLocalFolderEventRefresh = (options = {}) => { if (localFolderEventRefreshPending) return; localFolderEventRefreshPending = true; window.setTimeout(() => { localFolderEventRefreshPending = false; void refreshLocalFolderSnapshot(options); }, 120); }; const addTreeItemLocally = (item) => { if (!item?.nodeId || itemById.has(item.nodeId)) return false; normalizedItems.push(item); itemById.set(item.nodeId, item); fileTreeRowById.set(item.rowId, item); const parentId = item.parentNodeId && itemById.has(item.parentNodeId) ? item.parentNodeId : null; if (parentId) { const bucket = childrenByParentId.get(parentId) || []; bucket.push(item); bucket.sort(compareTreeShellItems); childrenByParentId.set(parentId, bucket); const parentItem = itemById.get(parentId); if (parentItem) parentItem.childCount = Math.max(parentItem.childCount || 0, bucket.length); expanded.add(parentId); } else { roots.push(item); roots.sort(compareTreeShellItems); } return true; }; const applyCreatedDocumentLocally = (result, parentId, title) => { const documentId = typeof result?.documentId === "string" && result.documentId.trim() ? result.documentId.trim() : typeof result?.id === "string" && result.id.trim() ? result.id.trim() : ""; if (!documentId) return false; const createdAt = normalizeText(result?.updatedAt || result?.execution?.updated_at); const parentNodeId = parentId && itemById.has(parentId) ? parentId : null; const item = { rowId: `doc:${documentId}`, rowKind: "document", nodeId: documentId, parentNodeId, title: normalizeText(result?.title, title || "无标题"), depth: parentNodeId ? normalizeNumber(itemById.get(parentNodeId)?.depth, 0) + 1 : 0, childCount: 0, position: normalizeNumber(result?.sortOrder ?? result?.execution?.sort_order ?? Date.now()), expandedByDefault: true, iconHint: "page", capabilities: ["open", "rename", "delete", "move", "create"], resourceMeta: { resourceKind: "document", documentId, assetId: "", assetKind: "", objectIdentity: { objectKind: "page", documentId, blockId: null, assetId: null }, blockAssetRelation: null, }, updatedAt: createdAt, }; if (!addTreeItemLocally(item)) return false; currentFocusedDocumentId = documentId; focusedNodeId = documentId; currentActiveDocumentId = documentId; renderTree(); return true; }; const removeTreeItemEverywhere = (item) => { if (!item) return; const itemIndex = normalizedItems.indexOf(item); if (itemIndex >= 0) normalizedItems.splice(itemIndex, 1); itemById.delete(item.nodeId); fileTreeRowById.delete(item.rowId); const rootIndex = roots.indexOf(item); if (rootIndex >= 0) roots.splice(rootIndex, 1); const bucket = item.parentNodeId ? childrenByParentId.get(item.parentNodeId) : null; if (bucket) { const bucketIndex = bucket.indexOf(item); if (bucketIndex >= 0) bucket.splice(bucketIndex, 1); if (bucket.length === 0) childrenByParentId.delete(item.parentNodeId); } }; const applyRemovedDocumentLocally = (documentId) => { const normalizedDocumentId = normalizeText(documentId); if (!normalizedDocumentId) return false; const removedNodeIds = new Set([normalizedDocumentId]); let changed = false; let expandedDuringScan = true; while (expandedDuringScan) { expandedDuringScan = false; normalizedItems.forEach((item) => { if (item.parentNodeId && removedNodeIds.has(item.parentNodeId) && !removedNodeIds.has(item.nodeId)) { removedNodeIds.add(item.nodeId); expandedDuringScan = true; } }); } normalizedItems.slice().forEach((item) => { const itemDocumentId = getFileTreeRowOwnerDocumentId(item) || item.nodeId; if (removedNodeIds.has(item.nodeId) || itemDocumentId === normalizedDocumentId) { removeTreeItemEverywhere(item); changed = true; } }); if (!changed) return false; if (currentActiveDocumentId === normalizedDocumentId) currentActiveDocumentId = roots[0]?.nodeId || ""; if (currentFocusedDocumentId === normalizedDocumentId) currentFocusedDocumentId = currentActiveDocumentId; focusedNodeId = resolveFocusedNodeIdFromHostState(); renderTree(); return true; }; if (sourceKind === "local_folder" && rootUri) { let localFolderWatchRevision = initialLocalWatchRevision; const applyLocalFolderRevision = (revision) => { const nextRevision = normalizeText(revision); if (!nextRevision || nextRevision === localFolderWatchRevision) return false; localFolderWatchRevision = nextRevision; document.documentElement.setAttribute("data-mnote-tree-live-revision", nextRevision); return true; }; window.addEventListener("tree:snapshot", function(event) { const detail = event && event.detail ? event.detail : {}; const payload = detail.payload || detail; if (!payload || payload.sourceKind !== "local_folder") return; if (!applyLocalFolderRevision(payload.revision || detail.revision)) return; scheduleLocalFolderEventRefresh({ appliedValue: "snapshot" }); }); window.addEventListener("tree:resync", function(event) { const detail = event && event.detail ? event.detail : {}; const payload = detail.payload || detail; if (!payload || payload.sourceKind !== "local_folder") return; if (!applyLocalFolderRevision(payload.revision || detail.revision)) return; scheduleLocalFolderEventRefresh({ appliedValue: "resync" }); }); window.addEventListener("tree:local-folder-watch-batch", function(event) { const detail = event && event.detail ? event.detail : {}; const payload = detail.payload || detail; if (!payload || payload.sourceKind !== "local_folder") return; if (!applyLocalFolderRevision(payload.revision || detail.revision)) return; scheduleLocalFolderEventRefresh({ appliedValue: "watch_batch" }); }); window.addEventListener("tree:error", function(event) { const detail = event && event.detail ? event.detail : {}; const payload = detail.payload || detail; if (!payload || payload.sourceKind !== "local_folder") return; document.documentElement.setAttribute( "data-mnote-tree-live-error", normalizeText(payload.code || payload.error || payload.message, "tree_live_error"), ); }); const transport = document.documentElement.getAttribute("data-mnote-tree-live-transport") || ""; if (transport !== "local-folder-events") { document.documentElement.setAttribute("data-mnote-local-folder-watch-applied", "static"); document.documentElement.setAttribute("data-mnote-local-folder-watch-disabled", "events-required"); } } const readErrorMessage = async (response) => { const text = await response.text(); try { const payload = text ? JSON.parse(text) : null; const fromMessage = payload && typeof payload.message === "string" ? payload.message : payload && typeof payload.error === "string" ? payload.error : payload && payload.result && typeof payload.result.message === "string" ? payload.result.message : ""; if (fromMessage) return fromMessage; } catch { // 忽略 JSON 解析失败,继续返回文本片段 } return text ? text.slice(0, 180) : "树命令执行失败"; }; const sendCommand = async (payload) => { setBusy(true); setStatus("正在提交树命令…"); try { const commandPayload = Object.assign( {}, payload, sourceKind ? { sourceKind } : {}, rootUri ? { rootUri } : {}, ); const response = await fetch(commandPath, { method: "POST", headers: { "content-type": "application/json", "x-mnote-workspace-id": workspaceId, "x-mnote-source-channel": "mnote_web_tree_shell", "x-mnote-source-client": "mnote-web", ...(actorId ? { "x-mnote-actor-id": actorId, "x-mnote-actor-type": "user", } : {}), }, body: JSON.stringify(commandPayload), }); if (!response.ok) { throw new Error(await readErrorMessage(response)); } const data = await response.json().catch(() => null); if (!data || typeof data !== "object") { throw new Error("tree command 返回了无效响应"); } if (data.ok === true && data.result) { return data.result; } if (data.result && typeof data.result === "object") { return data.result; } return data; } finally { setBusy(false); } }; const getSiblings = (parentId) => { if (!parentId) return roots.slice(); 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 commitPageExpandedIds = (expandedIds) => { const nextExpanded = new Set(normalizeStringArray(expandedIds)); let changed = nextExpanded.size !== expanded.size; if (!changed) { changed = Array.from(nextExpanded).some((nodeId) => !expanded.has(nodeId)); } if (!changed) return false; expanded.clear(); nextExpanded.forEach((nodeId) => expanded.add(nodeId)); return true; }; const patchPageTreeAfterRuntimeState = (changedNodeIds, focusedId) => { if (mode !== "page") return; if (usedRustInitialRenderer) { const ids = normalizeStringArray(changedNodeIds); ids.forEach((nodeId) => { patchPageTreeExpansionDom(nodeId); }); patchPageTreeActiveDom(); if (focusedId) focusRowElement(focusedId); return; } renderTree(); if (focusedId) focusRowElement(focusedId); }; const applyLocalPageExpansionFallback = (nodeId, nextExpanded) => { if (nextExpanded) expanded.add(nodeId); else expanded.delete(nodeId); postPageExpandChange(nodeId, nextExpanded); if (mode === "page" && usedRustInitialRenderer && patchPageTreeExpansionDom(nodeId)) { focusRowElement(nodeId); return; } renderTree(); }; const toggleExpand = (nodeId) => { applyLocalPageExpansionFallback(nodeId, !expanded.has(nodeId)); }; const getVisiblePageItems = () => { const visible = []; const walk = (entries) => { entries.forEach((item) => { visible.push(item); if (item.childCount > 0 && expanded.has(item.nodeId)) { walk(getSiblings(item.nodeId)); } }); }; walk(roots); return visible; }; const getVisiblePickerEntries = () => getTreeShellVisiblePickerEntries({ mode, allowRootPick, roots, expanded, getSiblings, }); const isPickerEntryPickable = (entry) => isTreeShellPickerEntryPickable(entry, { allowRootPick, excludedIds, }); const getPickablePickerEntries = () => getVisiblePickerEntries().filter((entry) => isPickerEntryPickable(entry)); const normalizePickerItemKey = (pickerItemKey) => normalizeTreeShellPickerItemKey(pickerItemKey, { allowRootPick, itemById, excludedIds, }); const resolveCurrentPickerItemKey = () => resolveTreeShellCurrentPickerItemKey({ currentActivePickerItemKey, currentActiveDocumentId, normalizePickerItemKey, getPickablePickerEntries, }); const computePickerStateActionResult = (action) => computeTreeShellPickerStateActionResult({ mode, pickerStateReducerContractName, pickerStateReducerActions, resolveCurrentPickerItemKey, getPickablePickerEntries, normalizePickerItemKey, }, action); const focusNode = (nodeId) => { if (!nodeId || !itemById.has(nodeId)) return; if (focusedNodeId === nodeId) { focusRowElement(nodeId); return; } focusedNodeId = nodeId; postPageFocusChange(nodeId); if (usedRustInitialRenderer) { patchPageTreeActiveDom(); focusRowElement(nodeId); return; } renderTree(); focusRowElement(nodeId); }; const resolvePageActionItem = (action, item) => { const actionNodeId = normalizeText(action?.nodeId); if (actionNodeId && itemById.has(actionNodeId)) { return itemById.get(actionNodeId); } if (item?.nodeId && itemById.has(item.nodeId)) { return item; } return focusedNodeId && itemById.has(focusedNodeId) ? itemById.get(focusedNodeId) : null; }; const buildPageRuntimeAction = (action, item) => { const actionKind = normalizeText(action?.kind).toLowerCase(); if (actionKind === "focus") { const nodeId = normalizeText(action?.nodeId || item?.nodeId); return nodeId ? { kind: "focus", nodeId } : null; } if (actionKind === "move_next") return { kind: "moveNext" }; if (actionKind === "move_previous") return { kind: "movePrevious" }; if (actionKind === "move_home") return { kind: "moveHome" }; if (actionKind === "move_end") return { kind: "moveEnd" }; if (actionKind === "open") return { kind: "openFocused" }; if (actionKind === "context_menu") return { kind: "contextMenuFocused" }; if (actionKind === "expand" || actionKind === "collapse" || actionKind === "toggle") { const nodeId = normalizeText(action?.nodeId || item?.nodeId); return nodeId ? { kind: actionKind, nodeId } : null; } return null; }; const buildPageRuntimeEnvironment = () => ({ visibleNodeIds: getVisiblePageItems().map((entry) => entry.nodeId), expandableNodeIds: normalizedItems .filter((entry) => entry.childCount > 0 && getSiblings(entry.nodeId).length > 0) .map((entry) => entry.nodeId), }); const readPageRuntimeState = (action, item) => { const actionKind = normalizeText(action?.kind).toLowerCase(); const nodeId = normalizeText(action?.nodeId || item?.nodeId); const focusedId = (actionKind === "open" || actionKind === "context_menu") && itemById.has(nodeId) ? nodeId : focusedNodeId || null; return { focusedId, expandedIds: Array.from(expanded), dropFeedback: null, }; }; const normalizePageRuntimeResult = (runtimeResult) => { if (!runtimeResult || runtimeResult.mode !== "page") { return null; } const stateSnapshot = runtimeResult.state && runtimeResult.state.mode === "page" ? runtimeResult.state.state : null; const pagePatch = Array.isArray(runtimeResult.domPatches) ? runtimeResult.domPatches.find((patch) => patch?.kind === "pageState") : null; const focusedId = typeof pagePatch?.focusedId === "string" ? pagePatch.focusedId : typeof stateSnapshot?.focusedId === "string" ? stateSnapshot.focusedId : ""; const expandedIds = Array.isArray(pagePatch?.expandedIds) ? pagePatch.expandedIds : Array.isArray(stateSnapshot?.expandedIds) ? stateSnapshot.expandedIds : null; return { focusedId: normalizeText(focusedId), expandedIds: expandedIds ? normalizeStringArray(expandedIds) : null, hostEvents: Array.isArray(runtimeResult.hostEvents) ? runtimeResult.hostEvents : [], }; }; const replayPageRuntimeHostEvents = (runtimeResult, item, sourceElement) => { const result = normalizePageRuntimeResult(runtimeResult); if (!result) return false; let replayed = false; result.hostEvents.forEach((event) => { if (!event || typeof event !== "object") return; if (event.kind === "pageOpen") { const nodeId = normalizeText(event.nodeId); if (nodeId) { handleNavigate(nodeId); replayed = true; } return; } if (event.kind === "pageContextMenu") { const nodeId = normalizeText(event.nodeId || item?.nodeId); if (!nodeId) return; const rect = sourceElement?.getBoundingClientRect?.(); openContextMenu( nodeId, rect ? rect.left + Math.min(rect.width - 12, 28) : 0, rect ? rect.top + Math.min(rect.height - 12, 18) : 0, ); replayed = true; } }); return replayed; }; const pageRuntimeBridge = { mode, runtimeReduceEndpoint, buildPageRuntimeAction, buildPageRuntimeEnvironment, readPageRuntimeState, readErrorMessage, normalizePageRuntimeResult, commitPageExpandedIds, normalizeText, postPageFocusChange, postPageExpandChange, patchPageTreeAfterRuntimeState, get expanded() { return expanded; }, getFocusedNodeId: () => focusedNodeId, setFocusedNodeId: (nextFocusedNodeId) => { focusedNodeId = nextFocusedNodeId; }, }; const applyLocalPageActionFallback = (action, item, sourceElement) => { if (mode !== "page") return; const actionKind = normalizeText(action?.kind).toLowerCase(); if (!pageFocusKeyboardReducerActions.has(actionKind)) { return; } if (actionKind === "focus") { const nextFocusId = normalizeText(action?.nodeId); if (nextFocusId && itemById.has(nextFocusId)) { focusNode(nextFocusId); } return; } const visible = getVisiblePageItems(); const visibleIds = visible.map((entry) => entry.nodeId); if (visibleIds.length === 0) { return; } const currentFocusId = visibleIds.includes(focusedNodeId) ? focusedNodeId : visibleIds[0]; const currentIndex = visibleIds.indexOf(currentFocusId); if (actionKind === "move_next") { const next = visible[currentIndex + 1]; if (next) focusNode(next.nodeId); return; } if (actionKind === "move_previous") { const previous = visible[currentIndex - 1]; if (previous) focusNode(previous.nodeId); return; } if (actionKind === "move_home") { focusNode(visibleIds[0]); return; } if (actionKind === "move_end") { focusNode(visibleIds[visibleIds.length - 1]); return; } if (actionKind === "expand") { if (item?.childCount > 0 && !expanded.has(item.nodeId)) { applyLocalPageExpansionFallback(item.nodeId, true); focusRowElement(item.nodeId); return; } const firstChild = item ? getSiblings(item.nodeId)[0] : null; if (firstChild) { focusNode(firstChild.nodeId); } return; } if (actionKind === "collapse") { if (item?.childCount > 0 && expanded.has(item.nodeId)) { applyLocalPageExpansionFallback(item.nodeId, false); focusRowElement(item.nodeId); return; } if (item?.parentNodeId && itemById.has(item.parentNodeId)) { focusNode(item.parentNodeId); } return; } if (actionKind === "open") { if (item?.nodeId) { handleNavigate(item.nodeId); } return; } if (actionKind === "context_menu") { if (!item?.nodeId) { return; } const rect = sourceElement?.getBoundingClientRect?.(); if (!rect) { return; } openContextMenu( item.nodeId, rect.left + Math.min(rect.width - 12, 28), rect.top + Math.min(rect.height - 12, 18), ); } }; const applyPageKeyboardAction = (action, item, sourceElement) => { if (mode !== "page") return; const actionKind = normalizeText(action?.kind).toLowerCase(); if (!pageFocusKeyboardReducerActions.has(actionKind)) { return; } const runtimeItem = resolvePageActionItem(action, item); const runtimeAction = buildPageRuntimeAction(action, runtimeItem); if (!runtimeAction) { applyLocalPageActionFallback(action, runtimeItem || item, sourceElement); return; } void reducePageActionWithRuntime(pageRuntimeBridge, action, runtimeItem) .then((runtimeResult) => { const replayedHostEvent = replayPageRuntimeHostEvents( runtimeResult, runtimeItem, sourceElement, ); const reconciledState = reconcilePageRuntimeResult( pageRuntimeBridge, runtimeResult, runtimeItem, ); if (!replayedHostEvent && !reconciledState) { applyLocalPageActionFallback(action, runtimeItem || item, sourceElement); } }) .catch(() => { applyLocalPageActionFallback(action, runtimeItem || item, sourceElement); }); }; 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, options = {}) => { if (mode !== "picker") return; const result = computePickerStateActionResult({ kind: "focus", itemKey: pickerItemKey, }); const shouldFocusDom = options.focusDom === true; const nextPickerItemKey = result.nextItemKey || ""; const nextDocumentId = nextPickerItemKey && nextPickerItemKey !== "__root__" ? nextPickerItemKey : null; currentActivePickerItemKey = nextPickerItemKey; currentActiveDocumentId = nextDocumentId; focusedNodeId = nextDocumentId || ""; if (usedRustInitialRenderer) { patchPickerActiveDom(); if (shouldFocusDom) focusPickerRowElement(nextPickerItemKey); } else { renderTree(); } if (shouldFocusDom && nextDocumentId) { focusRowElement(nextDocumentId); } postPickerFocusChange(nextPickerItemKey || null); }; const applyPickerStateAction = (action) => { if (mode !== "picker") { return { nextItemKey: "", pickedDocumentId: null, pickedRoot: false, }; } const result = computePickerStateActionResult(action); if (action?.kind !== "pick") { applyPickerFocusByItemKey(result.nextItemKey); } return result; }; const postPickerPickResultToHost = (result) => { if (mode !== "picker" || !result) return; if (result.pickedRoot) { setLastAction("已选择根目录"); postToHost("tree.pick.root", { documentId: null, target: { documentId: null }, payload: { documentId: null }, }); return; } if (result.pickedDocumentId) { postToHost("tree.pick", { documentId: result.pickedDocumentId, itemKey: result.nextItemKey || result.pickedDocumentId, target: { documentId: result.pickedDocumentId }, payload: { documentId: result.pickedDocumentId }, }); } }; const handlePickerCommand = (command) => { if (mode !== "picker") return; const normalizedCommand = normalizeText(command); if (!pickerStateReducerActions.has(normalizedCommand)) { return; } if (normalizedCommand === "pick") { postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" })); return; } applyPickerStateAction({ kind: normalizedCommand }); }; const localCreatedRowIdFromCommandResult = (result, rowKind) => { const relativePath = typeof result?.execution?.relativePath === "string" && result.execution.relativePath.trim() ? result.execution.relativePath.trim() : typeof result?.relativePath === "string" && result.relativePath.trim() ? result.relativePath.trim() : ""; if (!relativePath) return ""; return `local:${rowKind}:${relativePath}`; }; const fileTreeMenuRuntime = createTreeShellFileTreeMenuRuntime({ sourceKind, workspaceId, getFileTreeRowById: () => fileTreeRowById, getSelectedFileTreeRowIds: () => selectedFileTreeRowIds, getFileTreeClipboard: () => fileTreeClipboard, setFileTreeClipboard, commitFileTreeSelection, syncFileTreeSelectionDom, pasteFileTreeClipboardInto, runFileTreeDelete, handleCreate: (...args) => handleCreate(...args), sendCommand, setStatus, setLastAction, scheduleRefresh, refreshLocalFolderSnapshot, expanded, renderTree: () => renderTree(), postToHost, openHydratedFileTreeItem: (...args) => openHydratedFileTreeItem(...args), beginInlineRename: (...args) => beginInlineRename(...args), localCreatedRowIdFromCommandResult, }); const openFileTreeContextMenu = fileTreeMenuRuntime.openFileTreeContextMenu; const openContextMenu = (nodeId, clientX, clientY) => { if (!nodeId || mode !== "page") return; setLastAction(`已打开页面 ${nodeId} 的上下文菜单`); postToHost("tree.page.context-menu", { documentId: nodeId, target: { documentId: nodeId }, payload: { documentId: nodeId, x: clientX, y: clientY }, x: clientX, y: clientY, }); }; const getElementCenter = (element) => { const rect = element.getBoundingClientRect(); return { x: rect.left + rect.width / 2, y: rect.top + rect.height / 2, }; }; const readFileTreeInternalDropPayload = (event) => { const raw = event.dataTransfer?.getData("application/x-mnote-file-tree") || event.dataTransfer?.getData("text/plain") || ""; if (!raw) return null; try { const payload = JSON.parse(raw); if ( payload?.type !== "mnote-file-tree-dnd" || payload.version !== 1 || !Array.isArray(payload.rowIds) ) { return null; } const rowIds = payload.rowIds .map((value) => normalizeText(value)) .filter(Boolean); return rowIds.length > 0 ? rowIds : null; } catch { return null; } }; const handleRowKeyDown = (event, item) => { if (mode !== "page") return; if (event.key === "ArrowDown") { event.preventDefault(); applyPageKeyboardAction({ kind: "move_next" }, item, event.currentTarget); return; } if (event.key === "ArrowUp") { event.preventDefault(); applyPageKeyboardAction({ kind: "move_previous" }, item, event.currentTarget); return; } if (event.key === "ArrowRight") { event.preventDefault(); applyPageKeyboardAction({ kind: "expand" }, item, event.currentTarget); return; } if (event.key === "ArrowLeft") { event.preventDefault(); applyPageKeyboardAction({ kind: "collapse" }, item, event.currentTarget); return; } if (event.key === "Enter") { event.preventDefault(); applyPageKeyboardAction({ kind: "open" }, item, event.currentTarget); return; } if (event.key === "F2") { event.preventDefault(); beginInlineRename("page", item.nodeId); return; } if ( event.key === "ContextMenu" || (event.shiftKey && event.key === "F10") ) { event.preventDefault(); applyPageKeyboardAction({ kind: "context_menu" }, item, event.currentTarget); } }; const handleNavigate = (nodeId) => { if (!nodeId) return; if (mode === "picker") { setLastAction(`已选择页面 ${nodeId}`); postToHost("tree.pick", { documentId: nodeId, target: { documentId: nodeId }, payload: { documentId: nodeId }, }); return; } setLastAction(`准备打开页面 ${nodeId}`); postToHost("tree.navigate", { documentId: nodeId, target: { documentId: nodeId }, payload: { documentId: nodeId }, }); }; const handleCreate = async (parentId) => { const title = "无标题"; try { const result = await sendCommand({ action: "create", workspaceId, parentId, title, accessScope: "private", content: [], }); const documentId = typeof result.documentId === "string" && result.documentId.trim() ? result.documentId.trim() : ""; setStatus("创建页面成功"); setLastAction(parentId ? "已创建无标题子页面" : "已创建无标题页面"); postToHost("tree.node.created", { documentId, target: { documentId }, payload: { documentId }, }); if (sourceKind === "convex_workspace" && applyCreatedDocumentLocally(result, parentId, title)) { if (mode === "filetree") { beginInlineRename("filetree", `doc:${documentId}`); } else { beginInlineRename("page", documentId); } return; } if (documentId) { postToHost("tree.navigate", { documentId, target: { documentId }, payload: { documentId }, }); } scheduleRefresh({ renameRowId: localCreatedRowIdFromCommandResult(result, "markdown"), }); } catch (error) { const message = error instanceof Error ? error.message : "创建页面失败"; setStatus(message, "error"); setLastAction("创建页面失败", "error"); await window.mnote.alert(message); } }; const focusInlineRenameInput = (id) => { window.requestAnimationFrame(() => { const input = appElement.querySelector( `.tree-rename-input[data-rename-id="${CSS.escape(id)}"]`, ); if (!(input instanceof HTMLInputElement)) return; input.focus(); const dotIndex = input.value.lastIndexOf("."); const end = dotIndex > 0 ? dotIndex : input.value.length; input.setSelectionRange(0, end); }); }; const beginInlineRename = (renameMode, id) => { inlineRenameState = { mode: renameMode, id, committing: false }; renderTree(); focusInlineRenameInput(id); }; const cancelInlineRename = () => { inlineRenameState = { mode: null, id: null, committing: false }; renderTree(); }; const commitInlineRename = async (title) => { if (!inlineRenameState.mode || !inlineRenameState.id || inlineRenameState.committing) return; const trimmed = normalizeText(title); if (!trimmed) { cancelInlineRename(); return; } inlineRenameState.committing = true; const renameMode = inlineRenameState.mode; const id = inlineRenameState.id; const item = renameMode === "filetree" ? fileTreeRowById.get(id) : itemById.get(id); const documentId = renameMode === "filetree" ? getFileTreeRowDocumentId(item) : id; if (!documentId) { inlineRenameState = { mode: null, id: null, committing: false }; setLastAction("当前资源暂不支持重命名", "error"); renderTree(); return; } try { const result = await sendCommand({ action: "rename", workspaceId, documentId, title: trimmed, }); const resultDocumentId = typeof result.documentId === "string" && result.documentId.trim() ? result.documentId.trim() : id; setStatus("重命名成功"); setLastAction(`已重命名为 ${trimmed}`); postToHost("tree.node.renamed", { documentId: resultDocumentId, target: { documentId: resultDocumentId }, payload: { documentId: resultDocumentId }, }); inlineRenameState = { mode: null, id: null, committing: false }; scheduleRefresh(); } catch (error) { const message = error instanceof Error ? error.message : "重命名失败"; setStatus(message, "error"); setLastAction("重命名失败", "error"); inlineRenameState = { mode: null, id: null, committing: false }; renderTree(); await window.mnote.alert(message); } }; const attachInlineRenameInput = (input, originalTitle) => { input.addEventListener("click", (event) => event.stopPropagation()); input.addEventListener("dblclick", (event) => event.stopPropagation()); input.addEventListener("keydown", (event) => { event.stopPropagation(); if (event.key === "Enter") { event.preventDefault(); void commitInlineRename(input.value); } else if (event.key === "Escape") { event.preventDefault(); cancelInlineRename(); } }); input.addEventListener("blur", () => { if (!inlineRenameState.mode || inlineRenameState.committing) return; if (input.value === originalTitle) { cancelInlineRename(); return; } void commitInlineRename(input.value); }); }; const handleMove = async (nodeId, delta) => { const item = itemById.get(nodeId); if (!item) return; const siblings = getSiblings(item.parentNodeId); const currentIndex = siblings.findIndex(entry) => entry.nodeId === nodeId); if (currentIndex === -1) return; const nextIndex = currentIndex + delta; if (nextIndex < 0 || nextIndex >= siblings.length) return; try { const result = await sendCommand({ action: "move", workspaceId, documentId: nodeId, parentId: item.parentNodeId, sortOrder: nextIndex, }); const documentId = typeof result.documentId === "string" && result.documentId.trim() ? result.documentId.trim() : nodeId; setStatus("移动页面成功"); setLastAction(delta < 0 ? "页面已上移" : "页面已下移"); postToHost("tree.subtree.moved", { documentId, target: { documentId }, payload: { documentId }, }); scheduleRefresh(); } catch (error) { const message = error instanceof Error ? error.message : "移动页面失败"; setStatus(message, "error"); setLastAction("移动页面失败", "error"); await window.mnote.alert(message); } }; 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"); await window.mnote.alert(message); } }; const bindPageRowEvents = (row, item) => { if (!(row instanceof HTMLElement) || !item) return; row.dataset.active = String(item.nodeId === currentActiveDocumentId); row.dataset.focused = String(item.nodeId === focusedNodeId); row.dataset.nodeId = item.nodeId; row.dataset.shellMode = "page"; 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", item.childCount > 0 ? String(expanded.has(item.nodeId)) : "false"); row.draggable = true; row.dataset.draggable = "true"; row.addEventListener("focus", () => { if (focusedNodeId !== item.nodeId) { applyPageKeyboardAction({ kind: "focus", nodeId: item.nodeId }, item, row); } }); row.addEventListener("keydown", (event) => handleRowKeyDown(event, item)); row.addEventListener("contextmenu", (event) => { event.preventDefault(); openContextMenu(item.nodeId, event.clientX, event.clientY); }); row.addEventListener("dragstart", (event) => { 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) => { 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) => { const relatedTarget = event.relatedTarget instanceof Node ? event.relatedTarget : null; if (relatedTarget && row.contains(relatedTarget)) { return; } if (activePageDropNodeId === item.nodeId) { clearPageDropFeedback(); } }); row.addEventListener("drop", (event) => { 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", () => { draggingPageNodeId = ""; clearPageDropFeedback(); }); row.querySelectorAll("[data-rust-action]").forEach((element) => { if (!(element instanceof HTMLElement)) return; element.addEventListener("click", (event) => { event.stopPropagation(); const action = normalizeText(element.dataset.rustAction); if (action === "toggle") { event.preventDefault(); applyPageKeyboardAction({ kind: "toggle", nodeId: item.nodeId }, item, element); } else if (action === "open") { applyPageKeyboardAction({ kind: "open", nodeId: item.nodeId }, item, element); } else if (action === "create") { void handleCreate(item.nodeId); } else if (action === "rename") { beginInlineRename("page", item.nodeId); } else if (action === "menu") { applyPageKeyboardAction({ kind: "context_menu", nodeId: item.nodeId }, item, element); } }); }); }; const patchPageTreeActiveDom = () => patchTreeShellPageActiveDom({ mode, appElement, normalizeText, focusedNodeId, currentActiveDocumentId, activePageDropNodeId, }); const patchPageTreeExpansionDom = (nodeId) => patchTreeShellPageExpansionDom({ mode, appElement, normalizeText, itemById, expanded, getSiblings, renderNode, patchPageTreeActiveDom, }, nodeId); const hydrateInitialPageTree = () => hydrateTreeShellInitialPageTree({ mode, appElement, normalizeText, itemById, bindPageRowEvents, focusedNodeId, focusRowElement, }); const bindPickerRootEvents = (row) => { if (!(row instanceof HTMLElement)) return; row.dataset.focused = String(resolvePickerRootFocused()); row.addEventListener("click", () => { applyPickerFocusByItemKey("__root__", { focusDom: true }); postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" })); }); }; const bindPickerRowEvents = (row, item) => { if (!(row instanceof HTMLElement) || !item) return; row.dataset.nodeId = item.nodeId; row.dataset.shellMode = "picker"; row.dataset.focused = String( currentActivePickerItemKey === item.nodeId || (!currentActivePickerItemKey && currentActiveDocumentId === item.nodeId), ); row.addEventListener("click", () => { applyPickerFocusByItemKey(item.nodeId, { focusDom: true }); postPickerPickResultToHost(applyPickerStateAction({ kind: "pick" })); }); }; const focusPickerRowElement = (pickerItemKey) => focusTreeShellPickerRowElement({ appElement, normalizeText, }, pickerItemKey); const patchPickerActiveDom = () => patchTreeShellPickerActiveDom({ mode, appElement, normalizeText, currentActivePickerItemKey, currentActiveDocumentId, }); const hydrateInitialPickerTree = () => hydrateTreeShellInitialPickerTree({ mode, appElement, normalizeText, itemById, bindPickerRootEvents, bindPickerRowEvents, }); const hydrateInitialRenderer = () => { if (mode === "page") { const usedRustInitialPageRenderer = hydrateInitialPageTree(); return usedRustInitialPageRenderer; } if (mode === "filetree") { return hydrateInitialFileTree(); } if (mode === "picker") { return hydrateInitialPickerTree(); } return false; }; const treeShellRenderer = createTreeShellRenderer({ FILETREE_DRAG_MIME, PAGE_DRAG_MIME, allowRootPick, appElement, applyPageKeyboardAction, applyPickerFocusByItemKey, applyPickerStateAction, attachInlineRenameInput, beginInlineRename, canAcceptPageDrop, canExpandFileTreeRow, clearFileTreeDropFeedback, clearFileTreeSelection, clearPageDropFeedback, executeLocalFileTreeExternalDrop, executeLocalFileTreeInternalDrop, expanded, getActiveFileTreeDropPosition, getActiveFileTreeDropRowId, getActiveFileTreeRootDrop, getActivePageDropNodeId: () => activePageDropNodeId, getCurrentActiveDocumentId: () => currentActiveDocumentId, getElementCenter, getFileTreeDropTargetFromEvent, getFileTreeRowById: () => fileTreeRowById, focusRowElement, getFocusedNodeId: () => focusedNodeId, getInlineRenameState: () => inlineRenameState, getItemById: () => itemById, getRoots: () => roots, getSelectedFileTreeRowIds: () => selectedFileTreeRowIds, getSiblings, handleCreate, handleFileTreeKeyDown, handleMove, handleNavigate, handlePageDropMove, handleRowKeyDown, isBusy: () => busy, mode, normalizeText, normalizeFileTreeSelectionForVisibleRows, openContextMenu, openFileTreeContextMenu, patchFileTreeCutDecoration, postPickerPickResultToHost, postToHost, pushVisibleFileTreeRowId: (rowId) => { visibleFileTreeRowIds.push(rowId); }, readFileTreeInternalDropPayload, readPageDragNodeId, resolveFileTreeDraggedRowIds, resolvePageDropTargetNodeId, resolvePickerRootFocused, runFileTreePreflight, selectFileTreeContextRow, selectFileTreeRow, setFileTreeDropFeedback, setDraggingFileTreeRowIds: (rowIds) => { draggingFileTreeRowIds = rowIds; }, setDraggingPageNodeId: (nodeId) => { draggingPageNodeId = nodeId; }, setLastAction, setPageDropFeedback, setVisibleFileTreeRowIds: (rowIds) => { visibleFileTreeRowIds = rowIds; }, sourceKind, toggleExpand, validateFileTreeInternalDrop, workspaceId, }); renderNode = treeShellRenderer.renderNode; renderTree = treeShellRenderer.renderTree; syncFileTreeSelectionDom = treeShellRenderer.syncFileTreeSelectionDom; openHydratedFileTreeItem = treeShellRenderer.openHydratedFileTreeItem; hydrateInitialFileTree = treeShellRenderer.hydrateInitialFileTree; 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(); if (mode === "picker" && usedRustInitialRenderer) { patchPickerActiveDom(); return; } renderTree(); if (mode === "page" && focusedNodeId) { focusRowElement(focusedNodeId); } }); createRootButton.addEventListener("click", () => { if (mode === "picker") return; void handleCreate(null); }); const emitReady = () => { postToHost("tree.ready", { workspaceId, payload: { workspaceId }, }); }; const usedRustInitialRenderer = hydrateInitialRenderer(); if (!usedRustInitialRenderer) { renderTree(); } if (mode === "page" && focusedNodeId) { postPageFocusChange(focusedNodeId); } if (mode === "filetree") { emitFileTreeSelectionChange(); } if (mode === "filetree" && initialRenameRowId) { const url = new URL(window.location.href); url.searchParams.delete("renameRowId"); window.history.replaceState(null, "", url.toString()); window.setTimeout(() => { if (fileTreeRowById.has(initialRenameRowId)) { beginInlineRename("filetree", initialRenameRowId); } }, 120); } setStatus( mode === "picker" ? "Tree picker 已就绪,可以展开目录并选择目标页面。" : mode === "filetree" ? "File tree shell 已就绪,可以打开页面与附件。" : "Tree shell 已就绪,可以进行展开、创建、重命名和排序操作。", ); setLastAction("已向宿主发送 ready 消息。"); emitReady(); window.setTimeout(emitReady, 300); window.setTimeout(emitReady, 1200); } startTreeShellRuntime();