From c5e0396ebab930faf9faf1bbb0961195b7ac1a5a Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Tue, 26 May 2026 02:35:08 +0800 Subject: [PATCH] refactor: split sidebar page tree runtime --- ...ime-module-maintainability-checklist-v1.md | 2 +- .../browser/sidebar-page-tree-runtime.js | 253 ++++++++++++++++++ .../mnote-web/browser/sidebar-tree-runtime.js | 228 +++------------- rust/crates/mnote-web/src/routes/mod.rs | 5 + rust/crates/mnote-web/src/routes/web_shell.rs | 14 + rust/crates/mnote-web/src/ssr/pages/layout.rs | 19 +- 6 files changed, 321 insertions(+), 200 deletions(-) create mode 100644 rust/crates/mnote-web/browser/sidebar-page-tree-runtime.js diff --git a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md index 780c2b90..fc210625 100644 --- a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md +++ b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md @@ -164,7 +164,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::web_shell 拆分项: - [x] C1. `sidebar-workspace-runtime.js`:workspace source switch、sidebar tabs、collapse/resize 状态。 -- [ ] C2. `sidebar-page-tree-runtime.js`:page tree row selection、rename title patch、breadcrumb/title sync。 +- [x] C2. `sidebar-page-tree-runtime.js`:page tree row selection、rename title patch、breadcrumb/title sync。 - [x] C3. `sidebar-filetree-open-runtime.js`:local markdown / resource open target、active resource tab dispatch。 - [x] C4. `sidebar-filetree-command-runtime.js`:create/rename/delete/copy/move/trash/restore/purge command payload。 - [x] C5. `sidebar-filetree-upload-runtime.js`:local upload target plan、drop/paste preflight、readonly guard。 diff --git a/rust/crates/mnote-web/browser/sidebar-page-tree-runtime.js b/rust/crates/mnote-web/browser/sidebar-page-tree-runtime.js new file mode 100644 index 00000000..cffc9163 --- /dev/null +++ b/rust/crates/mnote-web/browser/sidebar-page-tree-runtime.js @@ -0,0 +1,253 @@ +export const createSidebarPageTreeRuntime = (dependencies = {}) => { + const { + activeSidebarTreeMode, + commandDocumentId, + copyWorkspaceSourceParams, + cssEscape, + currentDocumentId, + currentSourceKind, + dispatchSidebarEvent, + dispatchTreeCommand, + normalizeSidebarTreeMode, + openPageTreeContextMenu, + persistLocalFolderSelfChangeSuppression, + persistSidebarTreeMode, + refreshLocalFolderSidebarSnapshot, + resolveWorkspaceId, + rowCenter, + selectSidebarFileTreeDocument, + toggleChildren, + updateTitleEverywhere, + } = dependencies; + + const PAGE_DRAG_MIME = 'application/x-mnote-page-tree-node'; + var mnoteNavigationInFlight = ''; + var draggingPageNodeId = ''; + var activePageDropRow = null; + + function navigateToDocument(nodeId, workspaceId, options) { + if (!nodeId) return; + var treeView = normalizeSidebarTreeMode(options && options.treeView ? options.treeView : activeSidebarTreeMode()); + persistSidebarTreeMode(treeView); + if (currentDocumentId() === nodeId && window.location.pathname.indexOf('/documents/') === 0) { + if (typeof window.__mnoteDocumentPaneRuntime?.activatePageTab === 'function') { + window.__mnoteDocumentPaneRuntime.activatePageTab(); + } + document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach(function(row) { + if (row instanceof HTMLElement) { + row.setAttribute('data-active', 'false'); + row.setAttribute('data-selected', 'false'); + } + }); + document.querySelectorAll('.tree-row[data-node-id="' + cssEscape(nodeId) + '"], .tree-row[data-document-id="' + cssEscape(nodeId) + '"], .tree-row[data-doc-id="' + cssEscape(nodeId) + '"]').forEach(function(row) { + if (row instanceof HTMLElement) { + if (row.getAttribute('data-shell-mode') === 'filetree') { + row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === 'doc:' + nodeId)); + } else { + row.setAttribute('data-active', 'true'); + } + } + }); + selectSidebarFileTreeDocument(nodeId, { scrollIntoView: true }); + return; + } + var targetUrl = new URL('/documents/' + encodeURIComponent(nodeId), window.location.origin); + if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId); + if (treeView === 'filetree') targetUrl.searchParams.set('treeView', 'filetree'); + copyWorkspaceSourceParams(targetUrl); + var url = targetUrl.pathname + targetUrl.search; + if (mnoteNavigationInFlight === url) return; + if (typeof window.__mnoteDocumentPaneRuntime?.openPrimaryDocument === 'function') { + mnoteNavigationInFlight = url; + document.documentElement.setAttribute('data-mnote-navigation-pending', 'true'); + document.documentElement.setAttribute('data-mnote-navigation-target', nodeId); + window.__mnoteDocumentPaneRuntime.openPrimaryDocument({ + documentId: nodeId, + workspaceId: workspaceId || '', + sourceKind: targetUrl.searchParams.get('sourceKind') || 'convex_workspace', + rootUri: targetUrl.searchParams.get('rootUri') || '', + url: targetUrl, + }).then(function(){ + if (mnoteNavigationInFlight === url) mnoteNavigationInFlight = ''; + document.documentElement.removeAttribute('data-mnote-navigation-pending'); + selectSidebarFileTreeDocument(nodeId, { scrollIntoView: true }); + }).catch(function(error){ + console.warn('mnote pane 内导航失败,将回退整页导航', error); + if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') { + window.__mnoteTreeLiveEventSource.close(); + } + window.location.assign(url); + }); + return; + } + mnoteNavigationInFlight = url; + document.documentElement.setAttribute('data-mnote-navigation-pending', 'true'); + document.documentElement.setAttribute('data-mnote-navigation-target', nodeId); + if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') { + window.__mnoteTreeLiveEventSource.close(); + } + window.location.assign(url); + } + + async function createPage(trigger, parentId) { + var workspaceId = resolveWorkspaceId(trigger); + var effectiveParentId = (parentId || trigger.getAttribute('data-parent-id') || '').trim(); + if (!workspaceId) return; + var result = await dispatchTreeCommand(trigger, { + action: 'create', + workspaceId: workspaceId, + parentId: effectiveParentId || null, + title: '新页面' + }); + var nextWorkspaceId = result.workspaceId || workspaceId; + var nextDocumentId = commandDocumentId(result, ''); + if (nextDocumentId) { + persistLocalFolderSelfChangeSuppression(nextDocumentId, Date.now() + 5000); + } + if (nextDocumentId && currentSourceKind() === 'local_folder') { + await refreshLocalFolderSidebarSnapshot(); + selectSidebarFileTreeDocument(nextDocumentId, { scrollIntoView: true }); + document.documentElement.setAttribute('data-mnote-create-page-selected-document-id', nextDocumentId); + } + navigateToDocument(nextDocumentId, nextWorkspaceId, { treeView: activeSidebarTreeMode() }); + } + + function startPageDrag(pageRow, event) { + if (!(pageRow instanceof HTMLElement)) return false; + draggingPageNodeId = pageRow.getAttribute('data-node-id') || ''; + if (event && event.dataTransfer) { + event.dataTransfer.effectAllowed = 'move'; + event.dataTransfer.setData(PAGE_DRAG_MIME, draggingPageNodeId); + event.dataTransfer.setData('text/plain', draggingPageNodeId); + } + return true; + } + + function readPageDragNodeId(event) { + var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : ''; + return (fromTransfer || draggingPageNodeId || '').trim(); + } + + function clearPageDropFeedback() { + if (activePageDropRow instanceof HTMLElement) { + activePageDropRow.setAttribute('data-drop-feedback', 'false'); + } + activePageDropRow = null; + } + + function setActivePageDropRow(row) { + activePageDropRow = row instanceof HTMLElement ? row : null; + } + + function clearPageDragState() { + draggingPageNodeId = ''; + clearPageDropFeedback(); + } + + function handlePageTreeClick(event, helpers = {}) { + var closestAction = helpers.closestAction; + if (typeof closestAction !== 'function') return false; + var tree = document.getElementById('sidebar-tree-root'); + if (!tree || !tree.contains(event.target)) return false; + var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]'); + var btn = closestAction(event.target, '[data-rust-action]'); + if (!btn && !pageRow) return false; + var nodeId = (btn && btn.getAttribute('data-node-id')) || (pageRow && pageRow.getAttribute('data-node-id')) || ''; + var action = btn ? btn.getAttribute('data-rust-action') : 'open'; + + if (action === 'toggle') { + var row = btn.closest('.tree-row'); + if (!row) return true; + toggleChildren(row, btn); + event.preventDefault(); + } else if (action === 'open') { + var openTrigger = btn || pageRow; + if (openTrigger && openTrigger.getAttribute('data-page-openable') === 'false') { + event.preventDefault(); + return true; + } + var workspaceId = resolveWorkspaceId(openTrigger); + navigateToDocument(nodeId, workspaceId, { treeView: 'page' }); + event.preventDefault(); + } else if (action === 'create') { + event.preventDefault(); + void createPage(btn, nodeId); + } else if (action === 'rename') { + event.preventDefault(); + var title = window.prompt('重命名页面'); + if (title && title.trim()) { + void dispatchTreeCommand(btn, { + action: 'rename', + workspaceId: resolveWorkspaceId(btn), + documentId: nodeId, + title: title.trim() + }).then(function(){ + updateTitleEverywhere(nodeId, title.trim()); + }); + } + } else if (action === 'menu') { + event.preventDefault(); + var menuPoint = rowCenter(btn); + dispatchSidebarEvent('tree.page.context-menu', { documentId: nodeId, target: { documentId: nodeId } }); + openPageTreeContextMenu(btn.closest('.tree-row'), menuPoint.x, menuPoint.y, btn); + } + return true; + } + + function handlePageTreeContextMenu(event, helpers = {}) { + var closestAction = helpers.closestAction; + if (typeof closestAction !== 'function') return false; + var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]'); + if (!pageRow) return false; + event.preventDefault(); + openPageTreeContextMenu(pageRow, event.clientX, event.clientY, pageRow); + return true; + } + + function canDropPage(sourceNodeId, targetRow) { + if (!sourceNodeId || !(targetRow instanceof HTMLElement)) return false; + var targetNodeId = targetRow.getAttribute('data-node-id') || ''; + if (!targetNodeId || targetNodeId === sourceNodeId) return false; + var sourceNode = document.querySelector('#sidebar-tree-root .tree-node[data-node-id="' + cssEscape(sourceNodeId) + '"]'); + return !(sourceNode instanceof HTMLElement && sourceNode.contains(targetRow)); + } + + function pageDropPosition(event, row) { + var rect = row.getBoundingClientRect(); + var ratio = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0.5; + if (ratio < 0.25) return 'before'; + if (ratio > 0.75) return 'after'; + return 'inside'; + } + + function resolvePageMoveTarget(targetRow, position) { + var targetNodeId = targetRow.getAttribute('data-node-id') || ''; + var parentId = targetRow.getAttribute('data-parent-id') || null; + if (position === 'inside') { + var children = targetRow.parentElement ? targetRow.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : []; + return { parentId: targetNodeId, sortOrder: children.length }; + } + var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(row) { + return (row.getAttribute('data-parent-id') || '') === (parentId || ''); + }); + var index = siblings.indexOf(targetRow); + return { parentId: parentId, sortOrder: Math.max(0, index + (position === 'after' ? 1 : 0)) }; + } + + + + return { + navigateToDocument, + createPage, + startPageDrag, + readPageDragNodeId, + clearPageDropFeedback, + setActivePageDropRow, + clearPageDragState, + handlePageTreeClick, + handlePageTreeContextMenu, + canDropPage, + pageDropPosition, + resolvePageMoveTarget, + }; +}; diff --git a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js index 6a213b4f..f652df03 100644 --- a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js @@ -1,4 +1,5 @@ import { createSidebarWorkspaceRuntime } from './sidebar-workspace-runtime.js'; +import { createSidebarPageTreeRuntime } from './sidebar-page-tree-runtime.js'; import { createSidebarTreeLiveApplyRuntime } from './sidebar-tree-live-apply-runtime.js'; import { createSidebarFileTreeOpenRuntime } from './sidebar-filetree-open-runtime.js'; import { createSidebarAttachmentOpenRuntime } from './sidebar-attachment-open-runtime.js'; @@ -10,15 +11,12 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru window.__mnoteSidebarTreeRuntimeStarted = true; var PAGE_AI_SESSION_STORAGE_VERSION = 3; - var PAGE_DRAG_MIME = 'application/x-mnote-page-tree-node'; var FILETREE_DRAG_MIME = 'application/x-mnote-filetree-row-ids'; var MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode'; var MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX = 'mnote.localFolder.recentRoots:'; var MNOTE_LAST_CLOUD_WORKSPACE_KEY = 'mnote.workspace.lastCloudWorkspaceId'; var MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY = 'mnote.global.showHeadingNumbers'; var mnoteNavigationInFlight = ''; - var draggingPageNodeId = ''; - var activePageDropRow = null; var draggingFileTreeRowIds = []; var activeFileTreeDropRow = null; var sidebarFileTreeClipboard = null; @@ -688,92 +686,17 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru } } - function navigateToDocument(nodeId, workspaceId, options) { - if (!nodeId) return; - var treeView = normalizeSidebarTreeMode(options && options.treeView ? options.treeView : activeSidebarTreeMode()); - persistSidebarTreeMode(treeView); - if (currentDocumentId() === nodeId && window.location.pathname.indexOf('/documents/') === 0) { - if (typeof window.__mnoteDocumentPaneRuntime?.activatePageTab === 'function') { - window.__mnoteDocumentPaneRuntime.activatePageTab(); - } - document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach(function(row) { - if (row instanceof HTMLElement) { - row.setAttribute('data-active', 'false'); - row.setAttribute('data-selected', 'false'); - } - }); - document.querySelectorAll('.tree-row[data-node-id="' + cssEscape(nodeId) + '"], .tree-row[data-document-id="' + cssEscape(nodeId) + '"], .tree-row[data-doc-id="' + cssEscape(nodeId) + '"]').forEach(function(row) { - if (row instanceof HTMLElement) { - if (row.getAttribute('data-shell-mode') === 'filetree') { - row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === 'doc:' + nodeId)); - } else { - row.setAttribute('data-active', 'true'); - } - } - }); - selectSidebarFileTreeDocument(nodeId, { scrollIntoView: true }); - return; - } - var targetUrl = new URL('/documents/' + encodeURIComponent(nodeId), window.location.origin); - if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId); - if (treeView === 'filetree') targetUrl.searchParams.set('treeView', 'filetree'); - copyWorkspaceSourceParams(targetUrl); - var url = targetUrl.pathname + targetUrl.search; - if (mnoteNavigationInFlight === url) return; - if (typeof window.__mnoteDocumentPaneRuntime?.openPrimaryDocument === 'function') { - mnoteNavigationInFlight = url; - document.documentElement.setAttribute('data-mnote-navigation-pending', 'true'); - document.documentElement.setAttribute('data-mnote-navigation-target', nodeId); - window.__mnoteDocumentPaneRuntime.openPrimaryDocument({ - documentId: nodeId, - workspaceId: workspaceId || '', - sourceKind: targetUrl.searchParams.get('sourceKind') || 'convex_workspace', - rootUri: targetUrl.searchParams.get('rootUri') || '', - url: targetUrl, - }).then(function(){ - if (mnoteNavigationInFlight === url) mnoteNavigationInFlight = ''; - document.documentElement.removeAttribute('data-mnote-navigation-pending'); - selectSidebarFileTreeDocument(nodeId, { scrollIntoView: true }); - }).catch(function(error){ - console.warn('mnote pane 内导航失败,将回退整页导航', error); - if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') { - window.__mnoteTreeLiveEventSource.close(); - } - window.location.assign(url); - }); - return; - } - mnoteNavigationInFlight = url; - document.documentElement.setAttribute('data-mnote-navigation-pending', 'true'); - document.documentElement.setAttribute('data-mnote-navigation-target', nodeId); - if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') { - window.__mnoteTreeLiveEventSource.close(); - } - window.location.assign(url); - } - - async function createPage(trigger, parentId) { - var workspaceId = resolveWorkspaceId(trigger); - var effectiveParentId = (parentId || trigger.getAttribute('data-parent-id') || '').trim(); - if (!workspaceId) return; - var result = await dispatchTreeCommand(trigger, { - action: 'create', - workspaceId: workspaceId, - parentId: effectiveParentId || null, - title: '新页面' - }); - var nextWorkspaceId = result.workspaceId || workspaceId; - var nextDocumentId = commandDocumentId(result, ''); - if (nextDocumentId) { - persistLocalFolderSelfChangeSuppression(nextDocumentId, Date.now() + 5000); - } - if (nextDocumentId && currentSourceKind() === 'local_folder') { - await refreshLocalFolderSidebarSnapshot(); - selectSidebarFileTreeDocument(nextDocumentId, { scrollIntoView: true }); - document.documentElement.setAttribute('data-mnote-create-page-selected-document-id', nextDocumentId); - } - navigateToDocument(nextDocumentId, nextWorkspaceId, { treeView: activeSidebarTreeMode() }); - } + var sidebarPageTree = null; + const navigateToDocument = (...args) => sidebarPageTree.navigateToDocument(...args); + const createPage = (...args) => sidebarPageTree.createPage(...args); + const startPageDrag = (...args) => sidebarPageTree.startPageDrag(...args); + const readPageDragNodeId = (...args) => sidebarPageTree.readPageDragNodeId(...args); + const clearPageDropFeedback = (...args) => sidebarPageTree.clearPageDropFeedback(...args); + const setActivePageDropRow = (...args) => sidebarPageTree.setActivePageDropRow(...args); + const clearPageDragState = (...args) => sidebarPageTree.clearPageDragState(...args); + const canDropPage = (...args) => sidebarPageTree.canDropPage(...args); + const pageDropPosition = (...args) => sidebarPageTree.pageDropPosition(...args); + const resolvePageMoveTarget = (...args) => sidebarPageTree.resolvePageMoveTarget(...args); async function createFileTreeFolder(trigger, parentId) { if (currentSourceKind() !== 'local_folder') return false; @@ -828,6 +751,26 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru syncSidebarFileTreeSelection: (...args) => syncSidebarFileTreeSelection(...args), }); const updateTitleEverywhere = (...args) => sidebarTreeLiveApply.updateTitleEverywhere(...args); + sidebarPageTree = createSidebarPageTreeRuntime({ + activeSidebarTreeMode, + commandDocumentId, + copyWorkspaceSourceParams, + cssEscape, + currentDocumentId, + currentSourceKind, + dispatchTreeCommand, + normalizeSidebarTreeMode, + persistLocalFolderSelfChangeSuppression, + persistSidebarTreeMode, + refreshLocalFolderSidebarSnapshot: (...args) => refreshLocalFolderSidebarSnapshot(...args), + resolveWorkspaceId, + rowCenter: (...args) => rowCenter(...args), + selectSidebarFileTreeDocument: (...args) => selectSidebarFileTreeDocument(...args), + toggleChildren: (...args) => toggleChildren(...args), + dispatchSidebarEvent: (...args) => dispatchSidebarEvent(...args), + openPageTreeContextMenu: (...args) => openPageTreeContextMenu(...args), + updateTitleEverywhere: (...args) => updateTitleEverywhere(...args), + }); const fileTreePageTitle = (...args) => sidebarTreeLiveApply.fileTreePageTitle(...args); const isFileTreePageRow = (...args) => sidebarTreeLiveApply.isFileTreePageRow(...args); const normalizeFileTreePageRenameTitle = (...args) => sidebarTreeLiveApply.normalizeFileTreePageRenameTitle(...args); @@ -5754,50 +5697,7 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru return; } - var tree = document.getElementById('sidebar-tree-root'); - if (!tree || !tree.contains(e.target)) return; - var pageRow = closestAction(e.target, '.tree-row[data-shell-mode="page"]'); - var btn = closestAction(e.target, '[data-rust-action]'); - if (!btn && !pageRow) return; - var nodeId = (btn && btn.getAttribute('data-node-id')) || (pageRow && pageRow.getAttribute('data-node-id')) || ''; - var action = btn ? btn.getAttribute('data-rust-action') : 'open'; - - if (action === 'toggle') { - var row = btn.closest('.tree-row'); - if (!row) return; - toggleChildren(row, btn); - e.preventDefault(); - } else if (action === 'open') { - var openTrigger = btn || pageRow; - if (openTrigger && openTrigger.getAttribute('data-page-openable') === 'false') { - e.preventDefault(); - return; - } - var workspaceId = resolveWorkspaceId(openTrigger); - navigateToDocument(nodeId, workspaceId, { treeView: 'page' }); - e.preventDefault(); - } else if (action === 'create') { - e.preventDefault(); - void createPage(btn, nodeId); - } else if (action === 'rename') { - e.preventDefault(); - var title = window.prompt('重命名页面'); - if (title && title.trim()) { - void dispatchTreeCommand(btn, { - action: 'rename', - workspaceId: resolveWorkspaceId(btn), - documentId: nodeId, - title: title.trim() - }).then(function(){ - updateTitleEverywhere(nodeId, title.trim()); - }); - } - } else if (action === 'menu') { - e.preventDefault(); - var menuPoint = rowCenter(btn); - dispatchSidebarEvent('tree.page.context-menu', { documentId: nodeId, target: { documentId: nodeId } }); - openPageTreeContextMenu(btn.closest('.tree-row'), menuPoint.x, menuPoint.y, btn); - } + if (sidebarPageTree.handlePageTreeClick(e, { closestAction: closestAction })) return; }); document.addEventListener('contextmenu', function(event) { @@ -5819,11 +5719,7 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru openFileTreeContextMenu(fileRow, event.clientX, event.clientY, fileRow); return; } - var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]'); - if (pageRow) { - event.preventDefault(); - openPageTreeContextMenu(pageRow, event.clientX, event.clientY, pageRow); - } + if (sidebarPageTree.handlePageTreeContextMenu(event, { closestAction: closestAction })) return; }); document.addEventListener('keydown', function(event) { @@ -6045,57 +5941,10 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru } installMnoteDevHotReload(); - function readPageDragNodeId(event) { - var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : ''; - return (fromTransfer || draggingPageNodeId || '').trim(); - } - - function clearPageDropFeedback() { - if (activePageDropRow instanceof HTMLElement) { - activePageDropRow.setAttribute('data-drop-feedback', 'false'); - } - activePageDropRow = null; - } - - function canDropPage(sourceNodeId, targetRow) { - if (!sourceNodeId || !(targetRow instanceof HTMLElement)) return false; - var targetNodeId = targetRow.getAttribute('data-node-id') || ''; - if (!targetNodeId || targetNodeId === sourceNodeId) return false; - var sourceNode = document.querySelector('#sidebar-tree-root .tree-node[data-node-id="' + cssEscape(sourceNodeId) + '"]'); - return !(sourceNode instanceof HTMLElement && sourceNode.contains(targetRow)); - } - - function pageDropPosition(event, row) { - var rect = row.getBoundingClientRect(); - var ratio = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0.5; - if (ratio < 0.25) return 'before'; - if (ratio > 0.75) return 'after'; - return 'inside'; - } - - function resolvePageMoveTarget(targetRow, position) { - var targetNodeId = targetRow.getAttribute('data-node-id') || ''; - var parentId = targetRow.getAttribute('data-parent-id') || null; - if (position === 'inside') { - var children = targetRow.parentElement ? targetRow.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : []; - return { parentId: targetNodeId, sortOrder: children.length }; - } - var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(row) { - return (row.getAttribute('data-parent-id') || '') === (parentId || ''); - }); - var index = siblings.indexOf(targetRow); - return { parentId: parentId, sortOrder: Math.max(0, index + (position === 'after' ? 1 : 0)) }; - } - document.addEventListener('dragstart', function(event) { var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"][draggable="true"]'); if (pageRow) { - draggingPageNodeId = pageRow.getAttribute('data-node-id') || ''; - if (event.dataTransfer) { - event.dataTransfer.effectAllowed = 'move'; - event.dataTransfer.setData(PAGE_DRAG_MIME, draggingPageNodeId); - event.dataTransfer.setData('text/plain', draggingPageNodeId); - } + startPageDrag(pageRow, event); return; } var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"][draggable="true"]'); @@ -6134,7 +5983,7 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru clearPageDropFeedback(); pageRow.setAttribute('data-drop-feedback', 'true'); pageRow.setAttribute('data-drop-position', pageDropPosition(event, pageRow)); - activePageDropRow = pageRow; + setActivePageDropRow(pageRow); if (event.dataTransfer) event.dataTransfer.dropEffect = 'move'; return; } @@ -6167,8 +6016,7 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru event.preventDefault(); var position = pageDropPosition(event, pageRow); var target = resolvePageMoveTarget(pageRow, position); - clearPageDropFeedback(); - draggingPageNodeId = ''; + clearPageDragState(); void dispatchTreeCommand(pageRow, { action: 'move', workspaceId: resolveWorkspaceId(pageRow), @@ -6228,7 +6076,7 @@ import { createSidebarFileTreeUploadRuntime } from './sidebar-filetree-upload-ru }); document.addEventListener('dragend', function() { - draggingPageNodeId = ''; + clearPageDragState(); var _dndRT_ = window.__mnoteFileTreeDndRuntime; if (_dndRT_ && typeof _dndRT_.resetFileTreeDragState === 'function') { _dndRT_.resetFileTreeDragState(); diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index 4bf646a5..8cff99d1 100644 --- a/rust/crates/mnote-web/src/routes/mod.rs +++ b/rust/crates/mnote-web/src/routes/mod.rs @@ -130,6 +130,10 @@ pub fn build_router(state: AppState) -> Router { "/api/mnote-browser-runtime/sidebar-workspace-runtime.js", get(web_shell::sidebar_workspace_runtime_asset), ) + .route( + "/api/mnote-browser-runtime/sidebar-page-tree-runtime.js", + get(web_shell::sidebar_page_tree_runtime_asset), + ) .route( "/api/mnote-browser-runtime/sidebar-tree-live-apply-runtime.js", get(web_shell::sidebar_tree_live_apply_runtime_asset), @@ -604,6 +608,7 @@ mod tests { "/api/mnote-browser-runtime/filetree-keyboard-runtime.js", "/api/mnote-browser-runtime/sidebar-shell-runtime.js", "/api/mnote-browser-runtime/sidebar-workspace-runtime.js", + "/api/mnote-browser-runtime/sidebar-page-tree-runtime.js", "/api/mnote-browser-runtime/sidebar-tree-live-apply-runtime.js", "/api/mnote-browser-runtime/sidebar-filetree-open-runtime.js", "/api/mnote-browser-runtime/sidebar-filetree-command-runtime.js", diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index c2679dbb..48edf165 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -756,6 +756,20 @@ pub async fn sidebar_workspace_runtime_asset() -> Response { .unwrap_or_else(|_| Response::new(Body::empty())) } +pub async fn sidebar_page_tree_runtime_asset() -> Response { + const JS: &str = include_str!("../../browser/sidebar-page-tree-runtime.js"); + Response::builder() + .status(StatusCode::OK) + .header( + header::CONTENT_TYPE, + "application/javascript; charset=utf-8", + ) + .header(header::CACHE_CONTROL, "no-store") + .header(HEADER_MNOTE_WEB_OWNER, "mnote-web") + .body(Body::from(JS)) + .unwrap_or_else(|_| Response::new(Body::empty())) +} + pub async fn sidebar_tree_live_apply_runtime_asset() -> Response { const JS: &str = include_str!("../../browser/sidebar-tree-live-apply-runtime.js"); Response::builder() diff --git a/rust/crates/mnote-web/src/ssr/pages/layout.rs b/rust/crates/mnote-web/src/ssr/pages/layout.rs index 68957ae4..d9658832 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -213,6 +213,8 @@ mod tests { include_str!("../../../browser/sidebar-shell-runtime.js"); const SIDEBAR_WORKSPACE_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-workspace-runtime.js"); + const SIDEBAR_PAGE_TREE_RUNTIME_JS: &str = + include_str!("../../../browser/sidebar-page-tree-runtime.js"); const FILETREE_CONTEXT_MENU_RUNTIME_JS: &str = include_str!("../../../browser/filetree-context-menu-runtime.js"); const FILETREE_DND_RUNTIME_JS: &str = include_str!("../../../browser/filetree-dnd-runtime.js"); @@ -247,7 +249,7 @@ mod tests { #[test] fn sidebar_tree_runtime_handles_navigation_drag_and_filetree_actions() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnoteNavigationInFlight")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-navigation-pending")); + assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS.contains("data-mnote-navigation-pending")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_LAST_CLOUD_WORKSPACE_KEY")); @@ -276,7 +278,7 @@ mod tests { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("installMindmapAssetFetchObserver")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("parseMindmapApiTarget")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-assets-local-applied")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("application/x-mnote-page-tree-node")); + assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS.contains("application/x-mnote-page-tree-node")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("dragstart")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("drop")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-drop-feedback")); @@ -926,15 +928,14 @@ mod tests { assert!(SIDEBAR_TREE_RUNTIME_JS .contains("var localFilePath = localFilePathFromAssetId(assetId);")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("attachmentMetaCache[assetId] = localMeta;")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains( - "var pageRow = closestAction(e.target, '.tree-row[data-shell-mode=\"page\"]');" + assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS.contains( + "var pageRow = closestAction(event.target, '.tree-row[data-shell-mode=\"page\"]');" )); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS .contains("var action = btn ? btn.getAttribute('data-rust-action') : 'open';")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("var openTrigger = btn || pageRow;")); - assert!( - SIDEBAR_TREE_RUNTIME_JS.contains("var workspaceId = resolveWorkspaceId(openTrigger);") - ); + assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS.contains("var openTrigger = btn || pageRow;")); + assert!(SIDEBAR_PAGE_TREE_RUNTIME_JS + .contains("var workspaceId = resolveWorkspaceId(openTrigger);")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS.contains( r#".tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title"# ));