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 f08e553e..452b7730 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 @@ -166,7 +166,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] C3. `sidebar-filetree-open-runtime.js`:local markdown / resource open target、active resource tab dispatch。 -- [ ] C4. `sidebar-filetree-command-runtime.js`:create/rename/delete/copy/move/trash/restore/purge command payload。 +- [x] C4. `sidebar-filetree-command-runtime.js`:create/rename/delete/copy/move/trash/restore/purge command payload。 - [ ] C5. `sidebar-filetree-upload-runtime.js`:local upload target plan、drop/paste preflight、readonly guard。 - [x] C6. `sidebar-attachment-open-runtime.js`:OnlyOffice/PDF/code/image open mode guard。 - [x] C7. `sidebar-tree-live-apply-runtime.js`:WS/SSE snapshot/delta/resync DOM apply。 diff --git a/rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js b/rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js new file mode 100644 index 00000000..f4b48718 --- /dev/null +++ b/rust/crates/mnote-web/browser/sidebar-filetree-command-runtime.js @@ -0,0 +1,1571 @@ +export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => { + const { + applyRemoveDocumentDelta, + createFileTreeFolder, + createPage, + cssEscape, + currentDocumentId, + currentSourceKind, + deleteSingleFileTreeAsset, + dispatchSidebarEvent, + dispatchTreeCommand, + ensureFileTreeWritableTarget, + fileTreeRuntimeDeps, + fileTreeRuntimeFunction, + fileTreeSelectionRuntimeDeps, + fileTreeSelectionRuntimeFunction, + isFileTreePageRow, + localFilePathFromAssetId, + normalizeFileTreePageRenameTitle, + openConvexAssetFromFileTree, + openEditorAttachmentDetail, + openEditorAttachmentDownload, + openEditorAttachmentEditTab, + openEditorAttachmentNewWindow, + refreshLocalFolderSidebarSnapshot, + resolveWorkspaceId, + runtimeState, + selectedSidebarFileTreeSelection, + updateTitleEverywhere, + validateFileTreeRename, + } = dependencies; + const sidebarFileTreeSelection = selectedSidebarFileTreeSelection; + + function rowTitle(row) { + var title = row ? row.querySelector(':scope > .tree-link > .tree-link-title') : null; + return title && title.textContent ? title.textContent.trim() : '无标题'; + } + + function rowCenter(row) { + var rect = row.getBoundingClientRect(); + return { x: rect.left + Math.min(rect.width - 12, 180), y: rect.top + Math.min(rect.height, 22) }; + } + + async function renameFileTreeAsset(assetId, title) { + var response = await fetch('/api/media/batch', { + method: 'POST', + credentials: 'include', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ action: 'rename', assetIds: [assetId], newName: title }) + }); + var payload = await response.json().catch(function(){ return null; }); + if (!response.ok) throw new Error((payload && payload.error) || '重命名附件失败'); + return payload; + } + + function beginFileTreeInlineRename(row) { + if (!(row instanceof HTMLElement)) return false; + if (row.querySelector('.tree-rename-input')) return true; + var rowKind = row.getAttribute('data-row-kind') || ''; + var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || ''; + var assetId = row.getAttribute('data-asset-id') || ''; + if (!documentId && !assetId) return false; + if (rowKind && ['document', 'doc', 'index', 'markdown', 'asset'].indexOf(rowKind) < 0 && !assetId) return false; + var link = row.querySelector(':scope > .tree-link'); + var title = rowTitle(row); + if (!(link instanceof HTMLElement)) return false; + link.hidden = true; + var input = document.createElement('input'); + input.type = 'text'; + input.className = 'tree-rename-input'; + input.setAttribute('data-rename-id', row.getAttribute('data-row-id') || ''); + input.value = title; + input.style.minWidth = '0'; + input.style.flex = '1 1 auto'; + input.style.height = '22px'; + input.style.border = '1px solid #93c5fd'; + input.style.borderRadius = '3px'; + input.style.padding = '0 4px'; + input.style.font = 'inherit'; + input.style.background = '#fff'; + input.style.color = '#1f2937'; + var validation = document.createElement('div'); + validation.setAttribute('data-testid', 'tree-rename-validation'); + validation.setAttribute('data-mnote-rename-validation', 'true'); + validation.style.fontSize = '12px'; + validation.style.color = '#dc2626'; + validation.style.padding = '2px 4px'; + validation.hidden = true; + var setValidation = function(message) { + var text = String(message || '').trim(); + validation.textContent = text; + validation.hidden = !text; + input.setAttribute('aria-invalid', text ? 'true' : 'false'); + }; + var closed = false; + var committing = false; + var close = function() { + if (closed) return; + closed = true; + if (input.parentElement) input.parentElement.removeChild(input); + if (validation.parentElement) validation.parentElement.removeChild(validation); + link.hidden = false; + }; + var commit = function() { + if (closed || committing) return; + var nextTitle = input.value.trim(); + if (!nextTitle || nextTitle === title) { + close(); + return; + } + var validationMessage = validateFileTreeRename(row, nextTitle); + if (validationMessage) { + setValidation(validationMessage); + return; + } + setValidation(''); + var commandTitle = isFileTreePageRow(row) ? normalizeFileTreePageRenameTitle(nextTitle) : nextTitle; + committing = true; + input.disabled = true; + var work = assetId + ? Promise.resolve().then(function(){ + document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-asset-id="' + cssEscape(assetId) + '"] .tree-link-title').forEach(function(titleNode) { + titleNode.textContent = commandTitle; + }); + close(); + return renameFileTreeAsset(assetId, commandTitle); + }).then(function(){ return null; }) + : dispatchTreeCommand(row, { + action: 'rename', + workspaceId: resolveWorkspaceId(row), + documentId: documentId, + title: commandTitle + }).then(function(){ updateTitleEverywhere(documentId, commandTitle); }); + void work.then(close).catch(function(error) { + committing = false; + input.disabled = false; + window.alert(error && error.message ? error.message : '重命名失败'); + }); + }; + input.addEventListener('click', function(event) { event.stopPropagation(); }); + input.addEventListener('dblclick', function(event) { event.stopPropagation(); }); + input.addEventListener('keydown', function(event) { + event.stopPropagation(); + if (event.key === 'Enter') { + event.preventDefault(); + commit(); + } else if (event.key === 'Escape') { + event.preventDefault(); + close(); + } + }); + input.addEventListener('blur', commit); + link.parentElement.insertBefore(input, link.nextSibling); + link.parentElement.insertBefore(validation, input.nextSibling); + window.requestAnimationFrame(function() { + input.focus(); + input.select(); + }); + return true; + } + + function closeTreeContextMenu() { + if (runtimeState.activeTreeContextMenu && runtimeState.activeTreeContextMenu.parentElement) { + runtimeState.activeTreeContextMenu.parentElement.removeChild(runtimeState.activeTreeContextMenu); + } + runtimeState.activeTreeContextMenu = null; + } + + function copyTreeContextValue(value, actionName) { + var text = String(value || ''); + var done = function() { + document.documentElement.setAttribute('data-mnote-tree-context-last-copy', actionName || 'copy'); + }; + if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { + return navigator.clipboard.writeText(text).then(done).catch(function(){}); + } + var textarea = document.createElement('textarea'); + textarea.value = text; + textarea.setAttribute('readonly', 'readonly'); + textarea.style.position = 'fixed'; + textarea.style.left = '-9999px'; + document.body.appendChild(textarea); + textarea.select(); + try { document.execCommand('copy'); } catch (_) {} + document.body.removeChild(textarea); + done(); + return Promise.resolve(); + } + + function triggerBrowserDownload(url) { + if (!url) return false; + var link = document.createElement('a'); + link.href = url; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + link.download = ''; + link.style.position = 'fixed'; + link.style.left = '-9999px'; + link.style.top = '0'; + document.body.appendChild(link); + try { + link.click(); + } catch (_) { + if (typeof window.open === 'function') window.open(url, '_blank', 'noopener,noreferrer'); + } + window.setTimeout(function() { + if (link.parentElement) link.parentElement.removeChild(link); + }, 1000); + return true; + } + + function recordFileTreeAction(action, detail) { + var normalized = String(action || '').trim() || 'unknown'; + var rowId = String(detail && detail.rowId || '').trim(); + var documentId = String(detail && detail.documentId || '').trim(); + var assetId = String(detail && detail.assetId || '').trim(); + document.documentElement.setAttribute('data-mnote-filetree-last-action', normalized); + if (rowId) document.documentElement.setAttribute('data-mnote-filetree-last-action-row-id', rowId); + if (documentId) document.documentElement.setAttribute('data-mnote-filetree-last-action-document-id', documentId); + if (assetId) document.documentElement.setAttribute('data-mnote-filetree-last-action-asset-id', assetId); + window.dispatchEvent(new CustomEvent('tree.filetree.action', { + detail: Object.assign({}, detail || {}, { action: normalized }) + })); + } + + function recordFileTreeActionStatus(status, detail) { + var normalized = String(status || '').trim() || 'unknown'; + document.documentElement.setAttribute('data-mnote-filetree-last-action-status', normalized); + window.dispatchEvent(new CustomEvent('tree.filetree.action.status', { + detail: Object.assign({}, detail || {}, { status: normalized }) + })); + } + + function documentHref(documentId, workspaceId) { + var url = new URL('/documents/' + encodeURIComponent(documentId), window.location.origin); + if (workspaceId) url.searchParams.set('workspaceId', workspaceId); + copyWorkspaceSourceParams(url); + return url.toString(); + } + + function convertToPreviousSiblingChild(trigger, detail) { + var documentId = detail.documentId || ''; + var row = document.querySelector('#sidebar-tree-root .tree-row[data-node-id="' + cssEscape(documentId) + '"]'); + if (!(row instanceof HTMLElement)) return; + var parentId = row.getAttribute('data-parent-id') || ''; + var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(candidate) { + return (candidate.getAttribute('data-parent-id') || '') === parentId; + }); + var index = siblings.indexOf(row); + if (index <= 0) { + window.alert('当前页面前面没有同级页面。'); + return; + } + var previous = siblings[index - 1]; + var previousId = previous.getAttribute('data-node-id') || ''; + var children = previous.parentElement ? previous.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : []; + void dispatchTreeCommand(trigger || row, { + action: 'move', + workspaceId: detail.workspaceId || resolveWorkspaceId(row), + documentId: documentId, + parentId: previousId, + sortOrder: children.length + }); + } + + function fileTreeCopyPath(detail, trigger) { + // 对 local_folder 复制真实相对路径,不复制标题 + if (currentSourceKind() === 'local_folder') { + if (detail.assetId) { + var path = localFilePathFromAssetId(detail.assetId); + if (path) { + try { + return decodeURIComponent(path.replace(/~2F/g, '/')); + } catch (_) { + return path.replace(/~2F/g, '/'); + } + } + } + if (detail.documentId) { + var docPath = String(detail.documentId || '') + .replace(/^local-md:/, '') + .replace(/^local-dir:/, '') + .replace(/~2F/g, '/'); + if (docPath) { + try { + return decodeURIComponent(docPath); + } catch (_) { + return docPath; + } + } + } + } + return detail.title || ''; + } + + function fileTreeMenuTargetParentId(detail, trigger) { + var rowKind = String(detail && detail.rowKind || '').trim(); + var rowId = String(detail && detail.rowId || '').trim(); + var documentId = String(detail && detail.documentId || '').trim(); + if (rowKind === 'folder' && rowId) return rowId; + if (rowKind === 'directory' && rowId) return rowId; + if (documentId) return documentId; + if (trigger && typeof trigger.getAttribute === 'function') { + var triggerKind = String(trigger.getAttribute('data-row-kind') || '').trim(); + var triggerRowId = String(trigger.getAttribute('data-row-id') || '').trim(); + if ((triggerKind === 'folder' || triggerKind === 'directory') && triggerRowId) return triggerRowId; + } + return ''; + } + + function withOfficeEditModeGuard(callback) { + document.documentElement.setAttribute('data-mnote-last-office-edit-mode-requested', 'true'); + document.documentElement.setAttribute('data-mnote-last-office-edit-mode-guard', 'silent'); + callback(); + } + + function handleTreeContextMenuAction(action, detail, trigger) { + closeTreeContextMenu(); + detail = detail || {}; + if (detail.contextKind === 'attachment') { + if (action === 'copy-link') { + void copyTreeContextValue(detail.href || '', 'attachment-copy-link'); + return; + } + if (action === 'download') { + openEditorAttachmentDownload(detail); + return; + } + if (action === 'popup-preview') { + openEditorAttachmentDetail(detail); + return; + } + if (action === 'new-window') { + openEditorAttachmentNewWindow(detail); + return; + } + if (action === 'new-window-edit') { + withOfficeEditModeGuard(function() { + openEditorAttachmentNewWindow(detail, 'edit'); + }); + return; + } + if (action === 'open-edit-mode') { + withOfficeEditModeGuard(function() { + void openEditorAttachmentEditTab(detail); + }); + return; + } + if (action === 'right-preview') { + dispatchSidebarEvent('tree.attachment.open-right', detail); + return; + } + if (action === 'copy-id') { + void copyTreeContextValue(detail.assetId || '', 'attachment-copy-id'); + return; + } + dispatchSidebarEvent('tree.attachment.action', { action: action, attachment: detail }); + return; + } + var documentId = detail.documentId || ''; + var workspaceId = detail.workspaceId || resolveWorkspaceId(trigger || document.body); + var title = detail.title || '无标题'; + var isAsset = detail.contextKind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index'; + if (detail.contextKind === 'filetree' && action === 'download') { + downloadSelectedFileTreeAssetRows(detail, trigger); + return; + } + if (isAsset && action === 'new-window') { + recordFileTreeAction('new-window', detail); + void openConvexAssetFromFileTree({ ...detail, openTarget: 'new-window' }); + return; + } + if (isAsset && action === 'open-edit-mode') { + withOfficeEditModeGuard(function() { + recordFileTreeAction('open-edit-mode', detail); + void openConvexAssetFromFileTree({ ...detail, openTarget: 'edit-mode' }); + }); + return; + } + if (isAsset && action === 'open-right') { + recordFileTreeAction('open-right', detail); + void openConvexAssetFromFileTree({ ...detail, openTarget: 'side' }); + return; + } + if (action === 'open-right') { + recordFileTreeAction('open-right', detail); + dispatchSidebarEvent('tree.page.open-right', detail); + return; + } + if (action === 'share') { + dispatchSidebarEvent('tree.page.share', detail); + return; + } + if (action === 'copy-link') { + void copyTreeContextValue(documentHref(documentId, workspaceId), 'copy-link'); + return; + } + if (action === 'copy-link-title') { + void copyTreeContextValue(title + ' ' + documentHref(documentId, workspaceId), 'copy-link-title'); + return; + } + if (action === 'copy-reference-inline') { + void copyTreeContextValue('((' + title + ' ' + documentId + '))', 'copy-reference-inline'); + return; + } + if (action === 'copy-reference-embed') { + void copyTreeContextValue('{{' + title + ' ' + documentId + '}}', 'copy-reference-embed'); + return; + } + if (action === 'copy-id') { + void copyTreeContextValue(documentId || detail.assetId || detail.rowId || '', 'copy-id'); + return; + } + if (action === 'delete-trash' && detail.contextKind === 'filetree') { + var selectedFileTreeRows = selectedSidebarFileTreeRows(); + if (selectedFileTreeRows.length > 1) { + void deleteSelectedSidebarFileTreeRows(trigger || document.body); + return; + } + } + if (action === 'delete-trash' && isAsset) { + if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return; + recordFileTreeAction('delete-trash', detail); + recordFileTreeActionStatus('pending', detail); + void deleteSingleFileTreeAsset(detail, trigger).then(function() { + recordFileTreeActionStatus('archived', Object.assign({}, detail, { undo: 'trash-modal' })); + if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot(); + }).catch(function(error) { + recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); + window.alert(error && error.message ? error.message : '资源删除失败'); + }); + return; + } + if (action === 'new-file') { + void createPage(trigger || document.body, fileTreeMenuTargetParentId(detail, trigger) || null); + return; + } + if (action === 'new-folder') { + recordFileTreeAction('new-folder', detail); + void createFileTreeFolder(trigger || document.body, fileTreeMenuTargetParentId(detail, trigger) || null).then(function(ok) { + recordFileTreeActionStatus(ok ? 'created' : 'skipped', detail); + }).catch(function(error) { + recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); + window.alert(error && error.message ? error.message : '新建文件夹失败'); + }); + return; + } + if (action === 'paste-into') { + var pasteRow = trigger && trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger; + recordFileTreeAction('paste-into', detail); + void pasteSidebarFileTreeClipboard(pasteRow).then(function(ok) { + recordFileTreeActionStatus(ok ? 'applied' : 'skipped', detail); + }).catch(function(error) { + recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); + window.alert(error && error.message ? error.message : '粘贴失败'); + }); + return; + } + if (action === 'copy-path') { + void copyTreeContextValue(fileTreeCopyPath(detail, trigger), 'copy-path'); + return; + } + if (action === 'refresh') { + document.documentElement.setAttribute('data-mnote-filetree-refresh-requested', 'true'); + dispatchSidebarEvent('tree.filetree.refresh', detail); + return; + } + if (action === 'collapse-all') { + document.querySelectorAll('#sidebar-file-tree-root .tree-row[aria-expanded="true"]').forEach(function(row) { + if (!(row instanceof HTMLElement)) return; + row.setAttribute('aria-expanded', 'false'); + var node = row.closest('.tree-node'); + var children = node ? node.querySelector(':scope > .tree-children') : null; + if (children) children.classList.add('tree-children--collapsed'); + }); + return; + } + if (action === 'reveal') { + if (trigger && typeof trigger.scrollIntoView === 'function') { + trigger.scrollIntoView({ block: 'nearest' }); + trigger.focus && trigger.focus(); + } + return; + } + if (action === 'duplicate') { + dispatchSidebarEvent('tree.page.duplicate', detail); + return; + } + if (action === 'rename') { + if (detail.contextKind === 'filetree' && trigger) { + var renameRow = trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger; + if (beginFileTreeInlineRename(renameRow)) return; + } + var nextTitle = window.prompt('重命名页面', title); + if (nextTitle && nextTitle.trim() && documentId) { + void dispatchTreeCommand(trigger || document.body, { + action: 'rename', + workspaceId: workspaceId, + documentId: documentId, + title: nextTitle.trim() + }).then(function(){ updateTitleEverywhere(documentId, nextTitle.trim()); }); + } + return; + } + if (action === 'create-child') { + void createPage(trigger || document.body, documentId); + return; + } + if (action === 'convert-child') { + convertToPreviousSiblingChild(trigger, detail); + return; + } + var deleteTargetId = documentId || String(detail.rowId || '').trim(); + if (action === 'delete-trash' && deleteTargetId) { + if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return; + recordFileTreeAction('delete-trash', detail); + recordFileTreeActionStatus('pending', detail); + void dispatchTreeCommand(trigger || document.body, { + action: 'archive', + workspaceId: workspaceId, + documentId: deleteTargetId + }).then(function() { + recordFileTreeActionStatus('archived', Object.assign({}, detail, { undo: 'trash-modal' })); + if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot(); + }).catch(function(error) { + recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); + window.alert(error && error.message ? error.message : '删除失败'); + }); + } + } + + function appendTreeContextMenuButton(menu, item, detail, trigger) { + if (item.separator) { + var sep = document.createElement('div'); + sep.className = 'mnote-tree-context-menu__separator'; + sep.setAttribute('role', 'separator'); + menu.appendChild(sep); + return; + } + var button = document.createElement('button'); + button.type = 'button'; + button.className = item.danger ? 'mnote-tree-context-menu__item mnote-tree-context-menu__item--danger' : 'mnote-tree-context-menu__item'; + button.setAttribute('role', 'menuitem'); + button.setAttribute('data-action', item.action); + button.disabled = item.disabled === true; + if (item.title) button.title = item.title; + var icon = document.createElement('span'); + icon.className = 'material-symbols-outlined mnote-tree-context-menu__icon'; + icon.setAttribute('aria-hidden', 'true'); + icon.setAttribute('data-icon', item.icon || 'radio_button_unchecked'); + var label = document.createElement('span'); + label.className = 'mnote-tree-context-menu__label'; + label.textContent = item.label; + button.appendChild(icon); + button.appendChild(label); + if (item.shortcut) { + var shortcut = document.createElement('span'); + shortcut.className = 'mnote-tree-context-menu__shortcut'; + shortcut.textContent = item.shortcut; + button.appendChild(shortcut); + } + button.addEventListener('click', function(event) { + event.preventDefault(); + event.stopPropagation(); + handleTreeContextMenuAction(item.action, detail, trigger); + }); + menu.appendChild(button); + } + + // ── CommandContext 启用态辅助:与 Rust core-protocol CommandContext 保持同一口径 ── + + function buildSidebarFileTreeContext(kind) { + var runtime = window.__mnoteFileTreeContextMenuRuntime; + if (runtime && typeof runtime.buildSidebarFileTreeContext === 'function') { + try { + return runtime.buildSidebarFileTreeContext(kind, { + selection: sidebarFileTreeSelection, + currentSourceKind: currentSourceKind, + workspaceReadonly: function() { + return document.documentElement.getAttribute('data-mnote-workspace-readonly') === 'true'; + }, + queryRowById: function(rowId) { + return document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]'); + } + }); + } catch (_) {} + } + var s = sidebarFileTreeSelection; + var rowIds = Array.from(s.selectedRowIds || []); + var rows = []; + for (var i = 0; i < rowIds.length; i += 1) { + var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowIds[i]) + '"]'); + if (row instanceof HTMLElement) rows.push(row); + } + var resourceKinds = {}; + for (var i = 0; i < rows.length; i += 1) { + var rk = rows[i].getAttribute('data-row-kind') || 'unknown'; + resourceKinds[rk] = true; + } + var rkKeys = Object.keys(resourceKinds); + return { + 'workspace.sourceKind': currentSourceKind() || '', + 'workspace.readonly': document.documentElement.getAttribute('data-mnote-workspace-readonly') === 'true', + 'tree.focusKind': kind === 'filetree' ? 'file_tree' : kind === 'page' ? 'page_tree' : kind, + 'tree.selectionCount': rows.length, + 'tree.selectionResourceKind': rkKeys.length === 1 ? rkKeys[0] : 'mixed' + }; + } + + function evaluateSidebarFileTreeWhen(ctx, expr) { + var runtime = window.__mnoteFileTreeContextMenuRuntime; + if (runtime && typeof runtime.evaluateSidebarFileTreeWhen === 'function') { + return runtime.evaluateSidebarFileTreeWhen(ctx || {}, expr || ''); + } + if (!expr) return true; + try { + expr = expr.trim(); + // 词法切分:支持 key、!key、==、!=、&&、|| 和括号分组 + var tokens = []; + var i = 0; + while (i < expr.length) { + var ch = expr[i]; + if (ch === ' ' || ch === '\t') { i++; continue; } + if (ch === '(') { tokens.push({ t: '(' }); i++; continue; } + if (ch === ')') { tokens.push({ t: ')' }); i++; continue; } + if (ch === '&' && expr[i+1] === '&') { tokens.push({ t: '&&' }); i += 2; continue; } + if (ch === '|' && expr[i+1] === '|') { tokens.push({ t: '||' }); i += 2; continue; } + if (ch === '=' && expr[i+1] === '=') { tokens.push({ t: '==' }); i += 2; continue; } + if (ch === '!' && expr[i+1] === '=') { tokens.push({ t: '!=' }); i += 2; continue; } + if (ch === '!') { tokens.push({ t: '!' }); i++; continue; } + if (ch === '"' || ch === "'") { + i++; + var str = ''; + while (i < expr.length && expr[i] !== ch) { str += expr[i]; i++; } + if (i < expr.length) i++; + tokens.push({ t: 'str', v: str }); + continue; + } + if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '.' || (ch >= '0' && ch <= '9')) { + var id = ''; + while (i < expr.length && ((expr[i] >= 'a' && expr[i] <= 'z') || (expr[i] >= 'A' && expr[i] <= 'Z') || (expr[i] >= '0' && expr[i] <= '9') || expr[i] === '_' || expr[i] === '.')) { id += expr[i]; i++; } + tokens.push({ t: 'id', v: id }); + continue; + } + throw new Error('Unexpected char: ' + ch); + } + var pos = 0; + function peek() { return tokens[pos]; } + function consume() { return tokens[pos++]; } + function expect(t) { + var tok = consume(); + if (!tok || tok.t !== t) throw new Error('Expected ' + t + ' got ' + (tok ? tok.t : 'EOF')); + return tok; + } + function ctxVal(key) { + var v = ctx[key]; + if (v === undefined || v === null) return ''; + return v; + } + function parseOr() { + var left = parseAnd(); + while (peek() && peek().t === '||') { consume(); var right = parseAnd(); left = left || right; } + return left; + } + function parseAnd() { + var left = parsePrimary(); + while (peek() && peek().t === '&&') { consume(); var right = parsePrimary(); left = left && right; } + return left; + } + function parsePrimary() { + if (!peek()) throw new Error('Unexpected EOF'); + if (peek().t === '(') { + consume(); + var val = parseOr(); + expect(')'); + return val; + } + if (peek().t === '!') { + consume(); + var keyTok = consume(); + if (!keyTok || keyTok.t !== 'id') throw new Error('Expected key after !'); + return !ctxVal(keyTok.v); + } + if (peek().t === 'id') { + var keyTok = consume(); + if (peek() && peek().t === '==') { + consume(); + var valTok = consume(); + if (!valTok) throw new Error('Expected value after =='); + var val = valTok.t === 'str' ? valTok.v : (valTok.v || ''); + // 与 Rust evaluate_when 保持一致:按 string / bool / number 比较。 + var cv = ctxVal(keyTok.v); + if (val === 'true') return cv === true || cv === 'true'; + if (val === 'false') return cv === false || cv === '' || cv === 'false'; + var num = Number(val); + if (!isNaN(num) && String(num) === val) return Number(cv) === num; + return String(cv) === val; + } + if (peek() && peek().t === '!=') { + consume(); + var valTok = consume(); + if (!valTok) throw new Error('Expected value after !='); + var val = valTok.t === 'str' ? valTok.v : (valTok.v || ''); + var cv = ctxVal(keyTok.v); + if (val === 'true') return !(cv === true || cv === 'true'); + if (val === 'false') return !(cv === false || cv === '' || cv === 'false'); + var num = Number(val); + if (!isNaN(num) && String(num) === val) return Number(cv) !== num; + return String(cv) !== val; + } + // 单独 key:按 truthy 规则判断。 + var cv = ctxVal(keyTok.v); + if (typeof cv === 'boolean') return cv; + if (typeof cv === 'number') return cv !== 0; + return cv !== '' && cv !== undefined && cv !== null; + } + throw new Error('Unexpected token: ' + peek().t); + } + return parseOr(); + } catch(e) { + return true; + } + } + + function openTreeContextMenu(kind, detail, x, y, trigger) { + closeTreeContextMenu(); + detail = Object.assign({}, detail || {}, { contextKind: kind }); + var menu = document.createElement('div'); + menu.className = 'mnote-tree-context-menu'; + menu.setAttribute('role', 'menu'); + menu.setAttribute('data-testid', 'mnote-tree-context-menu'); + menu.setAttribute('data-kind', kind); + var isAttachment = kind === 'attachment'; + var isAsset = kind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index'; + var isFileTreeDownload = kind === 'filetree' && detail.downloadable; + var ctx = buildSidebarFileTreeContext(kind); + var items = isAttachment ? [ + { action: 'duplicate', icon: 'file_copy', label: '拷贝副本', shortcut: 'Ctrl + D' }, + { action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' }, + { separator: true }, + { action: 'copy-link', icon: 'link', label: '复制链接' }, + { action: 'move-embed', icon: 'subdirectory_arrow_right', label: '移动/嵌入到...', shortcut: 'Alt+Shift+M/G' }, + { action: 'history', icon: 'history', label: '块历史...' }, + { separator: true }, + { action: 'popup-preview', icon: 'preview', label: '弹窗预览' }, + { action: 'right-preview', icon: 'right_panel_open', label: '右侧预览' }, + { action: 'open-edit-mode', icon: 'edit_note', label: '弹窗编辑' }, + { action: 'new-window-edit', icon: 'open_in_new', label: '新窗口编辑' }, + { action: 'new-window', icon: 'open_in_new', label: '在新窗口打开' }, + { action: 'download', icon: 'download', label: '下载' }, + { action: 'replace-file', icon: 'sync', label: '更换文件' }, + { action: 'rename-attachment', icon: 'drive_file_rename_outline', label: '重命名' }, + { action: 'comment', icon: 'mode_comment', label: '评论', shortcut: 'Ctrl+Alt+M' }, + { action: 'caption', icon: 'notes', label: '添加说明文字' }, + { separator: true }, + { action: 'color', icon: 'format_paint', label: '颜色' } + ] : isAsset ? [ + { action: 'open-edit-mode', icon: 'edit_note', label: '使用编辑模式打开' }, + { action: 'new-window', icon: 'open_in_new', label: '在新窗口打开' }, + { action: 'download', icon: 'download', label: Number(detail.selectedDownloadCount || detail.selectedAssetCount || 0) > 1 ? '下载 ' + Number(detail.selectedDownloadCount || detail.selectedAssetCount || 0) + ' 个项目' : '下载' }, + { action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2', when: '!workspace.readonly' }, + { action: 'copy-id', icon: 'tag', label: '复制资源 ID' }, + { action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' }, + { separator: true }, + { action: 'copy-path', icon: 'content_copy', label: 'Copy Path' }, + { action: 'refresh', icon: 'refresh', label: 'Refresh' }, + { action: 'reveal', icon: 'my_location', label: 'Reveal' } + ] : kind === 'filetree' ? [ + { action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' }, + { action: 'download', icon: 'download', label: Number(detail.selectedDownloadCount || 0) > 1 ? '下载 ' + Number(detail.selectedDownloadCount || 0) + ' 个项目' : '下载', disabled: !isFileTreeDownload, title: isFileTreeDownload ? '下载当前本地文件或文件夹' : '当前项目没有可下载的本地路径' }, + { separator: true }, + { action: 'copy-link', icon: 'link', label: '复制访问链接' }, + { action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' }, + { action: 'copy-id', icon: 'tag', label: '复制页面ID' }, + { action: 'copy-path', icon: 'content_copy', label: 'Copy Path' }, + { separator: true }, + { action: 'new-file', icon: 'note_add', label: 'New File', when: '!workspace.readonly' }, + { action: 'new-folder', icon: 'create_new_folder', label: 'New Folder', disabled: currentSourceKind() !== 'local_folder', title: currentSourceKind() === 'local_folder' ? '在当前目录下创建子文件夹' : '仅 local folder 支持创建文件夹', when: '!workspace.readonly' }, + { action: 'paste-into', icon: 'content_paste', label: '粘贴到此处', disabled: !runtimeState.sidebarFileTreeClipboard, title: runtimeState.sidebarFileTreeClipboard ? '粘贴到当前文件树目标' : '剪贴板为空', when: '!workspace.readonly' }, + { action: 'refresh', icon: 'refresh', label: 'Refresh' }, + { action: 'collapse-all', icon: 'unfold_less', label: 'Collapse All' }, + { action: 'reveal', icon: 'my_location', label: 'Reveal' }, + { separator: true }, + { action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2', when: '!workspace.readonly' }, + { separator: true }, + { action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' } + ] : [ + { action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' }, + { separator: true }, + { action: 'copy-link', icon: 'link', label: '复制访问链接' }, + { action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' }, + { action: 'copy-id', icon: 'tag', label: '复制页面ID' }, + { separator: true }, + { action: 'rename', icon: 'edit', label: '重命名', when: '!workspace.readonly' }, + { separator: true }, + { action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' } + ]; + items.forEach(function(item) { + if (item.when !== undefined && !evaluateSidebarFileTreeWhen(ctx, item.when)) { + item = Object.assign({}, item, { disabled: true, title: item.title || '当前上下文不支持此操作' }); + } + appendTreeContextMenuButton(menu, item, detail, trigger); + }); + document.body.appendChild(menu); + var rect = menu.getBoundingClientRect(); + var left = Math.min(Math.max(8, x || 8), Math.max(8, window.innerWidth - rect.width - 8)); + var top = Math.min(Math.max(8, y || 8), Math.max(8, window.innerHeight - rect.height - 8)); + menu.style.left = left + 'px'; + menu.style.top = top + 'px'; + runtimeState.activeTreeContextMenu = menu; + } + + function openPageTreeContextMenu(row, x, y, trigger) { + if (!(row instanceof HTMLElement)) return; + var documentId = row.getAttribute('data-node-id') || ''; + openTreeContextMenu('page', { + documentId: documentId, + rowId: documentId, + rowKind: 'document', + title: rowTitle(row), + workspaceId: resolveWorkspaceId(row) + }, x, y, trigger || row); + } + + function openFileTreeContextMenu(row, x, y, trigger) { + if (!(row instanceof HTMLElement)) return; + var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || ''; + var selectedDownloadRows = selectedSidebarFileTreeRowsForDownload(row); + var selectedAssetRows = selectedDownloadRows.filter(isFileTreeDownloadableAssetRow); + openTreeContextMenu('filetree', { + documentId: documentId, + rowId: row.getAttribute('data-row-id') || '', + rowKind: row.getAttribute('data-row-kind') || '', + assetId: row.getAttribute('data-asset-id') || '', + localRelativePath: fileTreeRowLocalRelativePath(row), + title: rowTitle(row), + workspaceId: resolveWorkspaceId(row), + downloadable: isFileTreeDownloadableRow(row), + selectedDownloadCount: selectedDownloadRows.length, + selectedDownloadRowIds: selectedDownloadRows.map(function(downloadRow) { return downloadRow.getAttribute('data-row-id') || ''; }).filter(Boolean), + selectedAssetCount: selectedAssetRows.length, + selectedAssetRowIds: selectedAssetRows.map(function(assetRow) { return assetRow.getAttribute('data-row-id') || ''; }).filter(Boolean) + }, x, y, trigger || row); + } + + function visibleFileTreeRows() { + var runtimeFn = fileTreeSelectionRuntimeFunction('visibleFileTreeRows'); + if (runtimeFn) return runtimeFn(fileTreeSelectionRuntimeDeps()); + return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]')) + .filter(function(row) { + if (!(row instanceof HTMLElement)) return false; + if (row.closest('.tree-children--collapsed')) return false; + return row.offsetParent !== null || row.getClientRects().length > 0; + }); + } + + function syncSidebarFileTreeSelection() { + var runtimeFn = fileTreeSelectionRuntimeFunction('syncSidebarFileTreeSelection'); + if (runtimeFn) { + runtimeFn(sidebarFileTreeSelection, fileTreeSelectionRuntimeDeps()); + return; + } + var rows = visibleFileTreeRows(); + rows.forEach(function(row) { + var rowId = row.getAttribute('data-row-id') || ''; + row.setAttribute('data-selected', String(Boolean(rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId)))); + row.setAttribute('data-focused', String(rowId === sidebarFileTreeSelection.focusedRowId)); + }); + window.dispatchEvent(new CustomEvent('tree.filetree.selection.changed', { + detail: { + selectedRowIds: Array.from(sidebarFileTreeSelection.selectedRowIds), + anchorRowId: sidebarFileTreeSelection.anchorRowId, + focusedRowId: sidebarFileTreeSelection.focusedRowId + } + })); + } + + function selectSidebarFileTreeRow(row, modifiers) { + var runtimeFn = fileTreeSelectionRuntimeFunction('selectSidebarFileTreeRow'); + if (runtimeFn) return runtimeFn(row, sidebarFileTreeSelection, modifiers, fileTreeSelectionRuntimeDeps()); + if (!(row instanceof HTMLElement)) return []; + var rowId = row.getAttribute('data-row-id') || ''; + if (!rowId) return []; + var rows = visibleFileTreeRows(); + var visibleRowIds = rows.map(function(item) { return item.getAttribute('data-row-id') || ''; }).filter(Boolean); + var selected = new Set(sidebarFileTreeSelection.selectedRowIds); + var shiftKey = Boolean(modifiers && modifiers.shiftKey); + var ctrlKey = Boolean(modifiers && (modifiers.ctrlKey || modifiers.metaKey)); + if (shiftKey && sidebarFileTreeSelection.anchorRowId) { + var anchorIndex = visibleRowIds.indexOf(sidebarFileTreeSelection.anchorRowId); + var targetIndex = visibleRowIds.indexOf(rowId); + if (anchorIndex >= 0 && targetIndex >= 0) { + selected = new Set(visibleRowIds.slice(Math.min(anchorIndex, targetIndex), Math.max(anchorIndex, targetIndex) + 1)); + } else { + selected = new Set([rowId]); + sidebarFileTreeSelection.anchorRowId = rowId; + } + } else if (ctrlKey) { + if (selected.has(rowId) && selected.size > 1) selected.delete(rowId); + else selected.add(rowId); + sidebarFileTreeSelection.anchorRowId = rowId; + } else { + selected = new Set([rowId]); + sidebarFileTreeSelection.anchorRowId = rowId; + } + sidebarFileTreeSelection.selectedRowIds = selected; + sidebarFileTreeSelection.focusedRowId = rowId; + syncSidebarFileTreeSelection(); + return Array.from(selected); + } + + function selectSidebarFileTreeDocument(documentId, options) { + var id = String(documentId || '').trim(); + if (!id) return false; + var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + id) + '"]') + || document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-document-id="' + cssEscape(id) + '"]') + || document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-doc-id="' + cssEscape(id) + '"]'); + if (!(row instanceof HTMLElement)) return false; + var rowId = row.getAttribute('data-row-id') || ''; + if (!rowId) return false; + selectSidebarFileTreeRow(row, { ctrlKey: false, metaKey: false, shiftKey: false }); + document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-active="true"]').forEach(function(activeRow) { + if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'false'); + }); + row.setAttribute('data-active', 'true'); + if (options && options.scrollIntoView !== false) { + try { + row.scrollIntoView({ block: 'nearest' }); + } catch (_) { + row.scrollIntoView(); + } + } + return true; + } + + function selectSidebarFileTreeRowById(rowId, options) { + var id = String(rowId || '').trim(); + if (!id) return false; + var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(id) + '"]'); + if (!(row instanceof HTMLElement)) return false; + selectSidebarFileTreeRow(row, { ctrlKey: false, metaKey: false, shiftKey: false }); + document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-active="true"]').forEach(function(activeRow) { + if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'false'); + }); + row.setAttribute('data-active', 'true'); + if (options && options.scrollIntoView !== false) { + try { + row.scrollIntoView({ block: 'nearest' }); + } catch (_) { + row.scrollIntoView(); + } + } + return true; + } + + function pendingLocalFolderRestoreRowId() { + if (window.__mnotePendingLocalFolderRestoreFiletreeRowId) { + return String(window.__mnotePendingLocalFolderRestoreFiletreeRowId || '').trim(); + } + if (!window.localStorage) return ''; + var rowId = ''; + try { + rowId = String(window.localStorage.getItem('mnote.pendingLocalFolderRestoreFiletreeRowId') || '').trim(); + } catch (_) { + rowId = ''; + } + if (!rowId && typeof window.name === 'string' && window.name.indexOf('mnote.pendingLocalFolderRestoreFiletreeRowId=') === 0) { + rowId = decodeURIComponent(window.name.slice('mnote.pendingLocalFolderRestoreFiletreeRowId='.length)); + } + if (rowId) window.__mnotePendingLocalFolderRestoreFiletreeRowId = rowId; + return rowId; + } + + function clearPendingLocalFolderRestoreRowId() { + window.__mnotePendingLocalFolderRestoreFiletreeRowId = ''; + try { + window.localStorage.removeItem('mnote.pendingLocalFolderRestoreFiletreeRowId'); + } catch (_) {} + if (typeof window.name === 'string' && window.name.indexOf('mnote.pendingLocalFolderRestoreFiletreeRowId=') === 0) { + window.name = ''; + } + } + + function applyPendingLocalFolderRestoreFocusOnce() { + var rowId = pendingLocalFolderRestoreRowId(); + if (!rowId) return false; + document.documentElement.setAttribute('data-mnote-local-folder-restore-pending-row-id', rowId); + if (!selectSidebarFileTreeRowById(rowId, { scrollIntoView: true })) { + document.documentElement.setAttribute('data-mnote-local-folder-restore-focus-status', 'row-missing'); + return false; + } + document.documentElement.setAttribute('data-mnote-local-folder-restore-focused-row-id', rowId); + document.documentElement.setAttribute('data-mnote-local-folder-restore-focus-status', 'focused'); + return true; + } + + function schedulePendingLocalFolderRestoreFocus() { + if (!pendingLocalFolderRestoreRowId()) return false; + if (window.__mnotePendingLocalFolderRestoreFocusTimer) return false; + var attempts = 0; + var focused = false; + focused = applyPendingLocalFolderRestoreFocusOnce() || focused; + window.__mnotePendingLocalFolderRestoreFocusTimer = window.setInterval(function() { + attempts += 1; + focused = applyPendingLocalFolderRestoreFocusOnce() || focused; + if (attempts >= 20) { + if (focused) clearPendingLocalFolderRestoreRowId(); + else document.documentElement.setAttribute('data-mnote-local-folder-restore-focus-status', 'timeout'); + window.clearInterval(window.__mnotePendingLocalFolderRestoreFocusTimer); + window.__mnotePendingLocalFolderRestoreFocusTimer = 0; + } + }, 250); + return focused; + } + + function selectedSidebarFileTreeRowIdsForDrag(row) { + var runtimeFn = fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRowIdsForDrag'); + if (runtimeFn) return runtimeFn(row, sidebarFileTreeSelection); + var rowId = row instanceof HTMLElement ? row.getAttribute('data-row-id') || '' : ''; + if (rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId)) { + return Array.from(sidebarFileTreeSelection.selectedRowIds); + } + return rowId ? [rowId] : []; + } + + function selectedSidebarFileTreeRows() { + var runtimeFn = fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRows'); + if (runtimeFn) return runtimeFn(sidebarFileTreeSelection, fileTreeSelectionRuntimeDeps()); + var selectedIds = sidebarFileTreeSelection.selectedRowIds; + var rows = visibleFileTreeRows().filter(function(row) { + var rowId = row.getAttribute('data-row-id') || ''; + return rowId && selectedIds.has(rowId); + }); + if (rows.length > 0) return rows; + if (sidebarFileTreeSelection.focusedRowId) { + var focused = document.querySelector( + '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(sidebarFileTreeSelection.focusedRowId) + '"]' + ); + if (focused instanceof HTMLElement) return [focused]; + } + return []; + } + + function fileTreeRowDocumentId(row) { + var runtimeFn = fileTreeRuntimeFunction('fileTreeRowDocumentId'); + if (runtimeFn) return runtimeFn(row); + if (!(row instanceof HTMLElement)) return ''; + return String(row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '').trim(); + } + + function fileTreeRowAssetId(row) { + var runtimeFn = fileTreeRuntimeFunction('fileTreeRowAssetId'); + if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); + if (!(row instanceof HTMLElement)) return ''; + var assetId = String(row.getAttribute('data-asset-id') || '').trim(); + if (assetId) return assetId; + if (currentSourceKind() === 'local_folder' && fileTreeRowKind(row) === 'asset') { + return String(row.getAttribute('data-row-id') || '').trim(); + } + return ''; + } + + function decodeLocalEncodedPath(value) { + var runtimeFn = fileTreeRuntimeFunction('decodeLocalEncodedPath'); + if (runtimeFn) return runtimeFn(value); + var path = String(value || '').trim().replace(/~2F/g, '/'); + if (!path) return ''; + try { + return decodeURIComponent(path); + } catch (_) { + return path; + } + } + + function fileTreeRowLocalRelativePath(row) { + var runtimeFn = fileTreeRuntimeFunction('fileTreeRowLocalRelativePath'); + if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); + if (!(row instanceof HTMLElement)) return ''; + var direct = String(row.getAttribute('data-local-relative-path') || '').trim(); + if (direct) return direct; + var assetPath = localFilePathFromAssetId(fileTreeRowAssetId(row)); + if (assetPath) return assetPath; + var rowId = String(row.getAttribute('data-row-id') || '').trim(); + var nodeId = String(row.getAttribute('data-node-id') || '').trim(); + var documentId = fileTreeRowDocumentId(row); + var kind = fileTreeRowKind(row); + if (rowId.indexOf('local:asset:') === 0) return rowId.slice('local:asset:'.length); + if (rowId.indexOf('local:markdown:') === 0) return rowId.slice('local:markdown:'.length); + if (rowId.indexOf('local:folder:') === 0) return rowId.slice('local:folder:'.length); + if (rowId.indexOf('local:node:') === 0) return rowId.slice('local:node:'.length); + if (nodeId.indexOf('local:node:') === 0) return nodeId.slice('local:node:'.length); + if ((kind === 'document' || kind === 'doc' || kind === 'markdown') && documentId.indexOf('local-md:') === 0) { + return decodeLocalEncodedPath(documentId.slice('local-md:'.length)); + } + if ((kind === 'folder' || kind === 'directory' || kind === 'index') && documentId.indexOf('local-dir:') === 0) { + return decodeLocalEncodedPath(documentId.slice('local-dir:'.length)); + } + return ''; + } + + function fileTreeRowLocalUploadTargetRelativePath(row) { + var runtimeFn = fileTreeRuntimeFunction('fileTreeRowLocalUploadTargetRelativePath'); + if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); + if (!(row instanceof HTMLElement)) return ''; + var kind = fileTreeRowKind(row); + var relativePath = fileTreeRowLocalRelativePath(row); + if (!relativePath) return ''; + if (kind === 'folder' || kind === 'directory') return relativePath; + var lastSlash = relativePath.lastIndexOf('/'); + return lastSlash >= 0 ? relativePath.slice(0, lastSlash) : ''; + } + + function fileTreeRowKind(row) { + var runtimeFn = fileTreeRuntimeFunction('fileTreeRowKind'); + if (runtimeFn) return runtimeFn(row); + if (!(row instanceof HTMLElement)) return ''; + return String(row.getAttribute('data-row-kind') || '').trim(); + } + + function isFileTreeDownloadableAssetRow(row) { + var runtimeFn = fileTreeRuntimeFunction('isFileTreeDownloadableAssetRow'); + if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); + if (!(row instanceof HTMLElement)) return false; + var kind = fileTreeRowKind(row); + if (kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown' || kind === 'folder' || kind === 'directory') return false; + return Boolean(fileTreeRowAssetId(row)); + } + + function isFileTreeDownloadableRow(row) { + var runtimeFn = fileTreeRuntimeFunction('isFileTreeDownloadableRow'); + if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); + if (!(row instanceof HTMLElement)) return false; + if (currentSourceKind() !== 'local_folder') return isFileTreeDownloadableAssetRow(row); + return Boolean(fileTreeRowLocalRelativePath(row) || isFileTreeDownloadableAssetRow(row)); + } + + function fileTreeAssetDownloadDetail(row) { + var runtimeFn = fileTreeRuntimeFunction('fileTreeAssetDownloadDetail'); + if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); + if (!isFileTreeDownloadableRow(row)) return null; + var title = rowTitle(row); + var relativePath = fileTreeRowLocalRelativePath(row); + var assetId = fileTreeRowAssetId(row) || (relativePath ? 'local-file:' + relativePath : ''); + return { + contextKind: 'filetree', + documentId: fileTreeRowDocumentId(row), + rowId: row.getAttribute('data-row-id') || '', + rowKind: fileTreeRowKind(row), + assetId: assetId, + localRelativePath: relativePath, + title: title, + fileName: title, + workspaceId: resolveWorkspaceId(row) + }; + } + + function selectedSidebarFileTreeRowsForDownload(contextRow) { + var contextRowId = contextRow instanceof HTMLElement ? contextRow.getAttribute('data-row-id') || '' : ''; + var selectedRows = selectedSidebarFileTreeRows().filter(isFileTreeDownloadableRow); + if (contextRowId && sidebarFileTreeSelection.selectedRowIds.has(contextRowId) && selectedRows.length > 0) return selectedRows; + return isFileTreeDownloadableRow(contextRow) ? [contextRow] : selectedRows; + } + + function selectedSidebarFileTreeAssetRowsForDownload(contextRow) { + return selectedSidebarFileTreeRowsForDownload(contextRow).filter(isFileTreeDownloadableAssetRow); + } + + function downloadSelectedFileTreeAssetRows(detail, trigger) { + var contextRow = trigger && trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger; + if (!(contextRow instanceof HTMLElement) && detail && detail.rowId) { + contextRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(detail.rowId) + '"]'); + } + var rows = selectedSidebarFileTreeRowsForDownload(contextRow); + var seen = new Set(); + var downloads = []; + rows.forEach(function(row) { + var item = fileTreeAssetDownloadDetail(row); + var key = item && (item.localRelativePath || item.assetId || item.rowId); + if (!item || !key || seen.has(key)) return; + seen.add(key); + downloads.push(item); + }); + if (downloads.length === 0 && detail && detail.assetId) { + downloads.push(Object.assign({}, detail, { fileName: detail.fileName || detail.title || '附件' })); + } else if (downloads.length === 0 && detail && detail.localRelativePath) { + downloads.push(Object.assign({}, detail, { + assetId: detail.assetId || 'local-file:' + detail.localRelativePath, + fileName: detail.fileName || detail.title || '附件' + })); + } + if (downloads.length === 0) { + recordFileTreeActionStatus('blocked', Object.assign({}, detail || {}, { reason: 'no-downloadable-assets' })); + return false; + } + var primary = Object.assign({}, downloads[0], { + count: downloads.length, + assetIds: downloads.map(function(item) { return item.assetId || ''; }).filter(Boolean), + localRelativePaths: downloads.map(function(item) { return item.localRelativePath || ''; }).filter(Boolean) + }); + recordFileTreeAction(downloads.length > 1 ? 'bulk-download' : 'download', primary); + recordFileTreeActionStatus('requested', primary); + document.documentElement.setAttribute('data-mnote-filetree-download-count', String(downloads.length)); + document.documentElement.setAttribute('data-mnote-filetree-download-asset-ids', downloads.map(function(item) { return item.assetId || ''; }).filter(Boolean).join(',')); + document.documentElement.setAttribute('data-mnote-filetree-download-paths', downloads.map(function(item) { return item.localRelativePath || ''; }).filter(Boolean).join(',')); + downloads.forEach(function(item) { + void openEditorAttachmentDownload(item); + }); + return true; + } + + function hasSelectedDocumentAncestor(row, selectedDocRowIds) { + var node = row instanceof HTMLElement ? row.closest('.tree-node') : null; + while (node && node.parentElement) { + var parentChildren = node.parentElement.closest('.tree-children'); + var parentNode = parentChildren ? parentChildren.closest('.tree-node') : null; + var parentRow = parentNode ? parentNode.querySelector(':scope > .tree-row[data-shell-mode="filetree"]') : null; + if (parentRow instanceof HTMLElement) { + var parentRowId = parentRow.getAttribute('data-row-id') || ''; + if (selectedDocRowIds.has(parentRowId)) return true; + } + node = parentNode; + } + return false; + } + + function classifySidebarFileTreeAsset(row) { + var badge = row instanceof HTMLElement ? row.querySelector('.tree-kind-badge') : null; + var iconKind = badge instanceof HTMLElement ? String(badge.getAttribute('data-kind') || '').trim() : ''; + var objectKind = row instanceof HTMLElement ? String(row.getAttribute('data-object-kind') || '').trim() : ''; + var title = rowTitle(row).toLowerCase(); + if (objectKind === 'mindmap' || iconKind === 'mindmap') return 'mindmap'; + if (objectKind === 'table' || iconKind === 'table' || iconKind === 'luckysheet' || title.indexOf('.luckysheet') >= 0) return 'table'; + return 'file'; + } + + function buildSidebarFileTreeDeletePlan(rows) { + var docRows = []; + var folderRows = []; + var assetRows = []; + rows.forEach(function(row) { + var kind = fileTreeRowKind(row); + if ((kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown') && fileTreeRowDocumentId(row)) { + docRows.push(row); + return; + } + if (kind === 'folder' && currentSourceKind() === 'local_folder') { + folderRows.push(row); + return; + } + if (fileTreeRowAssetId(row)) assetRows.push(row); + }); + var selectedContainerRowIds = new Set(docRows.concat(folderRows).map(function(row) { return row.getAttribute('data-row-id') || ''; }).filter(Boolean)); + var topDocRows = []; + var topFolderRows = []; + var seenDocs = new Set(); + var seenFolders = new Set(); + docRows.forEach(function(row) { + var documentId = fileTreeRowDocumentId(row); + if (!documentId || seenDocs.has(documentId)) return; + if (hasSelectedDocumentAncestor(row, selectedContainerRowIds)) return; + seenDocs.add(documentId); + topDocRows.push(row); + }); + folderRows.forEach(function(row) { + var rowId = row.getAttribute('data-row-id') || ''; + if (!rowId || seenFolders.has(rowId)) return; + if (hasSelectedDocumentAncestor(row, selectedContainerRowIds)) return; + seenFolders.add(rowId); + topFolderRows.push(row); + }); + var selectedTopContainerRows = new Set(topDocRows.concat(topFolderRows).map(function(row) { return row.getAttribute('data-row-id') || ''; }).filter(Boolean)); + var fileAssetRows = []; + var mindmapRows = []; + var tableRows = []; + var seenAssets = new Set(); + assetRows.forEach(function(row) { + var assetId = fileTreeRowAssetId(row); + if (!assetId || seenAssets.has(assetId)) return; + if (hasSelectedDocumentAncestor(row, selectedTopContainerRows)) return; + seenAssets.add(assetId); + var assetKind = classifySidebarFileTreeAsset(row); + if (assetKind === 'mindmap') mindmapRows.push(row); + else if (assetKind === 'table') tableRows.push(row); + else fileAssetRows.push(row); + }); + return { + docRows: topDocRows, + folderRows: topFolderRows, + fileAssetRows: fileAssetRows, + mindmapRows: mindmapRows, + tableRows: tableRows + }; + } + + function sidebarFileTreeDeleteConfirmText(plan) { + var docCount = plan.docRows.length; + var folderCount = plan.folderRows.length; + var fileCount = plan.fileAssetRows.length; + var mindmapCount = plan.mindmapRows.length; + var tableCount = plan.tableRows.length; + var parts = []; + if (docCount > 0) parts.push(docCount + ' 个页面(删除到垃圾桶)'); + if (folderCount > 0) parts.push(folderCount + ' 个文件夹(删除到垃圾桶)'); + if (fileCount > 0) parts.push(fileCount + ' 个附件(删除,10 分钟内可撤销)'); + if (mindmapCount > 0) parts.push(mindmapCount + ' 个思维导图(移入垃圾桶,10 分钟内可恢复)'); + if (tableCount > 0) parts.push(tableCount + (currentSourceKind() === 'local_folder' ? ' 个在线表格(移入垃圾桶,10 分钟内可恢复)' : ' 个在线表格(删除)')); + return '确认删除选中的 ' + parts.join(' + ') + ' 吗?'; + } + + async function postSidebarFileTreeJson(url, body) { + var response = await fetch(url, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(body || {}) + }); + var payload = await response.json().catch(function() { return null; }); + if (!response.ok) { + throw new Error((payload && (payload.error || payload.message)) || (url + ' failed: ' + response.status)); + } + return payload; + } + + function fileTreeRowsByRowIds(rowIds) { + var runtimeFn = fileTreeRuntimeFunction('fileTreeRowsByRowIds'); + if (runtimeFn) return runtimeFn(rowIds, fileTreeRuntimeDeps()); + return (rowIds || []).map(function(rowId) { + return document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]'); + }).filter(function(row) { return row instanceof HTMLElement; }); + } + + function fileTreeChildCount(documentId) { + var runtimeFn = fileTreeRuntimeFunction('fileTreeChildCount'); + if (runtimeFn) return runtimeFn(documentId, fileTreeRuntimeDeps()); + if (!documentId) return 0; + var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + documentId) + '"]'); + var node = row ? row.closest('.tree-node') : null; + var children = node ? node.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"][data-row-kind="document"], :scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"][data-row-kind="doc"]') : []; + return children.length; + } + + async function pasteSidebarFileTreeClipboard(trigger) { + if (!runtimeState.sidebarFileTreeClipboard || !Array.isArray(runtimeState.sidebarFileTreeClipboard.rowIds) || runtimeState.sidebarFileTreeClipboard.rowIds.length === 0) return false; + var targetRow = trigger instanceof HTMLElement ? trigger : null; + if (!targetRow && sidebarFileTreeSelection.focusedRowId) { + targetRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(sidebarFileTreeSelection.focusedRowId) + '"]'); + } + var targetDocumentId = fileTreeRowDocumentId(targetRow) || currentDocumentId(); + if (!targetDocumentId) return false; + var action = runtimeState.sidebarFileTreeClipboard.action === 'cut' ? 'move' : 'copy'; + var rows = fileTreeRowsByRowIds(runtimeState.sidebarFileTreeClipboard.rowIds); + if (rows.length === 0) return false; + var writable = await ensureFileTreeWritableTarget('paste', targetRow, runtimeState.sidebarFileTreeClipboard.rowIds, action === 'copy'); + if (!writable) return false; + recordFileTreeAction('paste', { + rowId: targetRow ? targetRow.getAttribute('data-row-id') || '' : '', + documentId: targetDocumentId, + sourceRowIds: runtimeState.sidebarFileTreeClipboard.rowIds, + clipboardAction: runtimeState.sidebarFileTreeClipboard.action + }); + var plan = buildSidebarFileTreeDeletePlan(rows); + var workspaceId = resolveWorkspaceId(targetRow || document.body); + var failures = []; + if (action === 'copy') { + dispatchSidebarEvent('tree.filetree.copy-requested', { rowIds: runtimeState.sidebarFileTreeClipboard.rowIds, targetDocumentId: targetDocumentId }); + recordFileTreeActionStatus('copy-requested', { documentId: targetDocumentId }); + return true; + } + for (var i = 0; i < plan.docRows.length; i += 1) { + var docRow = plan.docRows[i]; + var documentId = fileTreeRowDocumentId(docRow); + if (!documentId || documentId === targetDocumentId) continue; + try { + await dispatchTreeCommand(targetRow || docRow, { + action: 'move', + workspaceId: workspaceId, + documentId: documentId, + parentId: targetDocumentId, + sortOrder: fileTreeChildCount(targetDocumentId) + i + }); + } catch (error) { + failures.push(documentId + ': ' + (error && error.message ? error.message : '移动页面失败')); + } + } + var assetIds = plan.fileAssetRows.map(fileTreeRowAssetId).filter(Boolean); + if (assetIds.length > 0) { + try { + await postSidebarFileTreeJson('/api/media/batch', { + action: 'move', + assetIds: assetIds, + targetDocumentId: targetDocumentId + }); + } catch (error) { + failures.push(assetIds.join(',') + ': ' + (error && error.message ? error.message : '移动附件失败')); + } + } + if (failures.length > 0) { + recordFileTreeActionStatus('failed', { documentId: targetDocumentId, fallback: 'alert' }); + window.alert('部分对象移动失败:' + failures.join(';')); + return false; + } + runtimeState.sidebarFileTreeClipboard = null; + recordFileTreeActionStatus('applied', { documentId: targetDocumentId }); + return true; + } + + async function deleteSelectedSidebarFileTreeRows(trigger) { + var rows = selectedSidebarFileTreeRows(); + var plan = buildSidebarFileTreeDeletePlan(rows); + var total = plan.docRows.length + plan.folderRows.length + plan.fileAssetRows.length + plan.mindmapRows.length + plan.tableRows.length; + if (total === 0) return false; + if (!window.confirm(sidebarFileTreeDeleteConfirmText(plan))) return false; + recordFileTreeAction('bulk-delete', { rowId: trigger instanceof HTMLElement ? trigger.getAttribute('data-row-id') || '' : '', count: total }); + recordFileTreeActionStatus('pending', { count: total }); + var failures = []; + for (var i = 0; i < plan.docRows.length; i += 1) { + var docRow = plan.docRows[i]; + var documentId = fileTreeRowDocumentId(docRow); + try { + await dispatchTreeCommand(trigger || docRow, { + action: 'archive', + workspaceId: resolveWorkspaceId(docRow), + documentId: documentId + }); + applyRemoveDocumentDelta({ documentId: documentId }); + } catch (error) { + failures.push(documentId); + } + } + for (var fd = 0; fd < plan.folderRows.length; fd += 1) { + var folderRow = plan.folderRows[fd]; + var folderId = folderRow.getAttribute('data-row-id') || folderRow.getAttribute('data-node-id') || ''; + try { + await dispatchTreeCommand(trigger || folderRow, { + action: 'archive', + workspaceId: resolveWorkspaceId(folderRow), + documentId: folderId + }); + } catch (error) { + failures.push(folderId); + } + } + if (plan.fileAssetRows.length > 0) { + var fileAssetIds = plan.fileAssetRows.map(fileTreeRowAssetId).filter(Boolean); + try { + if (currentSourceKind() === 'local_folder') { + for (var lf = 0; lf < fileAssetIds.length; lf += 1) { + await dispatchTreeCommand(trigger || plan.fileAssetRows[lf], { + action: 'archive', + workspaceId: resolveWorkspaceId(plan.fileAssetRows[lf]), + documentId: fileAssetIds[lf] + }); + } + } else { + await postSidebarFileTreeJson('/api/media/batch', { action: 'delete', assetIds: fileAssetIds }); + } + fileAssetIds.forEach(removeFileTreeAssetRow); + } catch (error) { + failures = failures.concat(fileAssetIds); + } + } + for (var m = 0; m < plan.mindmapRows.length; m += 1) { + var mindmapRow = plan.mindmapRows[m]; + var mindmapId = fileTreeRowAssetId(mindmapRow); + try { + if (currentSourceKind() === 'local_folder') { + await dispatchTreeCommand(trigger || mindmapRow, { + action: 'archive', + workspaceId: resolveWorkspaceId(mindmapRow), + documentId: mindmapId + }); + } else { + var mindmapDocId = fileTreeRowDocumentId(mindmapRow); + var mindmapResponse = await fetch('/api/mindmap/' + encodeURIComponent(mindmapDocId) + '/' + encodeURIComponent(mindmapId), { method: 'DELETE' }); + if (!mindmapResponse.ok) throw new Error('mindmap_delete_failed_' + mindmapResponse.status); + } + removeFileTreeAssetRow(mindmapId); + } catch (error) { + failures.push(mindmapId); + } + } + for (var t = 0; t < plan.tableRows.length; t += 1) { + var tableRow = plan.tableRows[t]; + var tableId = fileTreeRowAssetId(tableRow); + try { + if (currentSourceKind() === 'local_folder') { + await dispatchTreeCommand(trigger || tableRow, { + action: 'archive', + workspaceId: resolveWorkspaceId(tableRow), + documentId: tableId + }); + } else { + var tableResponse = await fetch('/api/tables/' + encodeURIComponent(tableId), { method: 'DELETE' }); + if (!tableResponse.ok) throw new Error('table_delete_failed_' + tableResponse.status); + window.dispatchEvent(new CustomEvent('online-table-deleted', { detail: { tableId: tableId } })); + } + removeFileTreeAssetRow(tableId); + } catch (error) { + failures.push(tableId); + } + } + sidebarFileTreeSelection.selectedRowIds = new Set(); + sidebarFileTreeSelection.focusedRowId = null; + syncSidebarFileTreeSelection(); + if (failures.length > 0) { + recordFileTreeActionStatus('failed', { count: total, failures: failures.slice(0, 20), fallback: 'alert' }); + window.alert('部分对象删除失败:' + failures.slice(0, 5).join(', ') + (failures.length > 5 ? '…' : '')); + return false; + } + document.documentElement.setAttribute('data-mnote-filetree-bulk-delete-applied', 'true'); + recordFileTreeActionStatus('archived', { count: total, undo: 'trash-modal' }); + if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot(); + return true; + } + + + return { + rowTitle, + rowCenter, + renameFileTreeAsset, + beginFileTreeInlineRename, + closeTreeContextMenu, + copyTreeContextValue, + triggerBrowserDownload, + recordFileTreeAction, + recordFileTreeActionStatus, + documentHref, + convertToPreviousSiblingChild, + fileTreeCopyPath, + fileTreeMenuTargetParentId, + withOfficeEditModeGuard, + handleTreeContextMenuAction, + appendTreeContextMenuButton, + buildSidebarFileTreeContext, + evaluateSidebarFileTreeWhen, + openTreeContextMenu, + openPageTreeContextMenu, + openFileTreeContextMenu, + visibleFileTreeRows, + syncSidebarFileTreeSelection, + selectSidebarFileTreeRow, + selectSidebarFileTreeDocument, + selectSidebarFileTreeRowById, + pendingLocalFolderRestoreRowId, + clearPendingLocalFolderRestoreRowId, + applyPendingLocalFolderRestoreFocusOnce, + schedulePendingLocalFolderRestoreFocus, + selectedSidebarFileTreeRowIdsForDrag, + selectedSidebarFileTreeRows, + fileTreeRowDocumentId, + fileTreeRowAssetId, + decodeLocalEncodedPath, + fileTreeRowLocalRelativePath, + fileTreeRowLocalUploadTargetRelativePath, + fileTreeRowKind, + isFileTreeDownloadableAssetRow, + isFileTreeDownloadableRow, + fileTreeAssetDownloadDetail, + selectedSidebarFileTreeRowsForDownload, + selectedSidebarFileTreeAssetRowsForDownload, + downloadSelectedFileTreeAssetRows, + hasSelectedDocumentAncestor, + classifySidebarFileTreeAsset, + buildSidebarFileTreeDeletePlan, + sidebarFileTreeDeleteConfirmText, + postSidebarFileTreeJson, + fileTreeRowsByRowIds, + fileTreeChildCount, + pasteSidebarFileTreeClipboard, + deleteSelectedSidebarFileTreeRows, + }; +}; diff --git a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js index dc1a3d02..c310a428 100644 --- a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js @@ -2,6 +2,7 @@ import { createSidebarWorkspaceRuntime } from './sidebar-workspace-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'; +import { createSidebarFileTreeCommandRuntime } from './sidebar-filetree-command-runtime.js'; (function(){ if (window.__mnoteSidebarTreeRuntimeStarted) return; @@ -2232,1486 +2233,95 @@ import { createSidebarAttachmentOpenRuntime } from './sidebar-attachment-open-ru }); }, true); - function rowTitle(row) { - var title = row ? row.querySelector(':scope > .tree-link > .tree-link-title') : null; - return title && title.textContent ? title.textContent.trim() : '无标题'; - } - - function rowCenter(row) { - var rect = row.getBoundingClientRect(); - return { x: rect.left + Math.min(rect.width - 12, 180), y: rect.top + Math.min(rect.height, 22) }; - } - - async function renameFileTreeAsset(assetId, title) { - var response = await fetch('/api/media/batch', { - method: 'POST', - credentials: 'include', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ action: 'rename', assetIds: [assetId], newName: title }) - }); - var payload = await response.json().catch(function(){ return null; }); - if (!response.ok) throw new Error((payload && payload.error) || '重命名附件失败'); - return payload; - } - - function beginFileTreeInlineRename(row) { - if (!(row instanceof HTMLElement)) return false; - if (row.querySelector('.tree-rename-input')) return true; - var rowKind = row.getAttribute('data-row-kind') || ''; - var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || ''; - var assetId = row.getAttribute('data-asset-id') || ''; - if (!documentId && !assetId) return false; - if (rowKind && ['document', 'doc', 'index', 'markdown', 'asset'].indexOf(rowKind) < 0 && !assetId) return false; - var link = row.querySelector(':scope > .tree-link'); - var title = rowTitle(row); - if (!(link instanceof HTMLElement)) return false; - link.hidden = true; - var input = document.createElement('input'); - input.type = 'text'; - input.className = 'tree-rename-input'; - input.setAttribute('data-rename-id', row.getAttribute('data-row-id') || ''); - input.value = title; - input.style.minWidth = '0'; - input.style.flex = '1 1 auto'; - input.style.height = '22px'; - input.style.border = '1px solid #93c5fd'; - input.style.borderRadius = '3px'; - input.style.padding = '0 4px'; - input.style.font = 'inherit'; - input.style.background = '#fff'; - input.style.color = '#1f2937'; - var validation = document.createElement('div'); - validation.setAttribute('data-testid', 'tree-rename-validation'); - validation.setAttribute('data-mnote-rename-validation', 'true'); - validation.style.fontSize = '12px'; - validation.style.color = '#dc2626'; - validation.style.padding = '2px 4px'; - validation.hidden = true; - var setValidation = function(message) { - var text = String(message || '').trim(); - validation.textContent = text; - validation.hidden = !text; - input.setAttribute('aria-invalid', text ? 'true' : 'false'); - }; - var closed = false; - var committing = false; - var close = function() { - if (closed) return; - closed = true; - if (input.parentElement) input.parentElement.removeChild(input); - if (validation.parentElement) validation.parentElement.removeChild(validation); - link.hidden = false; - }; - var commit = function() { - if (closed || committing) return; - var nextTitle = input.value.trim(); - if (!nextTitle || nextTitle === title) { - close(); - return; - } - var validationMessage = validateFileTreeRename(row, nextTitle); - if (validationMessage) { - setValidation(validationMessage); - return; - } - setValidation(''); - var commandTitle = isFileTreePageRow(row) ? normalizeFileTreePageRenameTitle(nextTitle) : nextTitle; - committing = true; - input.disabled = true; - var work = assetId - ? Promise.resolve().then(function(){ - document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-asset-id="' + cssEscape(assetId) + '"] .tree-link-title').forEach(function(titleNode) { - titleNode.textContent = commandTitle; - }); - close(); - return renameFileTreeAsset(assetId, commandTitle); - }).then(function(){ return null; }) - : dispatchTreeCommand(row, { - action: 'rename', - workspaceId: resolveWorkspaceId(row), - documentId: documentId, - title: commandTitle - }).then(function(){ updateTitleEverywhere(documentId, commandTitle); }); - void work.then(close).catch(function(error) { - committing = false; - input.disabled = false; - window.alert(error && error.message ? error.message : '重命名失败'); - }); - }; - input.addEventListener('click', function(event) { event.stopPropagation(); }); - input.addEventListener('dblclick', function(event) { event.stopPropagation(); }); - input.addEventListener('keydown', function(event) { - event.stopPropagation(); - if (event.key === 'Enter') { - event.preventDefault(); - commit(); - } else if (event.key === 'Escape') { - event.preventDefault(); - close(); - } - }); - input.addEventListener('blur', commit); - link.parentElement.insertBefore(input, link.nextSibling); - link.parentElement.insertBefore(validation, input.nextSibling); - window.requestAnimationFrame(function() { - input.focus(); - input.select(); - }); - return true; - } - - function closeTreeContextMenu() { - if (activeTreeContextMenu && activeTreeContextMenu.parentElement) { - activeTreeContextMenu.parentElement.removeChild(activeTreeContextMenu); - } - activeTreeContextMenu = null; - } - - function copyTreeContextValue(value, actionName) { - var text = String(value || ''); - var done = function() { - document.documentElement.setAttribute('data-mnote-tree-context-last-copy', actionName || 'copy'); - }; - if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') { - return navigator.clipboard.writeText(text).then(done).catch(function(){}); - } - var textarea = document.createElement('textarea'); - textarea.value = text; - textarea.setAttribute('readonly', 'readonly'); - textarea.style.position = 'fixed'; - textarea.style.left = '-9999px'; - document.body.appendChild(textarea); - textarea.select(); - try { document.execCommand('copy'); } catch (_) {} - document.body.removeChild(textarea); - done(); - return Promise.resolve(); - } - - function triggerBrowserDownload(url) { - if (!url) return false; - var link = document.createElement('a'); - link.href = url; - link.target = '_blank'; - link.rel = 'noopener noreferrer'; - link.download = ''; - link.style.position = 'fixed'; - link.style.left = '-9999px'; - link.style.top = '0'; - document.body.appendChild(link); - try { - link.click(); - } catch (_) { - if (typeof window.open === 'function') window.open(url, '_blank', 'noopener,noreferrer'); - } - window.setTimeout(function() { - if (link.parentElement) link.parentElement.removeChild(link); - }, 1000); - return true; - } - - function recordFileTreeAction(action, detail) { - var normalized = String(action || '').trim() || 'unknown'; - var rowId = String(detail && detail.rowId || '').trim(); - var documentId = String(detail && detail.documentId || '').trim(); - var assetId = String(detail && detail.assetId || '').trim(); - document.documentElement.setAttribute('data-mnote-filetree-last-action', normalized); - if (rowId) document.documentElement.setAttribute('data-mnote-filetree-last-action-row-id', rowId); - if (documentId) document.documentElement.setAttribute('data-mnote-filetree-last-action-document-id', documentId); - if (assetId) document.documentElement.setAttribute('data-mnote-filetree-last-action-asset-id', assetId); - window.dispatchEvent(new CustomEvent('tree.filetree.action', { - detail: Object.assign({}, detail || {}, { action: normalized }) - })); - } - - function recordFileTreeActionStatus(status, detail) { - var normalized = String(status || '').trim() || 'unknown'; - document.documentElement.setAttribute('data-mnote-filetree-last-action-status', normalized); - window.dispatchEvent(new CustomEvent('tree.filetree.action.status', { - detail: Object.assign({}, detail || {}, { status: normalized }) - })); - } - - function documentHref(documentId, workspaceId) { - var url = new URL('/documents/' + encodeURIComponent(documentId), window.location.origin); - if (workspaceId) url.searchParams.set('workspaceId', workspaceId); - copyWorkspaceSourceParams(url); - return url.toString(); - } - - function convertToPreviousSiblingChild(trigger, detail) { - var documentId = detail.documentId || ''; - var row = document.querySelector('#sidebar-tree-root .tree-row[data-node-id="' + cssEscape(documentId) + '"]'); - if (!(row instanceof HTMLElement)) return; - var parentId = row.getAttribute('data-parent-id') || ''; - var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(candidate) { - return (candidate.getAttribute('data-parent-id') || '') === parentId; - }); - var index = siblings.indexOf(row); - if (index <= 0) { - window.alert('当前页面前面没有同级页面。'); - return; - } - var previous = siblings[index - 1]; - var previousId = previous.getAttribute('data-node-id') || ''; - var children = previous.parentElement ? previous.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : []; - void dispatchTreeCommand(trigger || row, { - action: 'move', - workspaceId: detail.workspaceId || resolveWorkspaceId(row), - documentId: documentId, - parentId: previousId, - sortOrder: children.length - }); - } - - function fileTreeCopyPath(detail, trigger) { - // 对 local_folder 复制真实相对路径,不复制标题 - if (currentSourceKind() === 'local_folder') { - if (detail.assetId) { - var path = localFilePathFromAssetId(detail.assetId); - if (path) { - try { - return decodeURIComponent(path.replace(/~2F/g, '/')); - } catch (_) { - return path.replace(/~2F/g, '/'); - } - } - } - if (detail.documentId) { - var docPath = String(detail.documentId || '') - .replace(/^local-md:/, '') - .replace(/^local-dir:/, '') - .replace(/~2F/g, '/'); - if (docPath) { - try { - return decodeURIComponent(docPath); - } catch (_) { - return docPath; - } - } - } - } - return detail.title || ''; - } - - function fileTreeMenuTargetParentId(detail, trigger) { - var rowKind = String(detail && detail.rowKind || '').trim(); - var rowId = String(detail && detail.rowId || '').trim(); - var documentId = String(detail && detail.documentId || '').trim(); - if (rowKind === 'folder' && rowId) return rowId; - if (rowKind === 'directory' && rowId) return rowId; - if (documentId) return documentId; - if (trigger && typeof trigger.getAttribute === 'function') { - var triggerKind = String(trigger.getAttribute('data-row-kind') || '').trim(); - var triggerRowId = String(trigger.getAttribute('data-row-id') || '').trim(); - if ((triggerKind === 'folder' || triggerKind === 'directory') && triggerRowId) return triggerRowId; - } - return ''; - } - - function withOfficeEditModeGuard(callback) { - document.documentElement.setAttribute('data-mnote-last-office-edit-mode-requested', 'true'); - document.documentElement.setAttribute('data-mnote-last-office-edit-mode-guard', 'silent'); - callback(); - } - - function handleTreeContextMenuAction(action, detail, trigger) { - closeTreeContextMenu(); - detail = detail || {}; - if (detail.contextKind === 'attachment') { - if (action === 'copy-link') { - void copyTreeContextValue(detail.href || '', 'attachment-copy-link'); - return; - } - if (action === 'download') { - openEditorAttachmentDownload(detail); - return; - } - if (action === 'popup-preview') { - openEditorAttachmentDetail(detail); - return; - } - if (action === 'new-window') { - openEditorAttachmentNewWindow(detail); - return; - } - if (action === 'new-window-edit') { - withOfficeEditModeGuard(function() { - openEditorAttachmentNewWindow(detail, 'edit'); - }); - return; - } - if (action === 'open-edit-mode') { - withOfficeEditModeGuard(function() { - void openEditorAttachmentEditTab(detail); - }); - return; - } - if (action === 'right-preview') { - dispatchSidebarEvent('tree.attachment.open-right', detail); - return; - } - if (action === 'copy-id') { - void copyTreeContextValue(detail.assetId || '', 'attachment-copy-id'); - return; - } - dispatchSidebarEvent('tree.attachment.action', { action: action, attachment: detail }); - return; - } - var documentId = detail.documentId || ''; - var workspaceId = detail.workspaceId || resolveWorkspaceId(trigger || document.body); - var title = detail.title || '无标题'; - var isAsset = detail.contextKind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index'; - if (detail.contextKind === 'filetree' && action === 'download') { - downloadSelectedFileTreeAssetRows(detail, trigger); - return; - } - if (isAsset && action === 'new-window') { - recordFileTreeAction('new-window', detail); - void openConvexAssetFromFileTree({ ...detail, openTarget: 'new-window' }); - return; - } - if (isAsset && action === 'open-edit-mode') { - withOfficeEditModeGuard(function() { - recordFileTreeAction('open-edit-mode', detail); - void openConvexAssetFromFileTree({ ...detail, openTarget: 'edit-mode' }); - }); - return; - } - if (isAsset && action === 'open-right') { - recordFileTreeAction('open-right', detail); - void openConvexAssetFromFileTree({ ...detail, openTarget: 'side' }); - return; - } - if (action === 'open-right') { - recordFileTreeAction('open-right', detail); - dispatchSidebarEvent('tree.page.open-right', detail); - return; - } - if (action === 'share') { - dispatchSidebarEvent('tree.page.share', detail); - return; - } - if (action === 'copy-link') { - void copyTreeContextValue(documentHref(documentId, workspaceId), 'copy-link'); - return; - } - if (action === 'copy-link-title') { - void copyTreeContextValue(title + ' ' + documentHref(documentId, workspaceId), 'copy-link-title'); - return; - } - if (action === 'copy-reference-inline') { - void copyTreeContextValue('((' + title + ' ' + documentId + '))', 'copy-reference-inline'); - return; - } - if (action === 'copy-reference-embed') { - void copyTreeContextValue('{{' + title + ' ' + documentId + '}}', 'copy-reference-embed'); - return; - } - if (action === 'copy-id') { - void copyTreeContextValue(documentId || detail.assetId || detail.rowId || '', 'copy-id'); - return; - } - if (action === 'delete-trash' && detail.contextKind === 'filetree') { - var selectedFileTreeRows = selectedSidebarFileTreeRows(); - if (selectedFileTreeRows.length > 1) { - void deleteSelectedSidebarFileTreeRows(trigger || document.body); - return; - } - } - if (action === 'delete-trash' && isAsset) { - if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return; - recordFileTreeAction('delete-trash', detail); - recordFileTreeActionStatus('pending', detail); - void deleteSingleFileTreeAsset(detail, trigger).then(function() { - recordFileTreeActionStatus('archived', Object.assign({}, detail, { undo: 'trash-modal' })); - if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot(); - }).catch(function(error) { - recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); - window.alert(error && error.message ? error.message : '资源删除失败'); - }); - return; - } - if (action === 'new-file') { - void createPage(trigger || document.body, fileTreeMenuTargetParentId(detail, trigger) || null); - return; - } - if (action === 'new-folder') { - recordFileTreeAction('new-folder', detail); - void createFileTreeFolder(trigger || document.body, fileTreeMenuTargetParentId(detail, trigger) || null).then(function(ok) { - recordFileTreeActionStatus(ok ? 'created' : 'skipped', detail); - }).catch(function(error) { - recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); - window.alert(error && error.message ? error.message : '新建文件夹失败'); - }); - return; - } - if (action === 'paste-into') { - var pasteRow = trigger && trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger; - recordFileTreeAction('paste-into', detail); - void pasteSidebarFileTreeClipboard(pasteRow).then(function(ok) { - recordFileTreeActionStatus(ok ? 'applied' : 'skipped', detail); - }).catch(function(error) { - recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); - window.alert(error && error.message ? error.message : '粘贴失败'); - }); - return; - } - if (action === 'copy-path') { - void copyTreeContextValue(fileTreeCopyPath(detail, trigger), 'copy-path'); - return; - } - if (action === 'refresh') { - document.documentElement.setAttribute('data-mnote-filetree-refresh-requested', 'true'); - dispatchSidebarEvent('tree.filetree.refresh', detail); - return; - } - if (action === 'collapse-all') { - document.querySelectorAll('#sidebar-file-tree-root .tree-row[aria-expanded="true"]').forEach(function(row) { - if (!(row instanceof HTMLElement)) return; - row.setAttribute('aria-expanded', 'false'); - var node = row.closest('.tree-node'); - var children = node ? node.querySelector(':scope > .tree-children') : null; - if (children) children.classList.add('tree-children--collapsed'); - }); - return; - } - if (action === 'reveal') { - if (trigger && typeof trigger.scrollIntoView === 'function') { - trigger.scrollIntoView({ block: 'nearest' }); - trigger.focus && trigger.focus(); - } - return; - } - if (action === 'duplicate') { - dispatchSidebarEvent('tree.page.duplicate', detail); - return; - } - if (action === 'rename') { - if (detail.contextKind === 'filetree' && trigger) { - var renameRow = trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger; - if (beginFileTreeInlineRename(renameRow)) return; - } - var nextTitle = window.prompt('重命名页面', title); - if (nextTitle && nextTitle.trim() && documentId) { - void dispatchTreeCommand(trigger || document.body, { - action: 'rename', - workspaceId: workspaceId, - documentId: documentId, - title: nextTitle.trim() - }).then(function(){ updateTitleEverywhere(documentId, nextTitle.trim()); }); - } - return; - } - if (action === 'create-child') { - void createPage(trigger || document.body, documentId); - return; - } - if (action === 'convert-child') { - convertToPreviousSiblingChild(trigger, detail); - return; - } - var deleteTargetId = documentId || String(detail.rowId || '').trim(); - if (action === 'delete-trash' && deleteTargetId) { - if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return; - recordFileTreeAction('delete-trash', detail); - recordFileTreeActionStatus('pending', detail); - void dispatchTreeCommand(trigger || document.body, { - action: 'archive', - workspaceId: workspaceId, - documentId: deleteTargetId - }).then(function() { - recordFileTreeActionStatus('archived', Object.assign({}, detail, { undo: 'trash-modal' })); - if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot(); - }).catch(function(error) { - recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); - window.alert(error && error.message ? error.message : '删除失败'); - }); - } - } - - function appendTreeContextMenuButton(menu, item, detail, trigger) { - if (item.separator) { - var sep = document.createElement('div'); - sep.className = 'mnote-tree-context-menu__separator'; - sep.setAttribute('role', 'separator'); - menu.appendChild(sep); - return; - } - var button = document.createElement('button'); - button.type = 'button'; - button.className = item.danger ? 'mnote-tree-context-menu__item mnote-tree-context-menu__item--danger' : 'mnote-tree-context-menu__item'; - button.setAttribute('role', 'menuitem'); - button.setAttribute('data-action', item.action); - button.disabled = item.disabled === true; - if (item.title) button.title = item.title; - var icon = document.createElement('span'); - icon.className = 'material-symbols-outlined mnote-tree-context-menu__icon'; - icon.setAttribute('aria-hidden', 'true'); - icon.setAttribute('data-icon', item.icon || 'radio_button_unchecked'); - var label = document.createElement('span'); - label.className = 'mnote-tree-context-menu__label'; - label.textContent = item.label; - button.appendChild(icon); - button.appendChild(label); - if (item.shortcut) { - var shortcut = document.createElement('span'); - shortcut.className = 'mnote-tree-context-menu__shortcut'; - shortcut.textContent = item.shortcut; - button.appendChild(shortcut); - } - button.addEventListener('click', function(event) { - event.preventDefault(); - event.stopPropagation(); - handleTreeContextMenuAction(item.action, detail, trigger); - }); - menu.appendChild(button); - } - - // ── CommandContext 启用态辅助:与 Rust core-protocol CommandContext 保持同一口径 ── - - function buildSidebarFileTreeContext(kind) { - var runtime = window.__mnoteFileTreeContextMenuRuntime; - if (runtime && typeof runtime.buildSidebarFileTreeContext === 'function') { - try { - return runtime.buildSidebarFileTreeContext(kind, { - selection: sidebarFileTreeSelection, - currentSourceKind: currentSourceKind, - workspaceReadonly: function() { - return document.documentElement.getAttribute('data-mnote-workspace-readonly') === 'true'; - }, - queryRowById: function(rowId) { - return document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]'); - } - }); - } catch (_) {} - } - var s = sidebarFileTreeSelection; - var rowIds = Array.from(s.selectedRowIds || []); - var rows = []; - for (var i = 0; i < rowIds.length; i += 1) { - var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowIds[i]) + '"]'); - if (row instanceof HTMLElement) rows.push(row); - } - var resourceKinds = {}; - for (var i = 0; i < rows.length; i += 1) { - var rk = rows[i].getAttribute('data-row-kind') || 'unknown'; - resourceKinds[rk] = true; - } - var rkKeys = Object.keys(resourceKinds); - return { - 'workspace.sourceKind': currentSourceKind() || '', - 'workspace.readonly': document.documentElement.getAttribute('data-mnote-workspace-readonly') === 'true', - 'tree.focusKind': kind === 'filetree' ? 'file_tree' : kind === 'page' ? 'page_tree' : kind, - 'tree.selectionCount': rows.length, - 'tree.selectionResourceKind': rkKeys.length === 1 ? rkKeys[0] : 'mixed' - }; - } - - function evaluateSidebarFileTreeWhen(ctx, expr) { - var runtime = window.__mnoteFileTreeContextMenuRuntime; - if (runtime && typeof runtime.evaluateSidebarFileTreeWhen === 'function') { - return runtime.evaluateSidebarFileTreeWhen(ctx || {}, expr || ''); - } - if (!expr) return true; - try { - expr = expr.trim(); - // 词法切分:支持 key、!key、==、!=、&&、|| 和括号分组 - var tokens = []; - var i = 0; - while (i < expr.length) { - var ch = expr[i]; - if (ch === ' ' || ch === '\t') { i++; continue; } - if (ch === '(') { tokens.push({ t: '(' }); i++; continue; } - if (ch === ')') { tokens.push({ t: ')' }); i++; continue; } - if (ch === '&' && expr[i+1] === '&') { tokens.push({ t: '&&' }); i += 2; continue; } - if (ch === '|' && expr[i+1] === '|') { tokens.push({ t: '||' }); i += 2; continue; } - if (ch === '=' && expr[i+1] === '=') { tokens.push({ t: '==' }); i += 2; continue; } - if (ch === '!' && expr[i+1] === '=') { tokens.push({ t: '!=' }); i += 2; continue; } - if (ch === '!') { tokens.push({ t: '!' }); i++; continue; } - if (ch === '"' || ch === "'") { - i++; - var str = ''; - while (i < expr.length && expr[i] !== ch) { str += expr[i]; i++; } - if (i < expr.length) i++; - tokens.push({ t: 'str', v: str }); - continue; - } - if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '.' || (ch >= '0' && ch <= '9')) { - var id = ''; - while (i < expr.length && ((expr[i] >= 'a' && expr[i] <= 'z') || (expr[i] >= 'A' && expr[i] <= 'Z') || (expr[i] >= '0' && expr[i] <= '9') || expr[i] === '_' || expr[i] === '.')) { id += expr[i]; i++; } - tokens.push({ t: 'id', v: id }); - continue; - } - throw new Error('Unexpected char: ' + ch); - } - var pos = 0; - function peek() { return tokens[pos]; } - function consume() { return tokens[pos++]; } - function expect(t) { - var tok = consume(); - if (!tok || tok.t !== t) throw new Error('Expected ' + t + ' got ' + (tok ? tok.t : 'EOF')); - return tok; - } - function ctxVal(key) { - var v = ctx[key]; - if (v === undefined || v === null) return ''; - return v; - } - function parseOr() { - var left = parseAnd(); - while (peek() && peek().t === '||') { consume(); var right = parseAnd(); left = left || right; } - return left; - } - function parseAnd() { - var left = parsePrimary(); - while (peek() && peek().t === '&&') { consume(); var right = parsePrimary(); left = left && right; } - return left; - } - function parsePrimary() { - if (!peek()) throw new Error('Unexpected EOF'); - if (peek().t === '(') { - consume(); - var val = parseOr(); - expect(')'); - return val; - } - if (peek().t === '!') { - consume(); - var keyTok = consume(); - if (!keyTok || keyTok.t !== 'id') throw new Error('Expected key after !'); - return !ctxVal(keyTok.v); - } - if (peek().t === 'id') { - var keyTok = consume(); - if (peek() && peek().t === '==') { - consume(); - var valTok = consume(); - if (!valTok) throw new Error('Expected value after =='); - var val = valTok.t === 'str' ? valTok.v : (valTok.v || ''); - // 与 Rust evaluate_when 保持一致:按 string / bool / number 比较。 - var cv = ctxVal(keyTok.v); - if (val === 'true') return cv === true || cv === 'true'; - if (val === 'false') return cv === false || cv === '' || cv === 'false'; - var num = Number(val); - if (!isNaN(num) && String(num) === val) return Number(cv) === num; - return String(cv) === val; - } - if (peek() && peek().t === '!=') { - consume(); - var valTok = consume(); - if (!valTok) throw new Error('Expected value after !='); - var val = valTok.t === 'str' ? valTok.v : (valTok.v || ''); - var cv = ctxVal(keyTok.v); - if (val === 'true') return !(cv === true || cv === 'true'); - if (val === 'false') return !(cv === false || cv === '' || cv === 'false'); - var num = Number(val); - if (!isNaN(num) && String(num) === val) return Number(cv) !== num; - return String(cv) !== val; - } - // 单独 key:按 truthy 规则判断。 - var cv = ctxVal(keyTok.v); - if (typeof cv === 'boolean') return cv; - if (typeof cv === 'number') return cv !== 0; - return cv !== '' && cv !== undefined && cv !== null; - } - throw new Error('Unexpected token: ' + peek().t); - } - return parseOr(); - } catch(e) { - return true; - } - } - - function openTreeContextMenu(kind, detail, x, y, trigger) { - closeTreeContextMenu(); - detail = Object.assign({}, detail || {}, { contextKind: kind }); - var menu = document.createElement('div'); - menu.className = 'mnote-tree-context-menu'; - menu.setAttribute('role', 'menu'); - menu.setAttribute('data-testid', 'mnote-tree-context-menu'); - menu.setAttribute('data-kind', kind); - var isAttachment = kind === 'attachment'; - var isAsset = kind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index'; - var isFileTreeDownload = kind === 'filetree' && detail.downloadable; - var ctx = buildSidebarFileTreeContext(kind); - var items = isAttachment ? [ - { action: 'duplicate', icon: 'file_copy', label: '拷贝副本', shortcut: 'Ctrl + D' }, - { action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' }, - { separator: true }, - { action: 'copy-link', icon: 'link', label: '复制链接' }, - { action: 'move-embed', icon: 'subdirectory_arrow_right', label: '移动/嵌入到...', shortcut: 'Alt+Shift+M/G' }, - { action: 'history', icon: 'history', label: '块历史...' }, - { separator: true }, - { action: 'popup-preview', icon: 'preview', label: '弹窗预览' }, - { action: 'right-preview', icon: 'right_panel_open', label: '右侧预览' }, - { action: 'open-edit-mode', icon: 'edit_note', label: '弹窗编辑' }, - { action: 'new-window-edit', icon: 'open_in_new', label: '新窗口编辑' }, - { action: 'new-window', icon: 'open_in_new', label: '在新窗口打开' }, - { action: 'download', icon: 'download', label: '下载' }, - { action: 'replace-file', icon: 'sync', label: '更换文件' }, - { action: 'rename-attachment', icon: 'drive_file_rename_outline', label: '重命名' }, - { action: 'comment', icon: 'mode_comment', label: '评论', shortcut: 'Ctrl+Alt+M' }, - { action: 'caption', icon: 'notes', label: '添加说明文字' }, - { separator: true }, - { action: 'color', icon: 'format_paint', label: '颜色' } - ] : isAsset ? [ - { action: 'open-edit-mode', icon: 'edit_note', label: '使用编辑模式打开' }, - { action: 'new-window', icon: 'open_in_new', label: '在新窗口打开' }, - { action: 'download', icon: 'download', label: Number(detail.selectedDownloadCount || detail.selectedAssetCount || 0) > 1 ? '下载 ' + Number(detail.selectedDownloadCount || detail.selectedAssetCount || 0) + ' 个项目' : '下载' }, - { action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2', when: '!workspace.readonly' }, - { action: 'copy-id', icon: 'tag', label: '复制资源 ID' }, - { action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' }, - { separator: true }, - { action: 'copy-path', icon: 'content_copy', label: 'Copy Path' }, - { action: 'refresh', icon: 'refresh', label: 'Refresh' }, - { action: 'reveal', icon: 'my_location', label: 'Reveal' } - ] : kind === 'filetree' ? [ - { action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' }, - { action: 'download', icon: 'download', label: Number(detail.selectedDownloadCount || 0) > 1 ? '下载 ' + Number(detail.selectedDownloadCount || 0) + ' 个项目' : '下载', disabled: !isFileTreeDownload, title: isFileTreeDownload ? '下载当前本地文件或文件夹' : '当前项目没有可下载的本地路径' }, - { separator: true }, - { action: 'copy-link', icon: 'link', label: '复制访问链接' }, - { action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' }, - { action: 'copy-id', icon: 'tag', label: '复制页面ID' }, - { action: 'copy-path', icon: 'content_copy', label: 'Copy Path' }, - { separator: true }, - { action: 'new-file', icon: 'note_add', label: 'New File', when: '!workspace.readonly' }, - { action: 'new-folder', icon: 'create_new_folder', label: 'New Folder', disabled: currentSourceKind() !== 'local_folder', title: currentSourceKind() === 'local_folder' ? '在当前目录下创建子文件夹' : '仅 local folder 支持创建文件夹', when: '!workspace.readonly' }, - { action: 'paste-into', icon: 'content_paste', label: '粘贴到此处', disabled: !sidebarFileTreeClipboard, title: sidebarFileTreeClipboard ? '粘贴到当前文件树目标' : '剪贴板为空', when: '!workspace.readonly' }, - { action: 'refresh', icon: 'refresh', label: 'Refresh' }, - { action: 'collapse-all', icon: 'unfold_less', label: 'Collapse All' }, - { action: 'reveal', icon: 'my_location', label: 'Reveal' }, - { separator: true }, - { action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2', when: '!workspace.readonly' }, - { separator: true }, - { action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' } - ] : [ - { action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' }, - { separator: true }, - { action: 'copy-link', icon: 'link', label: '复制访问链接' }, - { action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' }, - { action: 'copy-id', icon: 'tag', label: '复制页面ID' }, - { separator: true }, - { action: 'rename', icon: 'edit', label: '重命名', when: '!workspace.readonly' }, - { separator: true }, - { action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' } - ]; - items.forEach(function(item) { - if (item.when !== undefined && !evaluateSidebarFileTreeWhen(ctx, item.when)) { - item = Object.assign({}, item, { disabled: true, title: item.title || '当前上下文不支持此操作' }); - } - appendTreeContextMenuButton(menu, item, detail, trigger); - }); - document.body.appendChild(menu); - var rect = menu.getBoundingClientRect(); - var left = Math.min(Math.max(8, x || 8), Math.max(8, window.innerWidth - rect.width - 8)); - var top = Math.min(Math.max(8, y || 8), Math.max(8, window.innerHeight - rect.height - 8)); - menu.style.left = left + 'px'; - menu.style.top = top + 'px'; - activeTreeContextMenu = menu; - } - - function openPageTreeContextMenu(row, x, y, trigger) { - if (!(row instanceof HTMLElement)) return; - var documentId = row.getAttribute('data-node-id') || ''; - openTreeContextMenu('page', { - documentId: documentId, - rowId: documentId, - rowKind: 'document', - title: rowTitle(row), - workspaceId: resolveWorkspaceId(row) - }, x, y, trigger || row); - } - - function openFileTreeContextMenu(row, x, y, trigger) { - if (!(row instanceof HTMLElement)) return; - var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || ''; - var selectedDownloadRows = selectedSidebarFileTreeRowsForDownload(row); - var selectedAssetRows = selectedDownloadRows.filter(isFileTreeDownloadableAssetRow); - openTreeContextMenu('filetree', { - documentId: documentId, - rowId: row.getAttribute('data-row-id') || '', - rowKind: row.getAttribute('data-row-kind') || '', - assetId: row.getAttribute('data-asset-id') || '', - localRelativePath: fileTreeRowLocalRelativePath(row), - title: rowTitle(row), - workspaceId: resolveWorkspaceId(row), - downloadable: isFileTreeDownloadableRow(row), - selectedDownloadCount: selectedDownloadRows.length, - selectedDownloadRowIds: selectedDownloadRows.map(function(downloadRow) { return downloadRow.getAttribute('data-row-id') || ''; }).filter(Boolean), - selectedAssetCount: selectedAssetRows.length, - selectedAssetRowIds: selectedAssetRows.map(function(assetRow) { return assetRow.getAttribute('data-row-id') || ''; }).filter(Boolean) - }, x, y, trigger || row); - } - - function visibleFileTreeRows() { - var runtimeFn = fileTreeSelectionRuntimeFunction('visibleFileTreeRows'); - if (runtimeFn) return runtimeFn(fileTreeSelectionRuntimeDeps()); - return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]')) - .filter(function(row) { - if (!(row instanceof HTMLElement)) return false; - if (row.closest('.tree-children--collapsed')) return false; - return row.offsetParent !== null || row.getClientRects().length > 0; - }); - } - - function syncSidebarFileTreeSelection() { - var runtimeFn = fileTreeSelectionRuntimeFunction('syncSidebarFileTreeSelection'); - if (runtimeFn) { - runtimeFn(sidebarFileTreeSelection, fileTreeSelectionRuntimeDeps()); - return; - } - var rows = visibleFileTreeRows(); - rows.forEach(function(row) { - var rowId = row.getAttribute('data-row-id') || ''; - row.setAttribute('data-selected', String(Boolean(rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId)))); - row.setAttribute('data-focused', String(rowId === sidebarFileTreeSelection.focusedRowId)); - }); - window.dispatchEvent(new CustomEvent('tree.filetree.selection.changed', { - detail: { - selectedRowIds: Array.from(sidebarFileTreeSelection.selectedRowIds), - anchorRowId: sidebarFileTreeSelection.anchorRowId, - focusedRowId: sidebarFileTreeSelection.focusedRowId - } - })); - } - - function selectSidebarFileTreeRow(row, modifiers) { - var runtimeFn = fileTreeSelectionRuntimeFunction('selectSidebarFileTreeRow'); - if (runtimeFn) return runtimeFn(row, sidebarFileTreeSelection, modifiers, fileTreeSelectionRuntimeDeps()); - if (!(row instanceof HTMLElement)) return []; - var rowId = row.getAttribute('data-row-id') || ''; - if (!rowId) return []; - var rows = visibleFileTreeRows(); - var visibleRowIds = rows.map(function(item) { return item.getAttribute('data-row-id') || ''; }).filter(Boolean); - var selected = new Set(sidebarFileTreeSelection.selectedRowIds); - var shiftKey = Boolean(modifiers && modifiers.shiftKey); - var ctrlKey = Boolean(modifiers && (modifiers.ctrlKey || modifiers.metaKey)); - if (shiftKey && sidebarFileTreeSelection.anchorRowId) { - var anchorIndex = visibleRowIds.indexOf(sidebarFileTreeSelection.anchorRowId); - var targetIndex = visibleRowIds.indexOf(rowId); - if (anchorIndex >= 0 && targetIndex >= 0) { - selected = new Set(visibleRowIds.slice(Math.min(anchorIndex, targetIndex), Math.max(anchorIndex, targetIndex) + 1)); - } else { - selected = new Set([rowId]); - sidebarFileTreeSelection.anchorRowId = rowId; - } - } else if (ctrlKey) { - if (selected.has(rowId) && selected.size > 1) selected.delete(rowId); - else selected.add(rowId); - sidebarFileTreeSelection.anchorRowId = rowId; - } else { - selected = new Set([rowId]); - sidebarFileTreeSelection.anchorRowId = rowId; - } - sidebarFileTreeSelection.selectedRowIds = selected; - sidebarFileTreeSelection.focusedRowId = rowId; - syncSidebarFileTreeSelection(); - return Array.from(selected); - } - - function selectSidebarFileTreeDocument(documentId, options) { - var id = String(documentId || '').trim(); - if (!id) return false; - var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + id) + '"]') - || document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-document-id="' + cssEscape(id) + '"]') - || document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-doc-id="' + cssEscape(id) + '"]'); - if (!(row instanceof HTMLElement)) return false; - var rowId = row.getAttribute('data-row-id') || ''; - if (!rowId) return false; - selectSidebarFileTreeRow(row, { ctrlKey: false, metaKey: false, shiftKey: false }); - document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-active="true"]').forEach(function(activeRow) { - if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'false'); - }); - row.setAttribute('data-active', 'true'); - if (options && options.scrollIntoView !== false) { - try { - row.scrollIntoView({ block: 'nearest' }); - } catch (_) { - row.scrollIntoView(); - } - } - return true; - } - - function selectSidebarFileTreeRowById(rowId, options) { - var id = String(rowId || '').trim(); - if (!id) return false; - var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(id) + '"]'); - if (!(row instanceof HTMLElement)) return false; - selectSidebarFileTreeRow(row, { ctrlKey: false, metaKey: false, shiftKey: false }); - document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-active="true"]').forEach(function(activeRow) { - if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'false'); - }); - row.setAttribute('data-active', 'true'); - if (options && options.scrollIntoView !== false) { - try { - row.scrollIntoView({ block: 'nearest' }); - } catch (_) { - row.scrollIntoView(); - } - } - return true; - } - - function pendingLocalFolderRestoreRowId() { - if (window.__mnotePendingLocalFolderRestoreFiletreeRowId) { - return String(window.__mnotePendingLocalFolderRestoreFiletreeRowId || '').trim(); - } - if (!window.localStorage) return ''; - var rowId = ''; - try { - rowId = String(window.localStorage.getItem('mnote.pendingLocalFolderRestoreFiletreeRowId') || '').trim(); - } catch (_) { - rowId = ''; - } - if (!rowId && typeof window.name === 'string' && window.name.indexOf('mnote.pendingLocalFolderRestoreFiletreeRowId=') === 0) { - rowId = decodeURIComponent(window.name.slice('mnote.pendingLocalFolderRestoreFiletreeRowId='.length)); - } - if (rowId) window.__mnotePendingLocalFolderRestoreFiletreeRowId = rowId; - return rowId; - } - - function clearPendingLocalFolderRestoreRowId() { - window.__mnotePendingLocalFolderRestoreFiletreeRowId = ''; - try { - window.localStorage.removeItem('mnote.pendingLocalFolderRestoreFiletreeRowId'); - } catch (_) {} - if (typeof window.name === 'string' && window.name.indexOf('mnote.pendingLocalFolderRestoreFiletreeRowId=') === 0) { - window.name = ''; - } - } - - function applyPendingLocalFolderRestoreFocusOnce() { - var rowId = pendingLocalFolderRestoreRowId(); - if (!rowId) return false; - document.documentElement.setAttribute('data-mnote-local-folder-restore-pending-row-id', rowId); - if (!selectSidebarFileTreeRowById(rowId, { scrollIntoView: true })) { - document.documentElement.setAttribute('data-mnote-local-folder-restore-focus-status', 'row-missing'); - return false; - } - document.documentElement.setAttribute('data-mnote-local-folder-restore-focused-row-id', rowId); - document.documentElement.setAttribute('data-mnote-local-folder-restore-focus-status', 'focused'); - return true; - } - - function schedulePendingLocalFolderRestoreFocus() { - if (!pendingLocalFolderRestoreRowId()) return false; - if (window.__mnotePendingLocalFolderRestoreFocusTimer) return false; - var attempts = 0; - var focused = false; - focused = applyPendingLocalFolderRestoreFocusOnce() || focused; - window.__mnotePendingLocalFolderRestoreFocusTimer = window.setInterval(function() { - attempts += 1; - focused = applyPendingLocalFolderRestoreFocusOnce() || focused; - if (attempts >= 20) { - if (focused) clearPendingLocalFolderRestoreRowId(); - else document.documentElement.setAttribute('data-mnote-local-folder-restore-focus-status', 'timeout'); - window.clearInterval(window.__mnotePendingLocalFolderRestoreFocusTimer); - window.__mnotePendingLocalFolderRestoreFocusTimer = 0; - } - }, 250); - return focused; - } - - function selectedSidebarFileTreeRowIdsForDrag(row) { - var runtimeFn = fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRowIdsForDrag'); - if (runtimeFn) return runtimeFn(row, sidebarFileTreeSelection); - var rowId = row instanceof HTMLElement ? row.getAttribute('data-row-id') || '' : ''; - if (rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId)) { - return Array.from(sidebarFileTreeSelection.selectedRowIds); - } - return rowId ? [rowId] : []; - } - - function selectedSidebarFileTreeRows() { - var runtimeFn = fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRows'); - if (runtimeFn) return runtimeFn(sidebarFileTreeSelection, fileTreeSelectionRuntimeDeps()); - var selectedIds = sidebarFileTreeSelection.selectedRowIds; - var rows = visibleFileTreeRows().filter(function(row) { - var rowId = row.getAttribute('data-row-id') || ''; - return rowId && selectedIds.has(rowId); - }); - if (rows.length > 0) return rows; - if (sidebarFileTreeSelection.focusedRowId) { - var focused = document.querySelector( - '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(sidebarFileTreeSelection.focusedRowId) + '"]' - ); - if (focused instanceof HTMLElement) return [focused]; - } - return []; - } - - function fileTreeRowDocumentId(row) { - var runtimeFn = fileTreeRuntimeFunction('fileTreeRowDocumentId'); - if (runtimeFn) return runtimeFn(row); - if (!(row instanceof HTMLElement)) return ''; - return String(row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '').trim(); - } - - function fileTreeRowAssetId(row) { - var runtimeFn = fileTreeRuntimeFunction('fileTreeRowAssetId'); - if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); - if (!(row instanceof HTMLElement)) return ''; - var assetId = String(row.getAttribute('data-asset-id') || '').trim(); - if (assetId) return assetId; - if (currentSourceKind() === 'local_folder' && fileTreeRowKind(row) === 'asset') { - return String(row.getAttribute('data-row-id') || '').trim(); - } - return ''; - } - - function decodeLocalEncodedPath(value) { - var runtimeFn = fileTreeRuntimeFunction('decodeLocalEncodedPath'); - if (runtimeFn) return runtimeFn(value); - var path = String(value || '').trim().replace(/~2F/g, '/'); - if (!path) return ''; - try { - return decodeURIComponent(path); - } catch (_) { - return path; - } - } - - function fileTreeRowLocalRelativePath(row) { - var runtimeFn = fileTreeRuntimeFunction('fileTreeRowLocalRelativePath'); - if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); - if (!(row instanceof HTMLElement)) return ''; - var direct = String(row.getAttribute('data-local-relative-path') || '').trim(); - if (direct) return direct; - var assetPath = localFilePathFromAssetId(fileTreeRowAssetId(row)); - if (assetPath) return assetPath; - var rowId = String(row.getAttribute('data-row-id') || '').trim(); - var nodeId = String(row.getAttribute('data-node-id') || '').trim(); - var documentId = fileTreeRowDocumentId(row); - var kind = fileTreeRowKind(row); - if (rowId.indexOf('local:asset:') === 0) return rowId.slice('local:asset:'.length); - if (rowId.indexOf('local:markdown:') === 0) return rowId.slice('local:markdown:'.length); - if (rowId.indexOf('local:folder:') === 0) return rowId.slice('local:folder:'.length); - if (rowId.indexOf('local:node:') === 0) return rowId.slice('local:node:'.length); - if (nodeId.indexOf('local:node:') === 0) return nodeId.slice('local:node:'.length); - if ((kind === 'document' || kind === 'doc' || kind === 'markdown') && documentId.indexOf('local-md:') === 0) { - return decodeLocalEncodedPath(documentId.slice('local-md:'.length)); - } - if ((kind === 'folder' || kind === 'directory' || kind === 'index') && documentId.indexOf('local-dir:') === 0) { - return decodeLocalEncodedPath(documentId.slice('local-dir:'.length)); - } - return ''; - } - - function fileTreeRowLocalUploadTargetRelativePath(row) { - var runtimeFn = fileTreeRuntimeFunction('fileTreeRowLocalUploadTargetRelativePath'); - if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); - if (!(row instanceof HTMLElement)) return ''; - var kind = fileTreeRowKind(row); - var relativePath = fileTreeRowLocalRelativePath(row); - if (!relativePath) return ''; - if (kind === 'folder' || kind === 'directory') return relativePath; - var lastSlash = relativePath.lastIndexOf('/'); - return lastSlash >= 0 ? relativePath.slice(0, lastSlash) : ''; - } - - function fileTreeRowKind(row) { - var runtimeFn = fileTreeRuntimeFunction('fileTreeRowKind'); - if (runtimeFn) return runtimeFn(row); - if (!(row instanceof HTMLElement)) return ''; - return String(row.getAttribute('data-row-kind') || '').trim(); - } - - function isFileTreeDownloadableAssetRow(row) { - var runtimeFn = fileTreeRuntimeFunction('isFileTreeDownloadableAssetRow'); - if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); - if (!(row instanceof HTMLElement)) return false; - var kind = fileTreeRowKind(row); - if (kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown' || kind === 'folder' || kind === 'directory') return false; - return Boolean(fileTreeRowAssetId(row)); - } - - function isFileTreeDownloadableRow(row) { - var runtimeFn = fileTreeRuntimeFunction('isFileTreeDownloadableRow'); - if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); - if (!(row instanceof HTMLElement)) return false; - if (currentSourceKind() !== 'local_folder') return isFileTreeDownloadableAssetRow(row); - return Boolean(fileTreeRowLocalRelativePath(row) || isFileTreeDownloadableAssetRow(row)); - } - - function fileTreeAssetDownloadDetail(row) { - var runtimeFn = fileTreeRuntimeFunction('fileTreeAssetDownloadDetail'); - if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps()); - if (!isFileTreeDownloadableRow(row)) return null; - var title = rowTitle(row); - var relativePath = fileTreeRowLocalRelativePath(row); - var assetId = fileTreeRowAssetId(row) || (relativePath ? 'local-file:' + relativePath : ''); - return { - contextKind: 'filetree', - documentId: fileTreeRowDocumentId(row), - rowId: row.getAttribute('data-row-id') || '', - rowKind: fileTreeRowKind(row), - assetId: assetId, - localRelativePath: relativePath, - title: title, - fileName: title, - workspaceId: resolveWorkspaceId(row) - }; - } - - function selectedSidebarFileTreeRowsForDownload(contextRow) { - var contextRowId = contextRow instanceof HTMLElement ? contextRow.getAttribute('data-row-id') || '' : ''; - var selectedRows = selectedSidebarFileTreeRows().filter(isFileTreeDownloadableRow); - if (contextRowId && sidebarFileTreeSelection.selectedRowIds.has(contextRowId) && selectedRows.length > 0) return selectedRows; - return isFileTreeDownloadableRow(contextRow) ? [contextRow] : selectedRows; - } - - function selectedSidebarFileTreeAssetRowsForDownload(contextRow) { - return selectedSidebarFileTreeRowsForDownload(contextRow).filter(isFileTreeDownloadableAssetRow); - } - - function downloadSelectedFileTreeAssetRows(detail, trigger) { - var contextRow = trigger && trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger; - if (!(contextRow instanceof HTMLElement) && detail && detail.rowId) { - contextRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(detail.rowId) + '"]'); - } - var rows = selectedSidebarFileTreeRowsForDownload(contextRow); - var seen = new Set(); - var downloads = []; - rows.forEach(function(row) { - var item = fileTreeAssetDownloadDetail(row); - var key = item && (item.localRelativePath || item.assetId || item.rowId); - if (!item || !key || seen.has(key)) return; - seen.add(key); - downloads.push(item); - }); - if (downloads.length === 0 && detail && detail.assetId) { - downloads.push(Object.assign({}, detail, { fileName: detail.fileName || detail.title || '附件' })); - } else if (downloads.length === 0 && detail && detail.localRelativePath) { - downloads.push(Object.assign({}, detail, { - assetId: detail.assetId || 'local-file:' + detail.localRelativePath, - fileName: detail.fileName || detail.title || '附件' - })); - } - if (downloads.length === 0) { - recordFileTreeActionStatus('blocked', Object.assign({}, detail || {}, { reason: 'no-downloadable-assets' })); - return false; - } - var primary = Object.assign({}, downloads[0], { - count: downloads.length, - assetIds: downloads.map(function(item) { return item.assetId || ''; }).filter(Boolean), - localRelativePaths: downloads.map(function(item) { return item.localRelativePath || ''; }).filter(Boolean) - }); - recordFileTreeAction(downloads.length > 1 ? 'bulk-download' : 'download', primary); - recordFileTreeActionStatus('requested', primary); - document.documentElement.setAttribute('data-mnote-filetree-download-count', String(downloads.length)); - document.documentElement.setAttribute('data-mnote-filetree-download-asset-ids', downloads.map(function(item) { return item.assetId || ''; }).filter(Boolean).join(',')); - document.documentElement.setAttribute('data-mnote-filetree-download-paths', downloads.map(function(item) { return item.localRelativePath || ''; }).filter(Boolean).join(',')); - downloads.forEach(function(item) { - void openEditorAttachmentDownload(item); - }); - return true; - } - - function hasSelectedDocumentAncestor(row, selectedDocRowIds) { - var node = row instanceof HTMLElement ? row.closest('.tree-node') : null; - while (node && node.parentElement) { - var parentChildren = node.parentElement.closest('.tree-children'); - var parentNode = parentChildren ? parentChildren.closest('.tree-node') : null; - var parentRow = parentNode ? parentNode.querySelector(':scope > .tree-row[data-shell-mode="filetree"]') : null; - if (parentRow instanceof HTMLElement) { - var parentRowId = parentRow.getAttribute('data-row-id') || ''; - if (selectedDocRowIds.has(parentRowId)) return true; - } - node = parentNode; - } - return false; - } - - function classifySidebarFileTreeAsset(row) { - var badge = row instanceof HTMLElement ? row.querySelector('.tree-kind-badge') : null; - var iconKind = badge instanceof HTMLElement ? String(badge.getAttribute('data-kind') || '').trim() : ''; - var objectKind = row instanceof HTMLElement ? String(row.getAttribute('data-object-kind') || '').trim() : ''; - var title = rowTitle(row).toLowerCase(); - if (objectKind === 'mindmap' || iconKind === 'mindmap') return 'mindmap'; - if (objectKind === 'table' || iconKind === 'table' || iconKind === 'luckysheet' || title.indexOf('.luckysheet') >= 0) return 'table'; - return 'file'; - } - - function buildSidebarFileTreeDeletePlan(rows) { - var docRows = []; - var folderRows = []; - var assetRows = []; - rows.forEach(function(row) { - var kind = fileTreeRowKind(row); - if ((kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown') && fileTreeRowDocumentId(row)) { - docRows.push(row); - return; - } - if (kind === 'folder' && currentSourceKind() === 'local_folder') { - folderRows.push(row); - return; - } - if (fileTreeRowAssetId(row)) assetRows.push(row); - }); - var selectedContainerRowIds = new Set(docRows.concat(folderRows).map(function(row) { return row.getAttribute('data-row-id') || ''; }).filter(Boolean)); - var topDocRows = []; - var topFolderRows = []; - var seenDocs = new Set(); - var seenFolders = new Set(); - docRows.forEach(function(row) { - var documentId = fileTreeRowDocumentId(row); - if (!documentId || seenDocs.has(documentId)) return; - if (hasSelectedDocumentAncestor(row, selectedContainerRowIds)) return; - seenDocs.add(documentId); - topDocRows.push(row); - }); - folderRows.forEach(function(row) { - var rowId = row.getAttribute('data-row-id') || ''; - if (!rowId || seenFolders.has(rowId)) return; - if (hasSelectedDocumentAncestor(row, selectedContainerRowIds)) return; - seenFolders.add(rowId); - topFolderRows.push(row); - }); - var selectedTopContainerRows = new Set(topDocRows.concat(topFolderRows).map(function(row) { return row.getAttribute('data-row-id') || ''; }).filter(Boolean)); - var fileAssetRows = []; - var mindmapRows = []; - var tableRows = []; - var seenAssets = new Set(); - assetRows.forEach(function(row) { - var assetId = fileTreeRowAssetId(row); - if (!assetId || seenAssets.has(assetId)) return; - if (hasSelectedDocumentAncestor(row, selectedTopContainerRows)) return; - seenAssets.add(assetId); - var assetKind = classifySidebarFileTreeAsset(row); - if (assetKind === 'mindmap') mindmapRows.push(row); - else if (assetKind === 'table') tableRows.push(row); - else fileAssetRows.push(row); - }); - return { - docRows: topDocRows, - folderRows: topFolderRows, - fileAssetRows: fileAssetRows, - mindmapRows: mindmapRows, - tableRows: tableRows - }; - } - - function sidebarFileTreeDeleteConfirmText(plan) { - var docCount = plan.docRows.length; - var folderCount = plan.folderRows.length; - var fileCount = plan.fileAssetRows.length; - var mindmapCount = plan.mindmapRows.length; - var tableCount = plan.tableRows.length; - var parts = []; - if (docCount > 0) parts.push(docCount + ' 个页面(删除到垃圾桶)'); - if (folderCount > 0) parts.push(folderCount + ' 个文件夹(删除到垃圾桶)'); - if (fileCount > 0) parts.push(fileCount + ' 个附件(删除,10 分钟内可撤销)'); - if (mindmapCount > 0) parts.push(mindmapCount + ' 个思维导图(移入垃圾桶,10 分钟内可恢复)'); - if (tableCount > 0) parts.push(tableCount + (currentSourceKind() === 'local_folder' ? ' 个在线表格(移入垃圾桶,10 分钟内可恢复)' : ' 个在线表格(删除)')); - return '确认删除选中的 ' + parts.join(' + ') + ' 吗?'; - } - - async function postSidebarFileTreeJson(url, body) { - var response = await fetch(url, { - method: 'POST', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify(body || {}) - }); - var payload = await response.json().catch(function() { return null; }); - if (!response.ok) { - throw new Error((payload && (payload.error || payload.message)) || (url + ' failed: ' + response.status)); - } - return payload; - } - - function fileTreeRowsByRowIds(rowIds) { - var runtimeFn = fileTreeRuntimeFunction('fileTreeRowsByRowIds'); - if (runtimeFn) return runtimeFn(rowIds, fileTreeRuntimeDeps()); - return (rowIds || []).map(function(rowId) { - return document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]'); - }).filter(function(row) { return row instanceof HTMLElement; }); - } - - function fileTreeChildCount(documentId) { - var runtimeFn = fileTreeRuntimeFunction('fileTreeChildCount'); - if (runtimeFn) return runtimeFn(documentId, fileTreeRuntimeDeps()); - if (!documentId) return 0; - var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + documentId) + '"]'); - var node = row ? row.closest('.tree-node') : null; - var children = node ? node.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"][data-row-kind="document"], :scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"][data-row-kind="doc"]') : []; - return children.length; - } - - async function pasteSidebarFileTreeClipboard(trigger) { - if (!sidebarFileTreeClipboard || !Array.isArray(sidebarFileTreeClipboard.rowIds) || sidebarFileTreeClipboard.rowIds.length === 0) return false; - var targetRow = trigger instanceof HTMLElement ? trigger : null; - if (!targetRow && sidebarFileTreeSelection.focusedRowId) { - targetRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(sidebarFileTreeSelection.focusedRowId) + '"]'); - } - var targetDocumentId = fileTreeRowDocumentId(targetRow) || currentDocumentId(); - if (!targetDocumentId) return false; - var action = sidebarFileTreeClipboard.action === 'cut' ? 'move' : 'copy'; - var rows = fileTreeRowsByRowIds(sidebarFileTreeClipboard.rowIds); - if (rows.length === 0) return false; - var writable = await ensureFileTreeWritableTarget('paste', targetRow, sidebarFileTreeClipboard.rowIds, action === 'copy'); - if (!writable) return false; - recordFileTreeAction('paste', { - rowId: targetRow ? targetRow.getAttribute('data-row-id') || '' : '', - documentId: targetDocumentId, - sourceRowIds: sidebarFileTreeClipboard.rowIds, - clipboardAction: sidebarFileTreeClipboard.action - }); - var plan = buildSidebarFileTreeDeletePlan(rows); - var workspaceId = resolveWorkspaceId(targetRow || document.body); - var failures = []; - if (action === 'copy') { - dispatchSidebarEvent('tree.filetree.copy-requested', { rowIds: sidebarFileTreeClipboard.rowIds, targetDocumentId: targetDocumentId }); - recordFileTreeActionStatus('copy-requested', { documentId: targetDocumentId }); - return true; - } - for (var i = 0; i < plan.docRows.length; i += 1) { - var docRow = plan.docRows[i]; - var documentId = fileTreeRowDocumentId(docRow); - if (!documentId || documentId === targetDocumentId) continue; - try { - await dispatchTreeCommand(targetRow || docRow, { - action: 'move', - workspaceId: workspaceId, - documentId: documentId, - parentId: targetDocumentId, - sortOrder: fileTreeChildCount(targetDocumentId) + i - }); - } catch (error) { - failures.push(documentId + ': ' + (error && error.message ? error.message : '移动页面失败')); - } - } - var assetIds = plan.fileAssetRows.map(fileTreeRowAssetId).filter(Boolean); - if (assetIds.length > 0) { - try { - await postSidebarFileTreeJson('/api/media/batch', { - action: 'move', - assetIds: assetIds, - targetDocumentId: targetDocumentId - }); - } catch (error) { - failures.push(assetIds.join(',') + ': ' + (error && error.message ? error.message : '移动附件失败')); - } - } - if (failures.length > 0) { - recordFileTreeActionStatus('failed', { documentId: targetDocumentId, fallback: 'alert' }); - window.alert('部分对象移动失败:' + failures.join(';')); - return false; - } - sidebarFileTreeClipboard = null; - recordFileTreeActionStatus('applied', { documentId: targetDocumentId }); - return true; - } - - async function deleteSelectedSidebarFileTreeRows(trigger) { - var rows = selectedSidebarFileTreeRows(); - var plan = buildSidebarFileTreeDeletePlan(rows); - var total = plan.docRows.length + plan.folderRows.length + plan.fileAssetRows.length + plan.mindmapRows.length + plan.tableRows.length; - if (total === 0) return false; - if (!window.confirm(sidebarFileTreeDeleteConfirmText(plan))) return false; - recordFileTreeAction('bulk-delete', { rowId: trigger instanceof HTMLElement ? trigger.getAttribute('data-row-id') || '' : '', count: total }); - recordFileTreeActionStatus('pending', { count: total }); - var failures = []; - for (var i = 0; i < plan.docRows.length; i += 1) { - var docRow = plan.docRows[i]; - var documentId = fileTreeRowDocumentId(docRow); - try { - await dispatchTreeCommand(trigger || docRow, { - action: 'archive', - workspaceId: resolveWorkspaceId(docRow), - documentId: documentId - }); - applyRemoveDocumentDelta({ documentId: documentId }); - } catch (error) { - failures.push(documentId); - } - } - for (var fd = 0; fd < plan.folderRows.length; fd += 1) { - var folderRow = plan.folderRows[fd]; - var folderId = folderRow.getAttribute('data-row-id') || folderRow.getAttribute('data-node-id') || ''; - try { - await dispatchTreeCommand(trigger || folderRow, { - action: 'archive', - workspaceId: resolveWorkspaceId(folderRow), - documentId: folderId - }); - } catch (error) { - failures.push(folderId); - } - } - if (plan.fileAssetRows.length > 0) { - var fileAssetIds = plan.fileAssetRows.map(fileTreeRowAssetId).filter(Boolean); - try { - if (currentSourceKind() === 'local_folder') { - for (var lf = 0; lf < fileAssetIds.length; lf += 1) { - await dispatchTreeCommand(trigger || plan.fileAssetRows[lf], { - action: 'archive', - workspaceId: resolveWorkspaceId(plan.fileAssetRows[lf]), - documentId: fileAssetIds[lf] - }); - } - } else { - await postSidebarFileTreeJson('/api/media/batch', { action: 'delete', assetIds: fileAssetIds }); - } - fileAssetIds.forEach(removeFileTreeAssetRow); - } catch (error) { - failures = failures.concat(fileAssetIds); - } - } - for (var m = 0; m < plan.mindmapRows.length; m += 1) { - var mindmapRow = plan.mindmapRows[m]; - var mindmapId = fileTreeRowAssetId(mindmapRow); - try { - if (currentSourceKind() === 'local_folder') { - await dispatchTreeCommand(trigger || mindmapRow, { - action: 'archive', - workspaceId: resolveWorkspaceId(mindmapRow), - documentId: mindmapId - }); - } else { - var mindmapDocId = fileTreeRowDocumentId(mindmapRow); - var mindmapResponse = await fetch('/api/mindmap/' + encodeURIComponent(mindmapDocId) + '/' + encodeURIComponent(mindmapId), { method: 'DELETE' }); - if (!mindmapResponse.ok) throw new Error('mindmap_delete_failed_' + mindmapResponse.status); - } - removeFileTreeAssetRow(mindmapId); - } catch (error) { - failures.push(mindmapId); - } - } - for (var t = 0; t < plan.tableRows.length; t += 1) { - var tableRow = plan.tableRows[t]; - var tableId = fileTreeRowAssetId(tableRow); - try { - if (currentSourceKind() === 'local_folder') { - await dispatchTreeCommand(trigger || tableRow, { - action: 'archive', - workspaceId: resolveWorkspaceId(tableRow), - documentId: tableId - }); - } else { - var tableResponse = await fetch('/api/tables/' + encodeURIComponent(tableId), { method: 'DELETE' }); - if (!tableResponse.ok) throw new Error('table_delete_failed_' + tableResponse.status); - window.dispatchEvent(new CustomEvent('online-table-deleted', { detail: { tableId: tableId } })); - } - removeFileTreeAssetRow(tableId); - } catch (error) { - failures.push(tableId); - } - } - sidebarFileTreeSelection.selectedRowIds = new Set(); - sidebarFileTreeSelection.focusedRowId = null; - syncSidebarFileTreeSelection(); - if (failures.length > 0) { - recordFileTreeActionStatus('failed', { count: total, failures: failures.slice(0, 20), fallback: 'alert' }); - window.alert('部分对象删除失败:' + failures.slice(0, 5).join(', ') + (failures.length > 5 ? '…' : '')); - return false; - } - document.documentElement.setAttribute('data-mnote-filetree-bulk-delete-applied', 'true'); - recordFileTreeActionStatus('archived', { count: total, undo: 'trash-modal' }); - if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot(); - return true; - } + const sidebarFileTreeCommandState = { + get activeTreeContextMenu() { return activeTreeContextMenu; }, + set activeTreeContextMenu(value) { activeTreeContextMenu = value; }, + get sidebarFileTreeClipboard() { return sidebarFileTreeClipboard; }, + set sidebarFileTreeClipboard(value) { sidebarFileTreeClipboard = value; }, + }; + const sidebarFileTreeCommand = createSidebarFileTreeCommandRuntime({ + applyRemoveDocumentDelta, + createFileTreeFolder, + createPage, + cssEscape, + currentDocumentId, + currentSourceKind, + deleteSingleFileTreeAsset, + dispatchSidebarEvent, + dispatchTreeCommand, + ensureFileTreeWritableTarget, + fileTreeRuntimeDeps, + fileTreeRuntimeFunction, + fileTreeSelectionRuntimeDeps, + fileTreeSelectionRuntimeFunction, + isFileTreePageRow, + localFilePathFromAssetId, + normalizeFileTreePageRenameTitle, + openConvexAssetFromFileTree, + openEditorAttachmentDetail, + openEditorAttachmentDownload, + openEditorAttachmentEditTab, + openEditorAttachmentNewWindow, + refreshLocalFolderSidebarSnapshot, + resolveWorkspaceId, + runtimeState: sidebarFileTreeCommandState, + selectedSidebarFileTreeSelection: sidebarFileTreeSelection, + updateTitleEverywhere, + validateFileTreeRename, + }); + const rowTitle = (...args) => sidebarFileTreeCommand.rowTitle(...args); + const rowCenter = (...args) => sidebarFileTreeCommand.rowCenter(...args); + const renameFileTreeAsset = (...args) => sidebarFileTreeCommand.renameFileTreeAsset(...args); + const beginFileTreeInlineRename = (...args) => sidebarFileTreeCommand.beginFileTreeInlineRename(...args); + const closeTreeContextMenu = (...args) => sidebarFileTreeCommand.closeTreeContextMenu(...args); + const copyTreeContextValue = (...args) => sidebarFileTreeCommand.copyTreeContextValue(...args); + const triggerBrowserDownload = (...args) => sidebarFileTreeCommand.triggerBrowserDownload(...args); + const recordFileTreeAction = (...args) => sidebarFileTreeCommand.recordFileTreeAction(...args); + const recordFileTreeActionStatus = (...args) => sidebarFileTreeCommand.recordFileTreeActionStatus(...args); + const documentHref = (...args) => sidebarFileTreeCommand.documentHref(...args); + const convertToPreviousSiblingChild = (...args) => sidebarFileTreeCommand.convertToPreviousSiblingChild(...args); + const fileTreeCopyPath = (...args) => sidebarFileTreeCommand.fileTreeCopyPath(...args); + const fileTreeMenuTargetParentId = (...args) => sidebarFileTreeCommand.fileTreeMenuTargetParentId(...args); + const withOfficeEditModeGuard = (...args) => sidebarFileTreeCommand.withOfficeEditModeGuard(...args); + const handleTreeContextMenuAction = (...args) => sidebarFileTreeCommand.handleTreeContextMenuAction(...args); + const appendTreeContextMenuButton = (...args) => sidebarFileTreeCommand.appendTreeContextMenuButton(...args); + const buildSidebarFileTreeContext = (...args) => sidebarFileTreeCommand.buildSidebarFileTreeContext(...args); + const evaluateSidebarFileTreeWhen = (...args) => sidebarFileTreeCommand.evaluateSidebarFileTreeWhen(...args); + const openTreeContextMenu = (...args) => sidebarFileTreeCommand.openTreeContextMenu(...args); + const openPageTreeContextMenu = (...args) => sidebarFileTreeCommand.openPageTreeContextMenu(...args); + const openFileTreeContextMenu = (...args) => sidebarFileTreeCommand.openFileTreeContextMenu(...args); + const visibleFileTreeRows = (...args) => sidebarFileTreeCommand.visibleFileTreeRows(...args); + const syncSidebarFileTreeSelection = (...args) => sidebarFileTreeCommand.syncSidebarFileTreeSelection(...args); + const selectSidebarFileTreeRow = (...args) => sidebarFileTreeCommand.selectSidebarFileTreeRow(...args); + const selectSidebarFileTreeDocument = (...args) => sidebarFileTreeCommand.selectSidebarFileTreeDocument(...args); + const selectSidebarFileTreeRowById = (...args) => sidebarFileTreeCommand.selectSidebarFileTreeRowById(...args); + const pendingLocalFolderRestoreRowId = (...args) => sidebarFileTreeCommand.pendingLocalFolderRestoreRowId(...args); + const clearPendingLocalFolderRestoreRowId = (...args) => sidebarFileTreeCommand.clearPendingLocalFolderRestoreRowId(...args); + const applyPendingLocalFolderRestoreFocusOnce = (...args) => sidebarFileTreeCommand.applyPendingLocalFolderRestoreFocusOnce(...args); + const schedulePendingLocalFolderRestoreFocus = (...args) => sidebarFileTreeCommand.schedulePendingLocalFolderRestoreFocus(...args); + const selectedSidebarFileTreeRowIdsForDrag = (...args) => sidebarFileTreeCommand.selectedSidebarFileTreeRowIdsForDrag(...args); + const selectedSidebarFileTreeRows = (...args) => sidebarFileTreeCommand.selectedSidebarFileTreeRows(...args); + const fileTreeRowDocumentId = (...args) => sidebarFileTreeCommand.fileTreeRowDocumentId(...args); + const fileTreeRowAssetId = (...args) => sidebarFileTreeCommand.fileTreeRowAssetId(...args); + const decodeLocalEncodedPath = (...args) => sidebarFileTreeCommand.decodeLocalEncodedPath(...args); + const fileTreeRowLocalRelativePath = (...args) => sidebarFileTreeCommand.fileTreeRowLocalRelativePath(...args); + const fileTreeRowLocalUploadTargetRelativePath = (...args) => sidebarFileTreeCommand.fileTreeRowLocalUploadTargetRelativePath(...args); + const fileTreeRowKind = (...args) => sidebarFileTreeCommand.fileTreeRowKind(...args); + const isFileTreeDownloadableAssetRow = (...args) => sidebarFileTreeCommand.isFileTreeDownloadableAssetRow(...args); + const isFileTreeDownloadableRow = (...args) => sidebarFileTreeCommand.isFileTreeDownloadableRow(...args); + const fileTreeAssetDownloadDetail = (...args) => sidebarFileTreeCommand.fileTreeAssetDownloadDetail(...args); + const selectedSidebarFileTreeRowsForDownload = (...args) => sidebarFileTreeCommand.selectedSidebarFileTreeRowsForDownload(...args); + const selectedSidebarFileTreeAssetRowsForDownload = (...args) => sidebarFileTreeCommand.selectedSidebarFileTreeAssetRowsForDownload(...args); + const downloadSelectedFileTreeAssetRows = (...args) => sidebarFileTreeCommand.downloadSelectedFileTreeAssetRows(...args); + const hasSelectedDocumentAncestor = (...args) => sidebarFileTreeCommand.hasSelectedDocumentAncestor(...args); + const classifySidebarFileTreeAsset = (...args) => sidebarFileTreeCommand.classifySidebarFileTreeAsset(...args); + const buildSidebarFileTreeDeletePlan = (...args) => sidebarFileTreeCommand.buildSidebarFileTreeDeletePlan(...args); + const sidebarFileTreeDeleteConfirmText = (...args) => sidebarFileTreeCommand.sidebarFileTreeDeleteConfirmText(...args); + const postSidebarFileTreeJson = (...args) => sidebarFileTreeCommand.postSidebarFileTreeJson(...args); + const fileTreeRowsByRowIds = (...args) => sidebarFileTreeCommand.fileTreeRowsByRowIds(...args); + const fileTreeChildCount = (...args) => sidebarFileTreeCommand.fileTreeChildCount(...args); + const pasteSidebarFileTreeClipboard = (...args) => sidebarFileTreeCommand.pasteSidebarFileTreeClipboard(...args); + const deleteSelectedSidebarFileTreeRows = (...args) => sidebarFileTreeCommand.deleteSelectedSidebarFileTreeRows(...args); function ensureSearchModal() { var existing = document.querySelector('[data-testid="wolai-search-modal"]'); diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index d7f07c09..d2188d42 100644 --- a/rust/crates/mnote-web/src/routes/mod.rs +++ b/rust/crates/mnote-web/src/routes/mod.rs @@ -138,6 +138,10 @@ pub fn build_router(state: AppState) -> Router { "/api/mnote-browser-runtime/sidebar-filetree-open-runtime.js", get(web_shell::sidebar_filetree_open_runtime_asset), ) + .route( + "/api/mnote-browser-runtime/sidebar-filetree-command-runtime.js", + get(web_shell::sidebar_filetree_command_runtime_asset), + ) .route( "/api/mnote-browser-runtime/sidebar-attachment-open-runtime.js", get(web_shell::sidebar_attachment_open_runtime_asset), @@ -598,6 +602,7 @@ mod tests { "/api/mnote-browser-runtime/sidebar-workspace-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", "/api/mnote-browser-runtime/sidebar-attachment-open-runtime.js", "/api/mnote-browser-runtime/sidebar-tree-runtime.js", "/api/mnote-browser-runtime/tree-live-controller.js", diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index 9e569c8e..8d15ce08 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -784,6 +784,20 @@ pub async fn sidebar_filetree_open_runtime_asset() -> Response { .unwrap_or_else(|_| Response::new(Body::empty())) } +pub async fn sidebar_filetree_command_runtime_asset() -> Response { + const JS: &str = include_str!("../../browser/sidebar-filetree-command-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_attachment_open_runtime_asset() -> Response { const JS: &str = include_str!("../../browser/sidebar-attachment-open-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 521fa635..9bfb0ee3 100644 --- a/rust/crates/mnote-web/src/ssr/pages/layout.rs +++ b/rust/crates/mnote-web/src/ssr/pages/layout.rs @@ -203,6 +203,8 @@ mod tests { include_str!("../../../browser/sidebar-tree-live-apply-runtime.js"); const SIDEBAR_FILETREE_OPEN_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-filetree-open-runtime.js"); + const SIDEBAR_FILETREE_COMMAND_RUNTIME_JS: &str = + include_str!("../../../browser/sidebar-filetree-command-runtime.js"); const SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-attachment-open-runtime.js"); const SIDEBAR_SHELL_RUNTIME_JS: &str = @@ -313,7 +315,7 @@ mod tests { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.filetree.internal-drop")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree.filetree.external-drop")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("beginFileTreeInlineRename")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("tree-rename-input")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("tree-rename-input")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("sidebarFileTreeClipboard")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("pasteSidebarFileTreeClipboard")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX")); @@ -578,16 +580,18 @@ mod tests { fn sidebar_filetree_runtime_helpers_are_externalized_with_inline_fallback() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function fileTreeRuntimeFunction")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.__mnoteFileTreeRuntime")); - assert!( - SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('fileTreeRowDocumentId')") - ); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('fileTreeRowAssetId')")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("fileTreeRuntimeFunction('fileTreeRowDocumentId')")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("fileTreeRuntimeFunction('fileTreeRowAssetId')")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeRowLocalRelativePath')")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeRuntimeFunction('fileTreeAssetDownloadDetail')")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('fileTreeRowsByRowIds')")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('fileTreeChildCount')")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("fileTreeRuntimeFunction('fileTreeRowsByRowIds')")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("fileTreeRuntimeFunction('fileTreeChildCount')")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('revealFileTreeRow')")); assert!( SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeRuntimeFunction('revealFileTreeAssetRow')") @@ -631,7 +635,8 @@ mod tests { .contains("fileTreeRuntimeFunction('applyLocalFolderSidebarSnapshot')")); assert!(SIDEBAR_TREE_LIVE_APPLY_RUNTIME_JS .contains("fileTreeRuntimeFunction('markLocalFolderWatchApplied')")); - let document_id_body = js_function_body(SIDEBAR_TREE_RUNTIME_JS, "fileTreeRowDocumentId"); + let document_id_body = + js_function_body(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS, "fileTreeRowDocumentId"); assert!( document_id_body.contains("data-document-id") && document_id_body.contains("data-doc-id"), @@ -759,24 +764,29 @@ mod tests { fn sidebar_filetree_selection_runtime_helpers_are_externalized_with_inline_fallback() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function fileTreeSelectionRuntimeFunction")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("window.__mnoteFileTreeSelectionRuntime")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeSelectionRuntimeFunction('visibleFileTreeRows')")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeSelectionRuntimeFunction('syncSidebarFileTreeSelection')")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeSelectionRuntimeFunction('selectSidebarFileTreeRow')")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRowIdsForDrag')")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRows')")); - let select_body = js_function_body(SIDEBAR_TREE_RUNTIME_JS, "selectSidebarFileTreeRow"); + let select_body = js_function_body( + SIDEBAR_FILETREE_COMMAND_RUNTIME_JS, + "selectSidebarFileTreeRow", + ); assert!( select_body.contains("shiftKey && sidebarFileTreeSelection.anchorRowId") && select_body.contains("ctrlKey"), "inline fallback 必须保留 shift range 与 ctrl/meta toggle" ); - let select_document_body = - js_function_body(SIDEBAR_TREE_RUNTIME_JS, "selectSidebarFileTreeDocument"); + let select_document_body = js_function_body( + SIDEBAR_FILETREE_COMMAND_RUNTIME_JS, + "selectSidebarFileTreeDocument", + ); assert!( select_document_body .contains("selectSidebarFileTreeRow(row, { ctrlKey: false, metaKey: false, shiftKey: false })"), @@ -800,8 +810,10 @@ mod tests { #[test] fn sidebar_tree_runtime_focuses_restored_local_folder_row() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("applyPendingLocalFolderRestoreFocusOnce")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote.pendingLocalFolderRestoreFiletreeRowId")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-local-folder-restore-focused-row-id")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("mnote.pendingLocalFolderRestoreFiletreeRowId")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("data-mnote-local-folder-restore-focused-row-id")); } #[test] @@ -885,29 +897,29 @@ mod tests { #[test] fn sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates() { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openTreeContextMenu")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mnote-tree-context-menu")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("复制访问链接")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("删除到垃圾桶")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("mnote-tree-context-menu")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("复制访问链接")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("删除到垃圾桶")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function createFileTreeFolder")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("action: 'create_folder'")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function fileTreeMenuTargetParentId")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (action === 'new-folder')")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeCopyPath(detail, trigger)")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function fileTreeMenuTargetParentId")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("if (action === 'new-folder')")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("fileTreeCopyPath(detail, trigger)")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("if (detail.contextKind === 'filetree' && action === 'download')")); - assert!( - SIDEBAR_TREE_RUNTIME_JS.contains("downloadSelectedFileTreeAssetRows(detail, trigger)") - ); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function selectedSidebarFileTreeRowsForDownload")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("downloadSelectedFileTreeAssetRows(detail, trigger)")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("function selectedSidebarFileTreeRowsForDownload")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("function selectedSidebarFileTreeAssetRowsForDownload")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-local-relative-path")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("data-local-relative-path")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("targetRelativePath")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains( + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains( "recordFileTreeAction(downloads.length > 1 ? 'bulk-download' : 'download', primary)" )); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-filetree-download-count")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function triggerBrowserDownload")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("data-mnote-filetree-download-count")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function triggerBrowserDownload")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openEditorAttachmentMenu(editorAttachmentLink, editorAttachmentLink, { x: event.clientX, y: event.clientY });")); assert!(SIDEBAR_TREE_RUNTIME_JS .contains("var localFilePath = localFilePathFromAssetId(assetId);")); @@ -974,36 +986,39 @@ mod tests { #[test] fn sidebar_tree_delete_to_trash_dispatches_archive_not_purge() { - let delete_trash_start = SIDEBAR_TREE_RUNTIME_JS + let delete_trash_start = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .find("if (action === 'delete-trash'") .expect("delete-trash branch"); - let delete_trash_end = SIDEBAR_TREE_RUNTIME_JS[delete_trash_start..] + let delete_trash_end = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS[delete_trash_start..] .find("function appendTreeContextMenuButton") .map(|offset| delete_trash_start + offset) .expect("delete-trash branch end"); - let delete_trash_branch = &SIDEBAR_TREE_RUNTIME_JS[delete_trash_start..delete_trash_end]; + let delete_trash_branch = + &SIDEBAR_FILETREE_COMMAND_RUNTIME_JS[delete_trash_start..delete_trash_end]; assert!(delete_trash_branch.contains("action: 'archive'")); assert!(!delete_trash_branch.contains("action: 'purge'")); } #[test] fn sidebar_filetree_delete_keys_support_mixed_doc_and_asset_selection() { - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function deleteSelectedSidebarFileTreeRows")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("function deleteSelectedSidebarFileTreeRows")); assert!( SIDEBAR_TREE_RUNTIME_JS.contains("event.key === 'Delete' || event.key === 'Backspace'") ); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("selectedFileTreeRows.length > 1")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("selectedFileTreeRows.length > 1")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("deleteSelectedSidebarFileTreeRows(trigger || document.body)")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains( + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains( "currentSourceKind() === 'local_folder' && fileTreeRowKind(row) === 'asset'" )); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (currentSourceKind() === 'local_folder')")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("documentId: fileAssetIds[lf]")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/media/batch")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/mindmap/")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tables/")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("确认删除选中的 ")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("if (currentSourceKind() === 'local_folder')")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("documentId: fileAssetIds[lf]")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("/api/media/batch")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("/api/mindmap/")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("/api/tables/")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("确认删除选中的 ")); } #[test] @@ -1011,23 +1026,31 @@ mod tests { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function blockReadonlyFileTreeAction")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-filetree-readonly-blocked")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-filetree-readonly-message")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("recordFileTreeActionStatus('blocked'")); + assert!( + SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("recordFileTreeActionStatus('blocked'") + ); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("/api/tree/filetree/drop-preflight")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("ensureFileTreeWritableTarget('paste'")); + assert!( + SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("ensureFileTreeWritableTarget('paste'") + ); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("ensureFileTreeWritableTarget('drop'")); } #[test] fn sidebar_filetree_command_context_helper_functions_exist() { - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function buildSidebarFileTreeContext")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function evaluateSidebarFileTreeWhen")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("__mnoteFileTreeContextMenuRuntime")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'workspace.sourceKind'")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'workspace.readonly'")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'tree.focusKind'")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'tree.selectionCount'")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("'tree.selectionResourceKind'")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-workspace-readonly")); + assert!( + SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function buildSidebarFileTreeContext") + ); + assert!( + SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("function evaluateSidebarFileTreeWhen") + ); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("__mnoteFileTreeContextMenuRuntime")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'workspace.sourceKind'")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'workspace.readonly'")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'tree.focusKind'")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'tree.selectionCount'")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("'tree.selectionResourceKind'")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("data-mnote-workspace-readonly")); } #[test] @@ -1045,11 +1068,11 @@ mod tests { fn sidebar_filetree_context_menu_items_have_when_for_readonly() { // FileTree 分支的写操作必须带 when: '!workspace.readonly'。 assert!( - SIDEBAR_TREE_RUNTIME_JS.contains("when: '!workspace.readonly'"), + SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains("when: '!workspace.readonly'"), "上下文菜单写操作应携带 when: '!workspace.readonly'" ); // Verify at least one delete-trash item has the when expression - let delete_trash_items = SIDEBAR_TREE_RUNTIME_JS + let delete_trash_items = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .match_indices("'delete-trash'") .count(); assert!(delete_trash_items > 0, "应至少有一个 delete-trash 菜单项"); @@ -1066,14 +1089,14 @@ mod tests { #[test] fn sidebar_filetree_local_table_bulk_delete_uses_tree_command() { - let table_loop_start = SIDEBAR_TREE_RUNTIME_JS + let table_loop_start = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .find("for (var t = 0; t < plan.tableRows.length; t += 1)") .expect("table bulk delete loop"); - let table_loop_end = SIDEBAR_TREE_RUNTIME_JS[table_loop_start..] + let table_loop_end = SIDEBAR_FILETREE_COMMAND_RUNTIME_JS[table_loop_start..] .find("sidebarFileTreeSelection.selectedRowIds") .map(|offset| table_loop_start + offset) .expect("table bulk delete loop end"); - let table_loop = &SIDEBAR_TREE_RUNTIME_JS[table_loop_start..table_loop_end]; + let table_loop = &SIDEBAR_FILETREE_COMMAND_RUNTIME_JS[table_loop_start..table_loop_end]; assert!(table_loop.contains("currentSourceKind() === 'local_folder'")); assert!(table_loop.contains("dispatchTreeCommand(trigger || tableRow")); assert!(table_loop.contains("action: 'archive'")); @@ -1138,21 +1161,25 @@ mod tests { assert!( SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("params.set('mode', input.mode || 'view');") ); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains( + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS.contains( "{ action: 'open-edit-mode', icon: 'edit_note', label: '使用编辑模式打开' }" )); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("{ action: 'open-edit-mode', icon: 'edit_note', label: '弹窗编辑' }")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS .contains("{ action: 'new-window-edit', icon: 'open_in_new', label: '新窗口编辑' }")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function withOfficeEditModeGuard(callback)")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-last-office-edit-mode-requested")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("function withOfficeEditModeGuard(callback)")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("data-mnote-last-office-edit-mode-requested")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .contains("function openEditorAttachmentEditTab(detail)")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("void openEditorAttachmentEditTab(detail);")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("void openEditorAttachmentEditTab(detail);")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .contains("openLocalOfficeFileInActiveTab(detail, 'edit')")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openEditorAttachmentNewWindow(detail, 'edit')")); + assert!(SIDEBAR_FILETREE_COMMAND_RUNTIME_JS + .contains("openEditorAttachmentNewWindow(detail, 'edit')")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("forceEditMode ? 'edit' : 'view'")); assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .contains("url.searchParams.set('mode', requestedMode);"));