refactor: split sidebar attachment open runtime
This commit is contained in:
+1
-1
@@ -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。
|
- [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。
|
- [ ] 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。
|
- [ ] 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。
|
- [x] C7. `sidebar-tree-live-apply-runtime.js`:WS/SSE snapshot/delta/resync DOM apply。
|
||||||
- [ ] C8. `sidebar-tree-runtime.js` 保留为 entrypoint,目标少于 3,000 行。
|
- [ ] C8. `sidebar-tree-runtime.js` 保留为 entrypoint,目标少于 3,000 行。
|
||||||
|
|
||||||
|
|||||||
@@ -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 = '' +
|
||||||
|
'<button type="button" class="mnote-attachment-action" data-testid="mnote-attachment-action-download" data-attachment-action="download" aria-label="下载附件"><span class="material-symbols-outlined" data-icon="download" aria-hidden="true"></span></button>' +
|
||||||
|
'<button type="button" class="mnote-attachment-action" data-testid="mnote-attachment-action-menu" data-attachment-action="menu" aria-label="更多操作"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>';
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createSidebarWorkspaceRuntime } from './sidebar-workspace-runtime.js';
|
import { createSidebarWorkspaceRuntime } from './sidebar-workspace-runtime.js';
|
||||||
import { createSidebarTreeLiveApplyRuntime } from './sidebar-tree-live-apply-runtime.js';
|
import { createSidebarTreeLiveApplyRuntime } from './sidebar-tree-live-apply-runtime.js';
|
||||||
import { createSidebarFileTreeOpenRuntime } from './sidebar-filetree-open-runtime.js';
|
import { createSidebarFileTreeOpenRuntime } from './sidebar-filetree-open-runtime.js';
|
||||||
|
import { createSidebarAttachmentOpenRuntime } from './sidebar-attachment-open-runtime.js';
|
||||||
|
|
||||||
(function(){
|
(function(){
|
||||||
if (window.__mnoteSidebarTreeRuntimeStarted) return;
|
if (window.__mnoteSidebarTreeRuntimeStarted) return;
|
||||||
@@ -25,8 +26,6 @@ import { createSidebarFileTreeOpenRuntime } from './sidebar-filetree-open-runtim
|
|||||||
focusedRowId: null
|
focusedRowId: null
|
||||||
};
|
};
|
||||||
var activeTreeContextMenu = null;
|
var activeTreeContextMenu = null;
|
||||||
var activeEditorAttachmentLink = null;
|
|
||||||
var attachmentActionsHideTimer = 0;
|
|
||||||
var pageUiState = {
|
var pageUiState = {
|
||||||
pageOptions: null,
|
pageOptions: null,
|
||||||
historySnapshots: [],
|
historySnapshots: [],
|
||||||
@@ -6903,617 +6902,37 @@ import { createSidebarFileTreeOpenRuntime } from './sidebar-filetree-open-runtim
|
|||||||
else openPageSettingsPopover();
|
else openPageSettingsPopover();
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachmentQueryParams(href) {
|
const sidebarAttachmentOpen = createSidebarAttachmentOpenRuntime({
|
||||||
try {
|
attachmentClassForFileName: (...args) => attachmentClassForFileName(...args),
|
||||||
return new URL(String(href || ''), window.location.origin).searchParams;
|
buildLocalFileOpenUrl,
|
||||||
} catch (_) {
|
buildLocalOnlyOfficeOpenUrl,
|
||||||
return new URLSearchParams();
|
buildOnlyOfficeOpenPath,
|
||||||
}
|
buildOnlyOfficeOpenUrl,
|
||||||
}
|
currentDocumentId,
|
||||||
|
currentRootUri,
|
||||||
function localFileOpenPathFromHref(href) {
|
currentWorkspaceSourcePayload,
|
||||||
try {
|
fileTreeIconKindForFileName,
|
||||||
var url = new URL(String(href || ''), window.location.origin);
|
inferCodeAttachmentLanguage: (...args) => inferCodeAttachmentLanguage(...args),
|
||||||
if (url.pathname !== '/api/local-folder/files/open') return '';
|
inferOnlyOfficeFileType,
|
||||||
return String(url.searchParams.get('path') || '').trim();
|
isCodeAttachmentFileName: (...args) => isCodeAttachmentFileName(...args),
|
||||||
} catch (_) {
|
isPdfAttachmentFileName: (...args) => isPdfAttachmentFileName(...args),
|
||||||
return '';
|
localFilePathFromAssetId,
|
||||||
}
|
openLocalOfficeFileInActiveTab,
|
||||||
}
|
openLocalResourceInActiveTab,
|
||||||
|
openTreeContextMenu: (...args) => openTreeContextMenu(...args),
|
||||||
function localFileOpenRootUriFromHref(href) {
|
resolveWorkspaceId,
|
||||||
try {
|
triggerBrowserDownload: (...args) => triggerBrowserDownload(...args),
|
||||||
var url = new URL(String(href || ''), window.location.origin);
|
uploadedFileSize,
|
||||||
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 = '' +
|
|
||||||
'<button type="button" class="mnote-attachment-action" data-testid="mnote-attachment-action-download" data-attachment-action="download" aria-label="下载附件"><span class="material-symbols-outlined" data-icon="download" aria-hidden="true"></span></button>' +
|
|
||||||
'<button type="button" class="mnote-attachment-action" data-testid="mnote-attachment-action-menu" data-attachment-action="menu" aria-label="更多操作"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>';
|
|
||||||
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 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) {
|
document.addEventListener('click', function(e) {
|
||||||
if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
|
if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
|
||||||
|
|||||||
@@ -138,6 +138,10 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
"/api/mnote-browser-runtime/sidebar-filetree-open-runtime.js",
|
"/api/mnote-browser-runtime/sidebar-filetree-open-runtime.js",
|
||||||
get(web_shell::sidebar_filetree_open_runtime_asset),
|
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(
|
.route(
|
||||||
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
|
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
|
||||||
get(web_shell::sidebar_tree_runtime_asset),
|
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-workspace-runtime.js",
|
||||||
"/api/mnote-browser-runtime/sidebar-tree-live-apply-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-open-runtime.js",
|
||||||
|
"/api/mnote-browser-runtime/sidebar-attachment-open-runtime.js",
|
||||||
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
|
"/api/mnote-browser-runtime/sidebar-tree-runtime.js",
|
||||||
"/api/mnote-browser-runtime/tree-live-controller.js",
|
"/api/mnote-browser-runtime/tree-live-controller.js",
|
||||||
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
||||||
|
|||||||
@@ -784,6 +784,20 @@ pub async fn sidebar_filetree_open_runtime_asset() -> Response {
|
|||||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
.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 {
|
pub async fn filetree_keyboard_runtime_asset() -> Response {
|
||||||
const JS: &str = include_str!("../../browser/filetree-keyboard-runtime.js");
|
const JS: &str = include_str!("../../browser/filetree-keyboard-runtime.js");
|
||||||
Response::builder()
|
Response::builder()
|
||||||
|
|||||||
@@ -203,6 +203,8 @@ mod tests {
|
|||||||
include_str!("../../../browser/sidebar-tree-live-apply-runtime.js");
|
include_str!("../../../browser/sidebar-tree-live-apply-runtime.js");
|
||||||
const SIDEBAR_FILETREE_OPEN_RUNTIME_JS: &str =
|
const SIDEBAR_FILETREE_OPEN_RUNTIME_JS: &str =
|
||||||
include_str!("../../../browser/sidebar-filetree-open-runtime.js");
|
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 =
|
const SIDEBAR_SHELL_RUNTIME_JS: &str =
|
||||||
include_str!("../../../browser/sidebar-shell-runtime.js");
|
include_str!("../../../browser/sidebar-shell-runtime.js");
|
||||||
const SIDEBAR_WORKSPACE_RUNTIME_JS: &str =
|
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("localForm.append('documentId', documentId)"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("isLocalUploadedAsset(asset)"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("isLocalUploadedAsset(asset)"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("buildLocalOnlyOfficeOpenUrl"));
|
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("fileTreeIconKindForFileName(title)"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("refreshLocalFolderSidebarSnapshot"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("refreshLocalFolderSidebarSnapshot"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("wolai:local-assets-changed"));
|
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: '新窗口编辑' }"));
|
.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("function withOfficeEditModeGuard(callback)"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("data-mnote-last-office-edit-mode-requested"));
|
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("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_TREE_RUNTIME_JS.contains("openEditorAttachmentNewWindow(detail, 'edit')"));
|
||||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("forceEditMode ? 'edit' : 'view'"));
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("forceEditMode ? 'edit' : 'view'"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("url.searchParams.set('mode', requestedMode);"));
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("mode: requestedMode"));
|
.contains("url.searchParams.set('mode', requestedMode);"));
|
||||||
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("mode: requestedMode"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn sidebar_tree_runtime_opens_pdf_and_code_assets_with_builtin_tools() {
|
fn sidebar_tree_runtime_opens_pdf_and_code_assets_with_builtin_tools() {
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function openPdfEditorAttachment"));
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function openPdfEditorAttachment"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function openCodeEditorAttachment"));
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function openCodeEditorAttachment"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("function resolveEditorAttachmentUrl"));
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("function resolveEditorAttachmentUrl"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
||||||
.contains("var localFilePath = localFilePathFromAssetId(assetId)"));
|
.contains("var localFilePath = localFilePathFromAssetId(assetId)"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
||||||
.contains("var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true)"));
|
.contains("var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true)"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("type: 'codeBlock'"));
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("type: 'codeBlock'"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("attrs: { language: language }"));
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS.contains("attrs: { language: language }"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("inferCodeAttachmentLanguage"));
|
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("inferCodeAttachmentLanguage"));
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (isPdfAttachmentFileName(detail.fileName))"));
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
||||||
assert!(SIDEBAR_TREE_RUNTIME_JS.contains("if (isCodeAttachmentFileName(detail.fileName))"));
|
.contains("if (isPdfAttachmentFileName(detail.fileName))"));
|
||||||
|
assert!(SIDEBAR_ATTACHMENT_OPEN_RUNTIME_JS
|
||||||
|
.contains("if (isCodeAttachmentFileName(detail.fileName))"));
|
||||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS
|
||||||
.contains("if (isCodeAttachmentFileName(fileName) && String(asset.document_id"));
|
.contains("if (isCodeAttachmentFileName(fileName) && String(asset.document_id"));
|
||||||
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("await openCodeEditorAttachment({"));
|
assert!(SIDEBAR_FILETREE_OPEN_RUNTIME_JS.contains("await openCodeEditorAttachment({"));
|
||||||
|
|||||||
Reference in New Issue
Block a user