From 7816ab7ae2f49aa2e5961f69511ebc614435ee12 Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Tue, 26 May 2026 02:05:36 +0800 Subject: [PATCH] refactor: split sidebar attachment open runtime --- ...ime-module-maintainability-checklist-v1.md | 2 +- .../sidebar-attachment-open-runtime.js | 652 ++++++++++++++++++ .../mnote-web/browser/sidebar-tree-runtime.js | 643 +---------------- rust/crates/mnote-web/src/routes/mod.rs | 5 + rust/crates/mnote-web/src/routes/web_shell.rs | 14 + rust/crates/mnote-web/src/ssr/pages/layout.rs | 35 +- 6 files changed, 724 insertions(+), 627 deletions(-) create mode 100644 rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js diff --git a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md index 0b37e4b3..f08e553e 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 @@ -168,7 +168,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::web_shell - [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。 - [ ] C5. `sidebar-filetree-upload-runtime.js`:local upload target plan、drop/paste preflight、readonly guard。 -- [ ] C6. `sidebar-attachment-open-runtime.js`:OnlyOffice/PDF/code/image open mode 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。 - [ ] C8. `sidebar-tree-runtime.js` 保留为 entrypoint,目标少于 3,000 行。 diff --git a/rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js b/rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js new file mode 100644 index 00000000..854166b7 --- /dev/null +++ b/rust/crates/mnote-web/browser/sidebar-attachment-open-runtime.js @@ -0,0 +1,652 @@ +export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => { + const { + attachmentClassForFileName, + buildLocalFileOpenUrl, + buildLocalOnlyOfficeOpenUrl, + buildOnlyOfficeOpenPath, + buildOnlyOfficeOpenUrl, + currentDocumentId, + currentRootUri, + currentWorkspaceSourcePayload, + fileTreeIconKindForFileName, + inferCodeAttachmentLanguage, + inferOnlyOfficeFileType, + isCodeAttachmentFileName, + isPdfAttachmentFileName, + localFilePathFromAssetId, + openLocalOfficeFileInActiveTab, + openLocalResourceInActiveTab, + openTreeContextMenu, + resolveWorkspaceId, + triggerBrowserDownload, + uploadedFileSize, + } = dependencies; + + var activeEditorAttachmentLink = null; + var attachmentActionsHideTimer = 0; + + function attachmentQueryParams(href) { + try { + return new URL(String(href || ''), window.location.origin).searchParams; + } catch (_) { + return new URLSearchParams(); + } + } + + function localFileOpenPathFromHref(href) { + try { + var url = new URL(String(href || ''), window.location.origin); + if (url.pathname !== '/api/local-folder/files/open') return ''; + return String(url.searchParams.get('path') || '').trim(); + } catch (_) { + return ''; + } + } + + function localFileOpenRootUriFromHref(href) { + try { + var url = new URL(String(href || ''), window.location.origin); + if (url.pathname !== '/api/local-folder/files/open') return ''; + return String(url.searchParams.get('rootUri') || '').trim(); + } catch (_) { + return ''; + } + } + + function buildLocalFileStatusUrl(relativePath, rootUri) { + var effectiveRootUri = String(rootUri || currentRootUri() || '').trim(); + if (!effectiveRootUri || !relativePath) return ''; + var url = new URL('/api/local-folder/files/stat', window.location.origin); + url.searchParams.set('rootUri', effectiveRootUri); + url.searchParams.set('path', relativePath); + return url.toString(); + } + + function setEditorAttachmentMissingState(link, missing) { + if (!(link instanceof HTMLAnchorElement)) return; + var value = Boolean(missing); + if (value) { + link.setAttribute('data-mnote-attachment-missing', 'true'); + link.classList.add('mnote-uploaded-attachment-missing'); + link.setAttribute('aria-label', (link.textContent || '附件') + '(文件不存在)'); + } else { + if (link.getAttribute('data-mnote-attachment-missing') !== 'true') return; + link.removeAttribute('data-mnote-attachment-missing'); + link.classList.remove('mnote-uploaded-attachment-missing'); + link.removeAttribute('aria-label'); + } + } + + async function refreshLocalAttachmentExistence(link) { + if (!(link instanceof HTMLAnchorElement)) return; + var href = link.getAttribute('href') || link.href || ''; + var localFilePath = localFileOpenPathFromHref(href); + if (!localFilePath) return; + var statusUrl = buildLocalFileStatusUrl(localFilePath, localFileOpenRootUriFromHref(href)); + if (!statusUrl) return; + try { + var response = await fetch(statusUrl, { headers: { accept: 'application/json' }, cache: 'no-store' }); + var payload = await response.json().catch(function() { return null; }); + var exists = Boolean(response.ok && payload && payload.ok === true && payload.result && payload.result.exists === true); + setEditorAttachmentMissingState(link, !exists); + } catch (_) {} + } + + function refreshEditorLocalAttachmentExistence() { + document.querySelectorAll('.editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]').forEach(function(link) { + if (link instanceof HTMLAnchorElement) void refreshLocalAttachmentExistence(link); + }); + } + window.__mnoteRefreshEditorLocalAttachmentExistence = refreshEditorLocalAttachmentExistence; + + function fileNameFromPath(path) { + var value = String(path || '').trim(); + return value.indexOf('/') >= 0 ? value.split('/').pop() : value; + } + + function isOnlyOfficeAttachmentHref(href) { + try { + var url = new URL(String(href || ''), window.location.origin); + return url.pathname === '/onlyoffice' && (url.searchParams.has('assetId') || url.searchParams.has('fileName')); + } catch (_) { + return false; + } + } + + function normalizeOnlyOfficeAttachmentHref(href) { + try { + var url = new URL(String(href || ''), window.location.origin); + if (url.pathname !== '/onlyoffice') return String(href || ''); + return buildOnlyOfficeOpenUrl({ + fileUrl: url.searchParams.get('fileUrl') || '', + fileName: url.searchParams.get('fileName') || '未命名附件', + fileType: url.searchParams.get('fileType') || inferOnlyOfficeFileType(url.searchParams.get('fileName') || '', ''), + assetId: url.searchParams.get('assetId') || '', + documentId: url.searchParams.get('documentId') || currentDocumentId() || '', + userId: url.searchParams.get('userId') || '', + mode: url.searchParams.get('mode') || 'view' + }); + } catch (_) { + return String(href || ''); + } + } + + function isOfficeFileName(fileName) { + return Boolean(inferOnlyOfficeFileType(fileName, '')); + } + + function editorAttachmentPaneContext(link) { + var pane = link && typeof link.closest === 'function' ? link.closest('[data-document-pane="true"]') : null; + var roleHost = link && typeof link.closest === 'function' ? link.closest('[data-pane-role]') : null; + var paneRole = ( + pane instanceof HTMLElement && pane.getAttribute('data-pane-role') === 'secondary' + ) || ( + roleHost instanceof HTMLElement && roleHost.getAttribute('data-pane-role') === 'secondary' + ) ? 'secondary' : 'primary'; + var paneDocumentId = ''; + var paneWorkspaceId = ''; + if (pane instanceof HTMLElement) { + paneDocumentId = (pane.getAttribute('data-pane-document-id') || '').trim(); + paneWorkspaceId = (pane.getAttribute('data-pane-workspace-id') || '').trim(); + var shell = pane.querySelector('.document-shell[data-document-id]'); + if (!paneDocumentId && shell instanceof HTMLElement) paneDocumentId = (shell.getAttribute('data-document-id') || '').trim(); + if (!paneWorkspaceId && shell instanceof HTMLElement) paneWorkspaceId = (shell.getAttribute('data-workspace-id') || '').trim(); + } + if (roleHost instanceof HTMLElement) { + if (!paneDocumentId) paneDocumentId = (roleHost.getAttribute('data-document-id') || '').trim(); + if (!paneWorkspaceId) paneWorkspaceId = (roleHost.getAttribute('data-workspace-id') || '').trim(); + var roleHostShell = roleHost.matches('.document-shell') ? roleHost : roleHost.querySelector?.('.document-shell[data-document-id]'); + if (!paneDocumentId && roleHostShell instanceof HTMLElement) paneDocumentId = (roleHostShell.getAttribute('data-document-id') || '').trim(); + if (!paneWorkspaceId && roleHostShell instanceof HTMLElement) paneWorkspaceId = (roleHostShell.getAttribute('data-workspace-id') || '').trim(); + } + return { + paneRole: paneRole, + documentId: paneDocumentId || currentDocumentId() || '', + workspaceId: paneWorkspaceId || resolveWorkspaceId(document.body) || '' + }; + } + + function detailFromEditorAttachmentLink(link) { + var rawHref = link instanceof HTMLAnchorElement ? link.href : ''; + var params = attachmentQueryParams(rawHref); + var paneContext = editorAttachmentPaneContext(link); + var localFilePath = localFileOpenPathFromHref(rawHref); + var fileName = params.get('fileName') || fileNameFromPath(localFilePath) || (link ? link.textContent : '') || '未命名附件'; + var fileType = params.get('fileType') || inferOnlyOfficeFileType(fileName, ''); + var assetId = params.get('assetId') || (link ? link.getAttribute('data-asset-id') : '') || (localFilePath ? 'local-file:' + localFilePath : ''); + var fileUrl = params.get('fileUrl') || ''; + var documentId = params.get('documentId') || paneContext.documentId || ''; + var href = rawHref; + if (!isOnlyOfficeAttachmentHref(rawHref) && fileType) { + fileUrl = rawHref; + href = buildOnlyOfficeOpenUrl({ + fileUrl: fileUrl, + fileName: fileName, + fileType: fileType, + assetId: assetId, + documentId: documentId, + userId: '', + mode: 'view' + }); + } else if (isOnlyOfficeAttachmentHref(rawHref)) { + href = normalizeOnlyOfficeAttachmentHref(rawHref); + } + return { + href: href, + fileUrl: fileUrl, + fileName: fileName, + title: fileName, + fileType: fileType, + assetId: assetId, + documentId: documentId, + workspaceId: paneContext.workspaceId, + paneRole: paneContext.paneRole, + fileSize: (link ? link.getAttribute('data-file-size') : '') || '' + }; + } + + function enhanceEditorAttachmentLink(link) { + if (!(link instanceof HTMLAnchorElement)) return; + var href = link.getAttribute('href') || ''; + var params = attachmentQueryParams(href); + var localFilePath = localFileOpenPathFromHref(href); + var paneContext = editorAttachmentPaneContext(link); + var fileName = params.get('fileName') || fileNameFromPath(localFilePath) || link.textContent || ''; + var className = link.getAttribute('class') || ''; + var shouldEnhance = isOnlyOfficeAttachmentHref(href) + || className.indexOf('mnote-uploaded-attachment-row') >= 0 + || isOfficeFileName(fileName) + || Boolean(localFilePath); + if (!shouldEnhance) return; + if (localFilePath && !isOnlyOfficeAttachmentHref(href)) { + void refreshLocalAttachmentExistence(link); + return; + } + link.setAttribute('data-mnote-attachment-link', 'true'); + var assetId = params.get('assetId') || link.getAttribute('data-asset-id') || (localFilePath ? 'local-file:' + localFilePath : ''); + if (assetId) link.setAttribute('data-asset-id', assetId); + if (isOnlyOfficeAttachmentHref(href)) { + link.setAttribute('href', buildOnlyOfficeOpenPath({ + fileUrl: params.get('fileUrl') || '', + fileName: fileName || '未命名附件', + fileType: params.get('fileType') || inferOnlyOfficeFileType(fileName, ''), + assetId: assetId, + documentId: params.get('documentId') || paneContext.documentId || '', + userId: params.get('userId') || '', + mode: params.get('mode') || 'view' + })); + } + attachmentClassForFileName(fileName).split(/\s+/).forEach(function(name) { + if (name) link.classList.add(name); + }); + link.setAttribute('target', '_blank'); + link.setAttribute('rel', 'noopener noreferrer nofollow'); + void hydrateEditorAttachmentMeta(link); + } + + function enhanceEditorAttachmentLinks() { + document.querySelectorAll('.editor-surface .ProseMirror a[href]').forEach(enhanceEditorAttachmentLink); + void healLegacyOfficeAttachmentParagraphs(); + } + window.__mnoteEnhanceEditorAttachmentLinks = function() { + observeEditorAttachmentRoots(); + enhanceEditorAttachmentLinks(); + }; + + function ensureAttachmentActions() { + var existing = document.querySelector('[data-testid="mnote-attachment-actions"]'); + if (existing instanceof HTMLElement) return existing; + var actions = document.createElement('div'); + actions.className = 'mnote-attachment-actions'; + actions.setAttribute('data-testid', 'mnote-attachment-actions'); + actions.hidden = true; + actions.innerHTML = '' + + '' + + ''; + actions.addEventListener('mouseenter', function() { + if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer); + }); + actions.addEventListener('mouseleave', scheduleHideAttachmentActions); + document.body.appendChild(actions); + return actions; + } + + function positionAttachmentActions(link) { + if (!(link instanceof HTMLElement)) return; + var actions = ensureAttachmentActions(); + var rect = link.getBoundingClientRect(); + actions.hidden = false; + actions.style.left = Math.min(window.innerWidth - 76, Math.max(8, rect.right + 6)) + 'px'; + actions.style.top = Math.max(8, rect.top + (rect.height - 28) / 2) + 'px'; + activeEditorAttachmentLink = link; + } + + function scheduleHideAttachmentActions() { + if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer); + attachmentActionsHideTimer = window.setTimeout(function() { + var actions = document.querySelector('[data-testid="mnote-attachment-actions"]'); + if (actions instanceof HTMLElement) actions.hidden = true; + activeEditorAttachmentLink = null; + }, 220); + } + + async function openEditorAttachmentDetail(detail) { + if (!detail || !detail.href) return; + var localFilePath = localFilePathFromAssetId(detail.assetId); + if (localFilePath) { + if (await openLocalOfficeFileInActiveTab(detail, 'view')) return; + // 非 Office 本地文件:用对应图标类型打开 active tab,失败则新窗口 + void openLocalResourceInActiveTab({ + path: localFilePath, + title: detail.fileName || localFilePath.split('/').pop() || localFilePath, + kind: fileTreeIconKindForFileName(detail.fileName || localFilePath.split('/').pop() || localFilePath), + assetId: detail.assetId, + documentId: detail.documentId || currentDocumentId() || '', + workspaceId: detail.workspaceId || resolveWorkspaceId(document.body) || '', + href: buildLocalFileOpenUrl(localFilePath, false), + paneRole: detail.paneRole || 'primary' + }).then(function(opened) { + if (!opened) window.open(buildLocalFileOpenUrl(localFilePath, false) || detail.href, '_blank', 'noopener,noreferrer'); + }); + return; + } + if (isPdfAttachmentFileName(detail.fileName)) { + void openPdfEditorAttachment(detail); + return; + } + if (isCodeAttachmentFileName(detail.fileName)) { + void openCodeEditorAttachment(detail); + return; + } + var fileType = String(detail.fileType || '').trim(); + if (fileType && typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') { + void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ + objectIdentity: 'resource:onlyoffice:' + (detail.documentId || '') + ':' + (detail.assetId || ''), + assetId: detail.assetId || '', + title: detail.fileName || '附件', + fileName: detail.fileName || '附件', + kind: 'office', + officeUrl: detail.href, + documentId: detail.documentId || '', + workspaceId: detail.workspaceId || '', + paneRole: detail.paneRole || 'primary' + }); + return; + } + window.open(detail.href, '_blank', 'noopener,noreferrer'); + } + + function openEditorAttachmentNewWindow(detail, mode) { + if (!detail) return; + var requestedMode = mode === 'edit' ? 'edit' : 'view'; + var localFilePath = localFilePathFromAssetId(detail.assetId); + if (localFilePath) { + var localFileName = localFilePath.split('/').pop() || localFilePath; + var localOfficeUrl = buildLocalOnlyOfficeOpenUrl(localFilePath, localFileName, String(detail.documentId || currentDocumentId() || '').trim(), detail.assetId, requestedMode); + var localFileUrl = buildLocalFileOpenUrl(localFilePath, false); + window.open(localOfficeUrl || localFileUrl || detail.href, '_blank', 'noopener,noreferrer'); + return; + } + var href = detail.href || detail.fileUrl; + if (detail.fileType && isOnlyOfficeAttachmentHref(href)) { + try { + var url = new URL(href, window.location.origin); + url.searchParams.set('mode', requestedMode); + href = url.toString(); + } catch (_) {} + } else if (detail.fileType) { + href = buildOnlyOfficeOpenUrl({ + fileUrl: detail.fileUrl || href || '', + fileName: detail.fileName || '未命名附件', + fileType: detail.fileType, + assetId: detail.assetId || '', + documentId: detail.documentId || currentDocumentId() || '', + userId: '', + mode: requestedMode + }); + } + window.open(href, '_blank', 'noopener,noreferrer'); + } + + async function openEditorAttachmentEditTab(detail) { + if (!detail) return false; + var localFilePath = localFilePathFromAssetId(detail.assetId); + if (localFilePath) { + if (await openLocalOfficeFileInActiveTab(detail, 'edit')) return true; + } + var href = detail.href || detail.fileUrl; + if (detail.fileType && isOnlyOfficeAttachmentHref(href)) { + try { + var url = new URL(href, window.location.origin); + url.searchParams.set('mode', 'edit'); + href = url.toString(); + } catch (_) {} + } else if (detail.fileType) { + href = buildOnlyOfficeOpenUrl({ + fileUrl: detail.fileUrl || href || '', + fileName: detail.fileName || '未命名附件', + fileType: detail.fileType, + assetId: detail.assetId || '', + documentId: detail.documentId || currentDocumentId() || '', + userId: '', + mode: 'edit' + }); + } + if (detail.fileType && typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') { + var didOpenEditTab = await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ + objectIdentity: 'resource:onlyoffice:' + (detail.documentId || '') + ':' + (detail.assetId || ''), + assetId: detail.assetId || '', + title: detail.fileName || '附件', + fileName: detail.fileName || '附件', + kind: 'office', + officeUrl: href, + documentId: detail.documentId || '', + workspaceId: detail.workspaceId || '', + paneRole: detail.paneRole || 'primary' + }); + if (!didOpenEditTab && href) window.open(href, '_blank', 'noopener,noreferrer'); + return didOpenEditTab; + } + if (href) window.open(href, '_blank', 'noopener,noreferrer'); + return false; + } + + async function resolveEditorAttachmentUrl(detail) { + var assetId = String(detail && detail.assetId || '').trim(); + var localFilePath = localFilePathFromAssetId(assetId); + if (localFilePath) { + var localUrl = buildLocalFileOpenUrl(localFilePath, false); + if (localUrl) { + return { + url: localUrl, + asset: { + file_name: String(detail && detail.fileName || '').trim(), + fileSize: String(detail && detail.fileSize || '').trim() + } + }; + } + } + if (assetId) { + var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), { + method: 'GET', + credentials: 'include', + cache: 'no-store' + }); + var payload = await response.json().catch(function() { return null; }); + if (!response.ok || !payload) { + throw new Error(payload && payload.error ? payload.error : '生成签名链接失败'); + } + var signedUrl = String(payload && payload.signedUrl || '').trim(); + if (!signedUrl) throw new Error('附件链接不可用'); + return { + url: signedUrl, + asset: payload.asset && typeof payload.asset === 'object' ? payload.asset : {} + }; + } + var url = String(detail && (detail.fileUrl || detail.href) || '').trim(); + if (!url) throw new Error('附件链接不可用'); + return { url: url, asset: {} }; + } + + async function openPdfEditorAttachment(detail) { + try { + var resolved = await resolveEditorAttachmentUrl(detail); + window.open(resolved.url, '_blank', 'noopener,noreferrer'); + } catch (error) { + window.alert(error && error.message ? error.message : '打开 PDF 失败'); + } + } + + async function openCodeEditorAttachment(detail) { + var resolved = null; + try { + resolved = await resolveEditorAttachmentUrl(detail); + var size = Number(resolved.asset && (resolved.asset.file_size || resolved.asset.fileSize) || 0); + if (Number.isFinite(size) && size > 1024 * 1024) { + window.open(resolved.url, '_blank', 'noopener,noreferrer'); + return; + } + var response = await fetch(resolved.url, { + method: 'GET', + credentials: 'include', + cache: 'no-store' + }); + if (!response.ok) throw new Error('读取附件内容失败'); + var text = await response.text(); + if (text.length > 1024 * 1024) { + window.open(resolved.url, '_blank', 'noopener,noreferrer'); + return; + } + var editorRoot = document.querySelector('.editor-surface .ProseMirror'); + var editor = editorRoot && editorRoot.editor; + if (!editor || !editor.chain) { + window.open(resolved.url, '_blank', 'noopener,noreferrer'); + return; + } + var title = String(detail.fileName || resolved.asset.file_name || '附件').trim() || '附件'; + var language = inferCodeAttachmentLanguage(title); + editor.chain().focus().insertContent([ + { + type: 'paragraph', + content: [{ type: 'text', text: title }] + }, + { + type: 'codeBlock', + attrs: { language: language }, + content: text ? [{ type: 'text', text: text.replace(/\r\n?/g, '\n') }] : [] + } + ]).run(); + } catch (error) { + console.warn('[mnote attachment] open code attachment failed', error); + if (resolved && resolved.url) { + window.open(resolved.url, '_blank', 'noopener,noreferrer'); + return; + } + window.alert(error && error.message ? error.message : '打开代码附件失败'); + } + } + + async function openEditorAttachmentDownload(detail) { + if (!detail) return; + var localFilePath = localFilePathFromAssetId(detail.assetId); + if (localFilePath) { + var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true); + if (localDownloadUrl) { + triggerBrowserDownload(localDownloadUrl); + return; + } + } + if (detail.assetId) { + try { + var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(detail.assetId), { + method: 'GET', + credentials: 'include' + }); + var payload = await response.json().catch(function() { return null; }); + var signedUrl = String(payload && payload.signedUrl || '').trim(); + if (response.ok && signedUrl) { + window.open(signedUrl, '_blank', 'noopener,noreferrer'); + return; + } + } catch (_) {} + } + var localRelativePath = String(detail.localRelativePath || '').trim(); + if (localRelativePath) { + var localPathDownloadUrl = buildLocalFileOpenUrl(localRelativePath, true); + if (localPathDownloadUrl) { + triggerBrowserDownload(localPathDownloadUrl); + return; + } + } + var target = detail.fileUrl || detail.href; + if (!target) return; + window.open(target, '_blank', 'noopener,noreferrer'); + } + + function openEditorAttachmentMenu(link, trigger, point) { + var detail = detailFromEditorAttachmentLink(link); + var rect = trigger && trigger.getBoundingClientRect ? trigger.getBoundingClientRect() : link.getBoundingClientRect(); + var x = point && typeof point.x === 'number' ? point.x : rect.right; + var y = point && typeof point.y === 'number' ? point.y : rect.bottom + 4; + openTreeContextMenu('attachment', detail, x, y, trigger || link); + } + + function openEditorAttachmentLink(link) { + enhanceEditorAttachmentLink(link); + openEditorAttachmentDetail(detailFromEditorAttachmentLink(link)); + } + + enhanceEditorAttachmentLinks(); + var attachmentEnhanceFrame = 0; + var attachmentEditorObserver = null; + var attachmentObservedEditors = typeof WeakSet === 'function' ? new WeakSet() : null; + function scheduleEditorAttachmentEnhance() { + if (attachmentEnhanceFrame) return; + attachmentEnhanceFrame = window.requestAnimationFrame(function() { + attachmentEnhanceFrame = 0; + enhanceEditorAttachmentLinks(); + observeEditorAttachmentRoots(); + }); + } + function addedNodeMayContainEditorAttachmentLink(node) { + return node instanceof HTMLElement && ( + node.matches('.editor-surface .ProseMirror, .editor-surface .ProseMirror a[href], .editor-surface .ProseMirror p, .editor-surface .ProseMirror span, .editor-surface .ProseMirror div') + || node.querySelector?.('.editor-surface .ProseMirror, .editor-surface .ProseMirror a[href]') + ); + } + function observeEditorAttachmentRoots() { + if (!attachmentEditorObserver) return; + document.querySelectorAll('.editor-surface .ProseMirror').forEach(function(editor) { + if (!(editor instanceof HTMLElement)) return; + if (attachmentObservedEditors && attachmentObservedEditors.has(editor)) return; + if (attachmentObservedEditors) attachmentObservedEditors.add(editor); + attachmentEditorObserver.observe(editor, { childList: true, subtree: true }); + }); + } + attachmentEditorObserver = new MutationObserver(function(records) { + var shouldEnhance = Array.isArray(records) && records.some(function(record) { + if (!record || record.type !== 'childList') return false; + return Array.from(record.addedNodes || []).some(function(node) { + return addedNodeMayContainEditorAttachmentLink(node); + }); + }); + if (!shouldEnhance) return; + scheduleEditorAttachmentEnhance(); + }); + attachmentEditorObserver.observe(document.documentElement, { childList: true, subtree: true }); + observeEditorAttachmentRoots(); + window.addEventListener('mnote:editor-attachment-links-changed', function() { + window.__mnoteEnhanceEditorAttachmentLinks(); + scheduleEditorAttachmentEnhance(); + }); + + function interceptEditorAttachmentLink(event) { + var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]'); + if (!(editorAttachmentLink instanceof HTMLAnchorElement)) return; + event.preventDefault(); + event.stopPropagation(); + if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation(); + openEditorAttachmentLink(editorAttachmentLink); + } + + function suppressEditorAttachmentLinkDefault(event) { + var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]'); + if (!(editorAttachmentLink instanceof HTMLAnchorElement)) return; + event.preventDefault(); + event.stopPropagation(); + if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation(); + } + + window.addEventListener('mousedown', suppressEditorAttachmentLinkDefault, true); + window.addEventListener('click', interceptEditorAttachmentLink, true); + + document.addEventListener('mouseover', function(event) { + var link = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]'); + if (!(link instanceof HTMLAnchorElement)) return; + if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer); + enhanceEditorAttachmentLink(link); + positionAttachmentActions(link); + }); + + document.addEventListener('mouseout', function(event) { + var link = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]'); + if (!(link instanceof HTMLAnchorElement)) return; + var next = event.relatedTarget; + var actions = document.querySelector('[data-testid="mnote-attachment-actions"]'); + if (next && (link.contains(next) || (actions && actions.contains(next)))) return; + scheduleHideAttachmentActions(); + }); + + + return { + detailFromEditorAttachmentLink, + enhanceEditorAttachmentLink, + enhanceEditorAttachmentLinks, + openCodeEditorAttachment, + openEditorAttachmentDetail, + openEditorAttachmentDownload, + openEditorAttachmentEditTab, + openEditorAttachmentNewWindow, + refreshEditorLocalAttachmentExistence, + }; +}; diff --git a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js index 580af23e..dc1a3d02 100644 --- a/rust/crates/mnote-web/browser/sidebar-tree-runtime.js +++ b/rust/crates/mnote-web/browser/sidebar-tree-runtime.js @@ -1,6 +1,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'; (function(){ if (window.__mnoteSidebarTreeRuntimeStarted) return; @@ -25,8 +26,6 @@ import { createSidebarFileTreeOpenRuntime } from './sidebar-filetree-open-runtim focusedRowId: null }; var activeTreeContextMenu = null; - var activeEditorAttachmentLink = null; - var attachmentActionsHideTimer = 0; var pageUiState = { pageOptions: null, historySnapshots: [], @@ -6903,617 +6902,37 @@ import { createSidebarFileTreeOpenRuntime } from './sidebar-filetree-open-runtim else openPageSettingsPopover(); } - function attachmentQueryParams(href) { - try { - return new URL(String(href || ''), window.location.origin).searchParams; - } catch (_) { - return new URLSearchParams(); - } - } - - function localFileOpenPathFromHref(href) { - try { - var url = new URL(String(href || ''), window.location.origin); - if (url.pathname !== '/api/local-folder/files/open') return ''; - return String(url.searchParams.get('path') || '').trim(); - } catch (_) { - return ''; - } - } - - function localFileOpenRootUriFromHref(href) { - try { - var url = new URL(String(href || ''), window.location.origin); - if (url.pathname !== '/api/local-folder/files/open') return ''; - return String(url.searchParams.get('rootUri') || '').trim(); - } catch (_) { - return ''; - } - } - - function buildLocalFileStatusUrl(relativePath, rootUri) { - var effectiveRootUri = String(rootUri || currentRootUri() || '').trim(); - if (!effectiveRootUri || !relativePath) return ''; - var url = new URL('/api/local-folder/files/stat', window.location.origin); - url.searchParams.set('rootUri', effectiveRootUri); - url.searchParams.set('path', relativePath); - return url.toString(); - } - - function setEditorAttachmentMissingState(link, missing) { - if (!(link instanceof HTMLAnchorElement)) return; - var value = Boolean(missing); - if (value) { - link.setAttribute('data-mnote-attachment-missing', 'true'); - link.classList.add('mnote-uploaded-attachment-missing'); - link.setAttribute('aria-label', (link.textContent || '附件') + '(文件不存在)'); - } else { - if (link.getAttribute('data-mnote-attachment-missing') !== 'true') return; - link.removeAttribute('data-mnote-attachment-missing'); - link.classList.remove('mnote-uploaded-attachment-missing'); - link.removeAttribute('aria-label'); - } - } - - async function refreshLocalAttachmentExistence(link) { - if (!(link instanceof HTMLAnchorElement)) return; - var href = link.getAttribute('href') || link.href || ''; - var localFilePath = localFileOpenPathFromHref(href); - if (!localFilePath) return; - var statusUrl = buildLocalFileStatusUrl(localFilePath, localFileOpenRootUriFromHref(href)); - if (!statusUrl) return; - try { - var response = await fetch(statusUrl, { headers: { accept: 'application/json' }, cache: 'no-store' }); - var payload = await response.json().catch(function() { return null; }); - var exists = Boolean(response.ok && payload && payload.ok === true && payload.result && payload.result.exists === true); - setEditorAttachmentMissingState(link, !exists); - } catch (_) {} - } - - function refreshEditorLocalAttachmentExistence() { - document.querySelectorAll('.editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]').forEach(function(link) { - if (link instanceof HTMLAnchorElement) void refreshLocalAttachmentExistence(link); - }); - } - window.__mnoteRefreshEditorLocalAttachmentExistence = refreshEditorLocalAttachmentExistence; - - function fileNameFromPath(path) { - var value = String(path || '').trim(); - return value.indexOf('/') >= 0 ? value.split('/').pop() : value; - } - - function isOnlyOfficeAttachmentHref(href) { - try { - var url = new URL(String(href || ''), window.location.origin); - return url.pathname === '/onlyoffice' && (url.searchParams.has('assetId') || url.searchParams.has('fileName')); - } catch (_) { - return false; - } - } - - function normalizeOnlyOfficeAttachmentHref(href) { - try { - var url = new URL(String(href || ''), window.location.origin); - if (url.pathname !== '/onlyoffice') return String(href || ''); - return buildOnlyOfficeOpenUrl({ - fileUrl: url.searchParams.get('fileUrl') || '', - fileName: url.searchParams.get('fileName') || '未命名附件', - fileType: url.searchParams.get('fileType') || inferOnlyOfficeFileType(url.searchParams.get('fileName') || '', ''), - assetId: url.searchParams.get('assetId') || '', - documentId: url.searchParams.get('documentId') || currentDocumentId() || '', - userId: url.searchParams.get('userId') || '', - mode: url.searchParams.get('mode') || 'view' - }); - } catch (_) { - return String(href || ''); - } - } - - function isOfficeFileName(fileName) { - return Boolean(inferOnlyOfficeFileType(fileName, '')); - } - - function editorAttachmentPaneContext(link) { - var pane = link && typeof link.closest === 'function' ? link.closest('[data-document-pane="true"]') : null; - var roleHost = link && typeof link.closest === 'function' ? link.closest('[data-pane-role]') : null; - var paneRole = ( - pane instanceof HTMLElement && pane.getAttribute('data-pane-role') === 'secondary' - ) || ( - roleHost instanceof HTMLElement && roleHost.getAttribute('data-pane-role') === 'secondary' - ) ? 'secondary' : 'primary'; - var paneDocumentId = ''; - var paneWorkspaceId = ''; - if (pane instanceof HTMLElement) { - paneDocumentId = (pane.getAttribute('data-pane-document-id') || '').trim(); - paneWorkspaceId = (pane.getAttribute('data-pane-workspace-id') || '').trim(); - var shell = pane.querySelector('.document-shell[data-document-id]'); - if (!paneDocumentId && shell instanceof HTMLElement) paneDocumentId = (shell.getAttribute('data-document-id') || '').trim(); - if (!paneWorkspaceId && shell instanceof HTMLElement) paneWorkspaceId = (shell.getAttribute('data-workspace-id') || '').trim(); - } - if (roleHost instanceof HTMLElement) { - if (!paneDocumentId) paneDocumentId = (roleHost.getAttribute('data-document-id') || '').trim(); - if (!paneWorkspaceId) paneWorkspaceId = (roleHost.getAttribute('data-workspace-id') || '').trim(); - var roleHostShell = roleHost.matches('.document-shell') ? roleHost : roleHost.querySelector?.('.document-shell[data-document-id]'); - if (!paneDocumentId && roleHostShell instanceof HTMLElement) paneDocumentId = (roleHostShell.getAttribute('data-document-id') || '').trim(); - if (!paneWorkspaceId && roleHostShell instanceof HTMLElement) paneWorkspaceId = (roleHostShell.getAttribute('data-workspace-id') || '').trim(); - } - return { - paneRole: paneRole, - documentId: paneDocumentId || currentDocumentId() || '', - workspaceId: paneWorkspaceId || resolveWorkspaceId(document.body) || '' - }; - } - - function detailFromEditorAttachmentLink(link) { - var rawHref = link instanceof HTMLAnchorElement ? link.href : ''; - var params = attachmentQueryParams(rawHref); - var paneContext = editorAttachmentPaneContext(link); - var localFilePath = localFileOpenPathFromHref(rawHref); - var fileName = params.get('fileName') || fileNameFromPath(localFilePath) || (link ? link.textContent : '') || '未命名附件'; - var fileType = params.get('fileType') || inferOnlyOfficeFileType(fileName, ''); - var assetId = params.get('assetId') || (link ? link.getAttribute('data-asset-id') : '') || (localFilePath ? 'local-file:' + localFilePath : ''); - var fileUrl = params.get('fileUrl') || ''; - var documentId = params.get('documentId') || paneContext.documentId || ''; - var href = rawHref; - if (!isOnlyOfficeAttachmentHref(rawHref) && fileType) { - fileUrl = rawHref; - href = buildOnlyOfficeOpenUrl({ - fileUrl: fileUrl, - fileName: fileName, - fileType: fileType, - assetId: assetId, - documentId: documentId, - userId: '', - mode: 'view' - }); - } else if (isOnlyOfficeAttachmentHref(rawHref)) { - href = normalizeOnlyOfficeAttachmentHref(rawHref); - } - return { - href: href, - fileUrl: fileUrl, - fileName: fileName, - title: fileName, - fileType: fileType, - assetId: assetId, - documentId: documentId, - workspaceId: paneContext.workspaceId, - paneRole: paneContext.paneRole, - fileSize: (link ? link.getAttribute('data-file-size') : '') || '' - }; - } - - function enhanceEditorAttachmentLink(link) { - if (!(link instanceof HTMLAnchorElement)) return; - var href = link.getAttribute('href') || ''; - var params = attachmentQueryParams(href); - var localFilePath = localFileOpenPathFromHref(href); - var paneContext = editorAttachmentPaneContext(link); - var fileName = params.get('fileName') || fileNameFromPath(localFilePath) || link.textContent || ''; - var className = link.getAttribute('class') || ''; - var shouldEnhance = isOnlyOfficeAttachmentHref(href) - || className.indexOf('mnote-uploaded-attachment-row') >= 0 - || isOfficeFileName(fileName) - || Boolean(localFilePath); - if (!shouldEnhance) return; - if (localFilePath && !isOnlyOfficeAttachmentHref(href)) { - void refreshLocalAttachmentExistence(link); - return; - } - link.setAttribute('data-mnote-attachment-link', 'true'); - var assetId = params.get('assetId') || link.getAttribute('data-asset-id') || (localFilePath ? 'local-file:' + localFilePath : ''); - if (assetId) link.setAttribute('data-asset-id', assetId); - if (isOnlyOfficeAttachmentHref(href)) { - link.setAttribute('href', buildOnlyOfficeOpenPath({ - fileUrl: params.get('fileUrl') || '', - fileName: fileName || '未命名附件', - fileType: params.get('fileType') || inferOnlyOfficeFileType(fileName, ''), - assetId: assetId, - documentId: params.get('documentId') || paneContext.documentId || '', - userId: params.get('userId') || '', - mode: params.get('mode') || 'view' - })); - } - attachmentClassForFileName(fileName).split(/\s+/).forEach(function(name) { - if (name) link.classList.add(name); - }); - link.setAttribute('target', '_blank'); - link.setAttribute('rel', 'noopener noreferrer nofollow'); - void hydrateEditorAttachmentMeta(link); - } - - function enhanceEditorAttachmentLinks() { - document.querySelectorAll('.editor-surface .ProseMirror a[href]').forEach(enhanceEditorAttachmentLink); - void healLegacyOfficeAttachmentParagraphs(); - } - window.__mnoteEnhanceEditorAttachmentLinks = function() { - observeEditorAttachmentRoots(); - enhanceEditorAttachmentLinks(); - }; - - function ensureAttachmentActions() { - var existing = document.querySelector('[data-testid="mnote-attachment-actions"]'); - if (existing instanceof HTMLElement) return existing; - var actions = document.createElement('div'); - actions.className = 'mnote-attachment-actions'; - actions.setAttribute('data-testid', 'mnote-attachment-actions'); - actions.hidden = true; - actions.innerHTML = '' + - '' + - ''; - actions.addEventListener('mouseenter', function() { - if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer); - }); - actions.addEventListener('mouseleave', scheduleHideAttachmentActions); - document.body.appendChild(actions); - return actions; - } - - function positionAttachmentActions(link) { - if (!(link instanceof HTMLElement)) return; - var actions = ensureAttachmentActions(); - var rect = link.getBoundingClientRect(); - actions.hidden = false; - actions.style.left = Math.min(window.innerWidth - 76, Math.max(8, rect.right + 6)) + 'px'; - actions.style.top = Math.max(8, rect.top + (rect.height - 28) / 2) + 'px'; - activeEditorAttachmentLink = link; - } - - function scheduleHideAttachmentActions() { - if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer); - attachmentActionsHideTimer = window.setTimeout(function() { - var actions = document.querySelector('[data-testid="mnote-attachment-actions"]'); - if (actions instanceof HTMLElement) actions.hidden = true; - activeEditorAttachmentLink = null; - }, 220); - } - - async function openEditorAttachmentDetail(detail) { - if (!detail || !detail.href) return; - var localFilePath = localFilePathFromAssetId(detail.assetId); - if (localFilePath) { - if (await openLocalOfficeFileInActiveTab(detail, 'view')) return; - // 非 Office 本地文件:用对应图标类型打开 active tab,失败则新窗口 - void openLocalResourceInActiveTab({ - path: localFilePath, - title: detail.fileName || localFilePath.split('/').pop() || localFilePath, - kind: fileTreeIconKindForFileName(detail.fileName || localFilePath.split('/').pop() || localFilePath), - assetId: detail.assetId, - documentId: detail.documentId || currentDocumentId() || '', - workspaceId: detail.workspaceId || resolveWorkspaceId(document.body) || '', - href: buildLocalFileOpenUrl(localFilePath, false), - paneRole: detail.paneRole || 'primary' - }).then(function(opened) { - if (!opened) window.open(buildLocalFileOpenUrl(localFilePath, false) || detail.href, '_blank', 'noopener,noreferrer'); - }); - return; - } - if (isPdfAttachmentFileName(detail.fileName)) { - void openPdfEditorAttachment(detail); - return; - } - if (isCodeAttachmentFileName(detail.fileName)) { - void openCodeEditorAttachment(detail); - return; - } - var fileType = String(detail.fileType || '').trim(); - if (fileType && typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') { - void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ - objectIdentity: 'resource:onlyoffice:' + (detail.documentId || '') + ':' + (detail.assetId || ''), - assetId: detail.assetId || '', - title: detail.fileName || '附件', - fileName: detail.fileName || '附件', - kind: 'office', - officeUrl: detail.href, - documentId: detail.documentId || '', - workspaceId: detail.workspaceId || '', - paneRole: detail.paneRole || 'primary' - }); - return; - } - window.open(detail.href, '_blank', 'noopener,noreferrer'); - } - - function openEditorAttachmentNewWindow(detail, mode) { - if (!detail) return; - var requestedMode = mode === 'edit' ? 'edit' : 'view'; - var localFilePath = localFilePathFromAssetId(detail.assetId); - if (localFilePath) { - var localFileName = localFilePath.split('/').pop() || localFilePath; - var localOfficeUrl = buildLocalOnlyOfficeOpenUrl(localFilePath, localFileName, String(detail.documentId || currentDocumentId() || '').trim(), detail.assetId, requestedMode); - var localFileUrl = buildLocalFileOpenUrl(localFilePath, false); - window.open(localOfficeUrl || localFileUrl || detail.href, '_blank', 'noopener,noreferrer'); - return; - } - var href = detail.href || detail.fileUrl; - if (detail.fileType && isOnlyOfficeAttachmentHref(href)) { - try { - var url = new URL(href, window.location.origin); - url.searchParams.set('mode', requestedMode); - href = url.toString(); - } catch (_) {} - } else if (detail.fileType) { - href = buildOnlyOfficeOpenUrl({ - fileUrl: detail.fileUrl || href || '', - fileName: detail.fileName || '未命名附件', - fileType: detail.fileType, - assetId: detail.assetId || '', - documentId: detail.documentId || currentDocumentId() || '', - userId: '', - mode: requestedMode - }); - } - window.open(href, '_blank', 'noopener,noreferrer'); - } - - async function openEditorAttachmentEditTab(detail) { - if (!detail) return false; - var localFilePath = localFilePathFromAssetId(detail.assetId); - if (localFilePath) { - if (await openLocalOfficeFileInActiveTab(detail, 'edit')) return true; - } - var href = detail.href || detail.fileUrl; - if (detail.fileType && isOnlyOfficeAttachmentHref(href)) { - try { - var url = new URL(href, window.location.origin); - url.searchParams.set('mode', 'edit'); - href = url.toString(); - } catch (_) {} - } else if (detail.fileType) { - href = buildOnlyOfficeOpenUrl({ - fileUrl: detail.fileUrl || href || '', - fileName: detail.fileName || '未命名附件', - fileType: detail.fileType, - assetId: detail.assetId || '', - documentId: detail.documentId || currentDocumentId() || '', - userId: '', - mode: 'edit' - }); - } - if (detail.fileType && typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') { - var didOpenEditTab = await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ - objectIdentity: 'resource:onlyoffice:' + (detail.documentId || '') + ':' + (detail.assetId || ''), - assetId: detail.assetId || '', - title: detail.fileName || '附件', - fileName: detail.fileName || '附件', - kind: 'office', - officeUrl: href, - documentId: detail.documentId || '', - workspaceId: detail.workspaceId || '', - paneRole: detail.paneRole || 'primary' - }); - if (!didOpenEditTab && href) window.open(href, '_blank', 'noopener,noreferrer'); - return didOpenEditTab; - } - if (href) window.open(href, '_blank', 'noopener,noreferrer'); - return false; - } - - async function resolveEditorAttachmentUrl(detail) { - var assetId = String(detail && detail.assetId || '').trim(); - var localFilePath = localFilePathFromAssetId(assetId); - if (localFilePath) { - var localUrl = buildLocalFileOpenUrl(localFilePath, false); - if (localUrl) { - return { - url: localUrl, - asset: { - file_name: String(detail && detail.fileName || '').trim(), - fileSize: String(detail && detail.fileSize || '').trim() - } - }; - } - } - if (assetId) { - var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), { - method: 'GET', - credentials: 'include', - cache: 'no-store' - }); - var payload = await response.json().catch(function() { return null; }); - if (!response.ok || !payload) { - throw new Error(payload && payload.error ? payload.error : '生成签名链接失败'); - } - var signedUrl = String(payload && payload.signedUrl || '').trim(); - if (!signedUrl) throw new Error('附件链接不可用'); - return { - url: signedUrl, - asset: payload.asset && typeof payload.asset === 'object' ? payload.asset : {} - }; - } - var url = String(detail && (detail.fileUrl || detail.href) || '').trim(); - if (!url) throw new Error('附件链接不可用'); - return { url: url, asset: {} }; - } - - async function openPdfEditorAttachment(detail) { - try { - var resolved = await resolveEditorAttachmentUrl(detail); - window.open(resolved.url, '_blank', 'noopener,noreferrer'); - } catch (error) { - window.alert(error && error.message ? error.message : '打开 PDF 失败'); - } - } - - async function openCodeEditorAttachment(detail) { - var resolved = null; - try { - resolved = await resolveEditorAttachmentUrl(detail); - var size = Number(resolved.asset && (resolved.asset.file_size || resolved.asset.fileSize) || 0); - if (Number.isFinite(size) && size > 1024 * 1024) { - window.open(resolved.url, '_blank', 'noopener,noreferrer'); - return; - } - var response = await fetch(resolved.url, { - method: 'GET', - credentials: 'include', - cache: 'no-store' - }); - if (!response.ok) throw new Error('读取附件内容失败'); - var text = await response.text(); - if (text.length > 1024 * 1024) { - window.open(resolved.url, '_blank', 'noopener,noreferrer'); - return; - } - var editorRoot = document.querySelector('.editor-surface .ProseMirror'); - var editor = editorRoot && editorRoot.editor; - if (!editor || !editor.chain) { - window.open(resolved.url, '_blank', 'noopener,noreferrer'); - return; - } - var title = String(detail.fileName || resolved.asset.file_name || '附件').trim() || '附件'; - var language = inferCodeAttachmentLanguage(title); - editor.chain().focus().insertContent([ - { - type: 'paragraph', - content: [{ type: 'text', text: title }] - }, - { - type: 'codeBlock', - attrs: { language: language }, - content: text ? [{ type: 'text', text: text.replace(/\r\n?/g, '\n') }] : [] - } - ]).run(); - } catch (error) { - console.warn('[mnote attachment] open code attachment failed', error); - if (resolved && resolved.url) { - window.open(resolved.url, '_blank', 'noopener,noreferrer'); - return; - } - window.alert(error && error.message ? error.message : '打开代码附件失败'); - } - } - - async function openEditorAttachmentDownload(detail) { - if (!detail) return; - var localFilePath = localFilePathFromAssetId(detail.assetId); - if (localFilePath) { - var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true); - if (localDownloadUrl) { - triggerBrowserDownload(localDownloadUrl); - return; - } - } - if (detail.assetId) { - try { - var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(detail.assetId), { - method: 'GET', - credentials: 'include' - }); - var payload = await response.json().catch(function() { return null; }); - var signedUrl = String(payload && payload.signedUrl || '').trim(); - if (response.ok && signedUrl) { - window.open(signedUrl, '_blank', 'noopener,noreferrer'); - return; - } - } catch (_) {} - } - var localRelativePath = String(detail.localRelativePath || '').trim(); - if (localRelativePath) { - var localPathDownloadUrl = buildLocalFileOpenUrl(localRelativePath, true); - if (localPathDownloadUrl) { - triggerBrowserDownload(localPathDownloadUrl); - return; - } - } - var target = detail.fileUrl || detail.href; - if (!target) return; - window.open(target, '_blank', 'noopener,noreferrer'); - } - - function openEditorAttachmentMenu(link, trigger, point) { - var detail = detailFromEditorAttachmentLink(link); - var rect = trigger && trigger.getBoundingClientRect ? trigger.getBoundingClientRect() : link.getBoundingClientRect(); - var x = point && typeof point.x === 'number' ? point.x : rect.right; - var y = point && typeof point.y === 'number' ? point.y : rect.bottom + 4; - openTreeContextMenu('attachment', detail, x, y, trigger || link); - } - - function openEditorAttachmentLink(link) { - enhanceEditorAttachmentLink(link); - openEditorAttachmentDetail(detailFromEditorAttachmentLink(link)); - } - - enhanceEditorAttachmentLinks(); - var attachmentEnhanceFrame = 0; - var attachmentEditorObserver = null; - var attachmentObservedEditors = typeof WeakSet === 'function' ? new WeakSet() : null; - function scheduleEditorAttachmentEnhance() { - if (attachmentEnhanceFrame) return; - attachmentEnhanceFrame = window.requestAnimationFrame(function() { - attachmentEnhanceFrame = 0; - enhanceEditorAttachmentLinks(); - observeEditorAttachmentRoots(); - }); - } - function addedNodeMayContainEditorAttachmentLink(node) { - return node instanceof HTMLElement && ( - node.matches('.editor-surface .ProseMirror, .editor-surface .ProseMirror a[href], .editor-surface .ProseMirror p, .editor-surface .ProseMirror span, .editor-surface .ProseMirror div') - || node.querySelector?.('.editor-surface .ProseMirror, .editor-surface .ProseMirror a[href]') - ); - } - function observeEditorAttachmentRoots() { - if (!attachmentEditorObserver) return; - document.querySelectorAll('.editor-surface .ProseMirror').forEach(function(editor) { - if (!(editor instanceof HTMLElement)) return; - if (attachmentObservedEditors && attachmentObservedEditors.has(editor)) return; - if (attachmentObservedEditors) attachmentObservedEditors.add(editor); - attachmentEditorObserver.observe(editor, { childList: true, subtree: true }); - }); - } - attachmentEditorObserver = new MutationObserver(function(records) { - var shouldEnhance = Array.isArray(records) && records.some(function(record) { - if (!record || record.type !== 'childList') return false; - return Array.from(record.addedNodes || []).some(function(node) { - return addedNodeMayContainEditorAttachmentLink(node); - }); - }); - if (!shouldEnhance) return; - scheduleEditorAttachmentEnhance(); - }); - attachmentEditorObserver.observe(document.documentElement, { childList: true, subtree: true }); - observeEditorAttachmentRoots(); - window.addEventListener('mnote:editor-attachment-links-changed', function() { - window.__mnoteEnhanceEditorAttachmentLinks(); - scheduleEditorAttachmentEnhance(); - }); - - function interceptEditorAttachmentLink(event) { - var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]'); - if (!(editorAttachmentLink instanceof HTMLAnchorElement)) return; - event.preventDefault(); - event.stopPropagation(); - if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation(); - openEditorAttachmentLink(editorAttachmentLink); - } - - function suppressEditorAttachmentLinkDefault(event) { - var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]'); - if (!(editorAttachmentLink instanceof HTMLAnchorElement)) return; - event.preventDefault(); - event.stopPropagation(); - if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation(); - } - - window.addEventListener('mousedown', suppressEditorAttachmentLinkDefault, true); - window.addEventListener('click', interceptEditorAttachmentLink, true); - - document.addEventListener('mouseover', function(event) { - var link = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]'); - if (!(link instanceof HTMLAnchorElement)) return; - if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer); - enhanceEditorAttachmentLink(link); - positionAttachmentActions(link); - }); - - document.addEventListener('mouseout', function(event) { - var link = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]'); - if (!(link instanceof HTMLAnchorElement)) return; - var next = event.relatedTarget; - var actions = document.querySelector('[data-testid="mnote-attachment-actions"]'); - if (next && (link.contains(next) || (actions && actions.contains(next)))) return; - scheduleHideAttachmentActions(); + const sidebarAttachmentOpen = createSidebarAttachmentOpenRuntime({ + attachmentClassForFileName: (...args) => attachmentClassForFileName(...args), + buildLocalFileOpenUrl, + buildLocalOnlyOfficeOpenUrl, + buildOnlyOfficeOpenPath, + buildOnlyOfficeOpenUrl, + currentDocumentId, + currentRootUri, + currentWorkspaceSourcePayload, + fileTreeIconKindForFileName, + inferCodeAttachmentLanguage: (...args) => inferCodeAttachmentLanguage(...args), + inferOnlyOfficeFileType, + isCodeAttachmentFileName: (...args) => isCodeAttachmentFileName(...args), + isPdfAttachmentFileName: (...args) => isPdfAttachmentFileName(...args), + localFilePathFromAssetId, + openLocalOfficeFileInActiveTab, + openLocalResourceInActiveTab, + openTreeContextMenu: (...args) => openTreeContextMenu(...args), + resolveWorkspaceId, + triggerBrowserDownload: (...args) => triggerBrowserDownload(...args), + uploadedFileSize, }); + const refreshEditorLocalAttachmentExistence = (...args) => sidebarAttachmentOpen.refreshEditorLocalAttachmentExistence(...args); + const detailFromEditorAttachmentLink = (...args) => sidebarAttachmentOpen.detailFromEditorAttachmentLink(...args); + const enhanceEditorAttachmentLink = (...args) => sidebarAttachmentOpen.enhanceEditorAttachmentLink(...args); + const enhanceEditorAttachmentLinks = (...args) => sidebarAttachmentOpen.enhanceEditorAttachmentLinks(...args); + const openEditorAttachmentDetail = (...args) => sidebarAttachmentOpen.openEditorAttachmentDetail(...args); + const openEditorAttachmentNewWindow = (...args) => sidebarAttachmentOpen.openEditorAttachmentNewWindow(...args); + const openEditorAttachmentEditTab = (...args) => sidebarAttachmentOpen.openEditorAttachmentEditTab(...args); + const openCodeEditorAttachment = (...args) => sidebarAttachmentOpen.openCodeEditorAttachment(...args); + const openEditorAttachmentDownload = (...args) => sidebarAttachmentOpen.openEditorAttachmentDownload(...args); document.addEventListener('click', function(e) { if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return; diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index a9044691..d7f07c09 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-attachment-open-runtime.js", + get(web_shell::sidebar_attachment_open_runtime_asset), + ) .route( "/api/mnote-browser-runtime/sidebar-tree-runtime.js", get(web_shell::sidebar_tree_runtime_asset), @@ -594,6 +598,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-attachment-open-runtime.js", "/api/mnote-browser-runtime/sidebar-tree-runtime.js", "/api/mnote-browser-runtime/tree-live-controller.js", "/api/mnote-browser-runtime/tree-shell-runtime.js", diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index 99796c38..9e569c8e 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_attachment_open_runtime_asset() -> Response { + const JS: &str = include_str!("../../browser/sidebar-attachment-open-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 filetree_keyboard_runtime_asset() -> Response { const JS: &str = include_str!("../../browser/filetree-keyboard-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 30886ce3..521fa635 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_ATTACHMENT_OPEN_RUNTIME_JS: &str = + include_str!("../../../browser/sidebar-attachment-open-runtime.js"); const SIDEBAR_SHELL_RUNTIME_JS: &str = include_str!("../../../browser/sidebar-shell-runtime.js"); const SIDEBAR_WORKSPACE_RUNTIME_JS: &str = @@ -531,7 +533,7 @@ mod tests { assert!(SIDEBAR_TREE_RUNTIME_JS.contains("localForm.append('documentId', documentId)")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("isLocalUploadedAsset(asset)")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildLocalOnlyOfficeOpenUrl")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("localOfficeUrl")); + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("localOfficeUrl")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("fileTreeIconKindForFileName(title)")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("refreshLocalFolderSidebarSnapshot")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("wolai:local-assets-changed")); @@ -1145,29 +1147,34 @@ mod tests { .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_TREE_RUNTIME_JS.contains("function openEditorAttachmentEditTab(detail)")); + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS + .contains("function openEditorAttachmentEditTab(detail)")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("void openEditorAttachmentEditTab(detail);")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openLocalOfficeFileInActiveTab(detail, 'edit')")); + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS + .contains("openLocalOfficeFileInActiveTab(detail, 'edit')")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("openEditorAttachmentNewWindow(detail, 'edit')")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("forceEditMode ? 'edit' : 'view'")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("url.searchParams.set('mode', requestedMode);")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mode: requestedMode")); + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS + .contains("url.searchParams.set('mode', requestedMode);")); + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("mode: requestedMode")); } #[test] fn sidebar_tree_runtime_opens_pdf_and_code_assets_with_builtin_tools() { - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function openPdfEditorAttachment")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function openCodeEditorAttachment")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function resolveEditorAttachmentUrl")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function openPdfEditorAttachment")); + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function openCodeEditorAttachment")); + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function resolveEditorAttachmentUrl")); + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .contains("var localFilePath = localFilePathFromAssetId(assetId)")); - assert!(SIDEBAR_TREE_RUNTIME_JS + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS .contains("var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true)")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("type: 'codeBlock'")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("attrs: { language: language }")); + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("type: 'codeBlock'")); + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("attrs: { language: language }")); assert!(SIDEBAR_TREE_RUNTIME_JS.contains("inferCodeAttachmentLanguage")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (isPdfAttachmentFileName(detail.fileName))")); - assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (isCodeAttachmentFileName(detail.fileName))")); + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS + .contains("if (isPdfAttachmentFileName(detail.fileName))")); + assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS + .contains("if (isCodeAttachmentFileName(detail.fileName))")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS .contains("if (isCodeAttachmentFileName(fileName) && String(asset.document_id")); assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("await openCodeEditorAttachment({"));