import { localMarkdownDocumentIdFromRelativePath, localMarkdownRelativePathFromDocumentId, localizeTiptapAssetUrls, } from './document-tiptap-conversion-runtime.js'; /** * @typedef {Object} MNoteWorkspacePath * @property {'mnote.workspace_path.v1'} schema * @property {string} workspaceId * @property {string} sourceKind * @property {string} rootUri * @property {string} relativePath * @property {string} documentId * @property {string} objectIdentity * @property {string} assetId * @property {string} resourceKind * * @typedef {Object} MNoteOpenEditorEntry * @property {string} objectIdentity * @property {MNoteWorkspacePath|null} workspacePath * @property {'primary'|'secondary'} paneRole * @property {string} editorKind * @property {boolean} active * @property {string} dirtyState * @property {boolean} preview * @property {boolean} pinned * @property {number} lastActiveAt * * @typedef {Object} MNoteOpenEditorsSnapshot * @property {'mnote.open_editors_snapshot.v1'} schema * @property {number} generatedAt * @property {string} activeObjectIdentity * @property {MNoteOpenEditorEntry[]} editors * @property {{primary: Object, secondary: Object}} groups */ export const createResourceTabRuntime = (dependencies = {}) => { const { loadRuntime, createEditorViewBinding, ensureLocalFolderEventChannel, unmountEditorViewBinding, setStatus, documentSessionRegistry, sessionHasRecentLocalInput, currentUrl, replaceUrlState, applyStoredSecondaryWidth, workspace, setSecondaryEditorHostVisible, markIntendedSlashRoot, toTiptapDocument, openMindmapResourceTab, unmountMindmapPane, paneViewRegistry, rootSelector, currentDocumentId, currentWorkspaceId, secondaryQueryParamNames = [], } = dependencies; const selector = rootSelector || '[data-testid="mnote-leptos-tiptap-island-editor-root"]'; const resourceTabRegistry = new Map(); const resourceTabMru = { primary: [], secondary: [] }; const resourceTabMruMax = 20; const resourceTabCloseGuardAttribute = 'data-resource-tab-close-guarded'; let onlyofficeBridgeReadyListenerBound = false; const normalizePaneRole = (paneRole) => String(paneRole || '').trim() === 'secondary' ? 'secondary' : 'primary'; const resourceTabRegistryKey = (paneRole, objectIdentity) => `${normalizePaneRole(paneRole)}::${String(objectIdentity || '').trim()}`; const resourceTabHostNodes = (paneRole = 'primary') => { const role = normalizePaneRole(paneRole); return { paneRole: role, strip: document.querySelector(`[data-mnote-main-tab-strip][data-pane-role="${role}"]`), pageTab: document.querySelector(`[data-mnote-main-tab="page"][data-pane-role="${role}"]`), pagePanel: document.querySelector(`[data-mnote-page-tab-panel][data-pane-role="${role}"]`), host: document.querySelector(`[data-mnote-resource-tab-host][data-pane-role="${role}"]`), panelRoot: document.querySelector(`[data-mnote-resource-tab-panel-root][data-pane-role="${role}"]`), }; }; const resourceTabBadgeKind = (input, kind) => { const title = String(input?.title || input?.fileName || input?.path || '').trim().toLowerCase(); if (kind === 'mindmap') return 'mindmap'; if (kind === 'office') { if (/\.(ppt|pptx|odp)$/i.test(title)) return 'ppt'; if (/\.(xls|xlsx|ods|csv)$/i.test(title)) return 'sheet'; return 'word'; } if (kind === 'pdf') return 'pdf'; if (kind === 'markdown' || kind === 'text' || kind === 'code') return 'code'; if (kind === 'image') return 'image'; return 'file'; }; const resourceTabWidthType = (entry) => { if (!entry) return ''; const kind = String(entry.kind || '').trim(); if (kind === 'office') { const badgeKind = String(entry.badgeKind || resourceTabBadgeKind(entry, kind) || '').trim(); if (badgeKind === 'ppt') return 'ppt'; if (badgeKind === 'sheet') return 'excel'; return 'word'; } if (kind === 'pdf') return 'pdf'; if (kind === 'mindmap') return 'mindmap'; if (kind === 'markdown' || kind === 'text' || kind === 'code') return 'markdown'; return ''; }; const currentWebShellWorkspaceId = () => { const explicit = typeof currentWorkspaceId === 'function' ? String(currentWorkspaceId() || '').trim() : ''; if (explicit) return explicit; const fromBody = document.body?.dataset?.workspaceId || ''; if (fromBody) return String(fromBody).trim(); const shell = document.querySelector('.document-shell[data-workspace-id], [data-document-pane="true"][data-pane-role="primary"][data-pane-workspace-id]'); if (shell instanceof HTMLElement) { const value = shell.getAttribute('data-workspace-id') || shell.getAttribute('data-pane-workspace-id') || ''; if (value) return value.trim(); } try { return currentUrl().searchParams.get('workspaceId') || ''; } catch (_) { return ''; } }; const currentFileTreeWorkspacePath = () => { try { const row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-local-relative-path]'); const reader = window.__mnoteFileTreeRuntime?.readWorkspacePathFromRow; if (row instanceof HTMLElement && typeof reader === 'function') { const workspacePath = reader(row); if (workspacePath && typeof workspacePath === 'object') { return { ...workspacePath, sourceKind: String(workspacePath.sourceKind || row.getAttribute('data-source-kind') || '').trim(), rootUri: String(workspacePath.rootUri || row.getAttribute('data-root-uri') || '').trim(), }; } } if (row instanceof HTMLElement) { return { sourceKind: String(row.getAttribute('data-source-kind') || '').trim(), rootUri: String(row.getAttribute('data-root-uri') || '').trim(), }; } } catch (_) {} return null; }; const currentWebShellDocumentId = () => { const explicit = typeof currentDocumentId === 'function' ? String(currentDocumentId() || '').trim() : ''; if (explicit) return explicit; const fromBody = document.body?.dataset?.documentId || ''; if (fromBody) return String(fromBody).trim(); const pageTab = document.querySelector('[data-mnote-main-tab="page"]'); if (pageTab instanceof HTMLElement) return String(pageTab.getAttribute('data-document-id') || '').trim(); return ''; }; const documentIdForPane = (paneRole) => { const role = normalizePaneRole(paneRole); const nodes = resourceTabHostNodes(role); const fromTab = String(nodes.pageTab?.getAttribute?.('data-document-id') || '').trim(); if (fromTab) return fromTab; const pane = document.querySelector(`[data-document-pane="true"][data-pane-role="${role}"]`); if (pane instanceof HTMLElement) { const fromPane = String(pane.getAttribute('data-pane-document-id') || '').trim(); if (fromPane) return fromPane; const root = pane.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"][data-document-id], .document-shell[data-document-id]'); if (root instanceof HTMLElement) { const fromRoot = String(root.getAttribute('data-document-id') || '').trim(); if (fromRoot) return fromRoot; } } return role === 'primary' ? currentWebShellDocumentId() : ''; }; const currentWebShellSourceKind = () => { try { return currentUrl().searchParams.get('sourceKind') || document.body?.dataset?.mnoteSourceKind || currentFileTreeWorkspacePath()?.sourceKind || ''; } catch (_) { return document.body?.dataset?.mnoteSourceKind || currentFileTreeWorkspacePath()?.sourceKind || ''; } }; const currentWebShellRootUri = () => { try { return currentUrl().searchParams.get('rootUri') || document.body?.dataset?.mnoteRootUri || currentFileTreeWorkspacePath()?.rootUri || ''; } catch (_) { return document.body?.dataset?.mnoteRootUri || currentFileTreeWorkspacePath()?.rootUri || ''; } }; const buildWorkspacePath = ({ workspacePath, workspaceId, sourceKind, rootUri, relativePath, documentId, objectIdentity, assetId, resourceKind, } = {}) => { const fromInput = workspacePath && typeof workspacePath === 'object' ? workspacePath : null; if (fromInput && String(fromInput.schema || '') === 'mnote.workspace_path.v1') { return { schema: 'mnote.workspace_path.v1', workspaceId: String(fromInput.workspaceId || workspaceId || '').trim(), sourceKind: String(fromInput.sourceKind || sourceKind || '').trim(), rootUri: String(fromInput.rootUri || rootUri || '').trim(), relativePath: String(fromInput.relativePath || fromInput.localRelativePath || relativePath || '').trim(), documentId: String(fromInput.documentId || documentId || '').trim(), objectIdentity: fromInput.objectIdentity ?? String(objectIdentity || '').trim(), assetId: String(fromInput.assetId || assetId || '').trim(), resourceKind: String(fromInput.resourceKind || fromInput.objectKind || resourceKind || '').trim(), }; } const structuredObjectIdentity = objectIdentity && typeof objectIdentity === 'object' ? objectIdentity : null; const objectIdentityText = structuredObjectIdentity ? '' : String(objectIdentity || '').trim(); const id = String(objectIdentityText || documentId || assetId || relativePath || '').trim(); if (!id) return null; return { schema: 'mnote.workspace_path.v1', workspaceId: String(workspaceId || '').trim(), sourceKind: String(sourceKind || '').trim(), rootUri: String(rootUri || '').trim(), relativePath: String(relativePath || '').trim(), documentId: String(documentId || '').trim(), objectIdentity: structuredObjectIdentity || objectIdentityText, assetId: String(assetId || '').trim(), resourceKind: String(resourceKind || '').trim(), }; }; const normalizeResourceTabKind = (input) => { const kind = String(input?.kind || '').trim().toLowerCase(); const title = String(input?.title || input?.fileName || input?.path || '').trim().toLowerCase(); if (kind === 'mindmap' || kind === 'office' || kind === 'pdf' || kind === 'image' || kind === 'markdown' || kind === 'text' || kind === 'code') return kind; if (/\.(doc|docx|ppt|pptx|xls|xlsx|odt|odp|ods)$/i.test(title)) return 'office'; if (/\.pdf$/i.test(title)) return 'pdf'; if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(title)) return 'image'; if (/\.(md|markdown)$/i.test(title)) return 'markdown'; if (/\.(txt|log)$/i.test(title)) return 'text'; if (/\.(rs|ts|tsx|js|jsx|json|css|scss|html|xml|py|go|java|kt|swift|c|h|cpp|hpp|sh|bash|zsh|toml|yaml|yml|sql)$/i.test(title)) return 'code'; return 'file'; }; const resolveResourceOpen = (input = {}) => { const kind = normalizeResourceTabKind(input); const badgeKind = resourceTabBadgeKind(input, kind); const rawTarget = String(input?.openTarget || '').trim().toLowerCase(); const openTarget = rawTarget === 'new-window' || rawTarget === 'side' ? rawTarget : 'active-tab'; const editable = kind === 'markdown' || kind === 'text' || kind === 'code'; const defaultOpenMode = kind === 'office' && openTarget === 'active-tab' ? 'active-tab-iframe' : openTarget; const viewerUrl = kind === 'office' ? String(input?.officeUrl || input?.href || '').trim() : String(input?.href || '').trim(); return { editorKind: kind, badgeKind, defaultOpenMode, editable, viewerUrl, openTarget }; }; const touchResourceTabMru = (paneRole, key) => { const role = normalizePaneRole(paneRole); const id = String(key || '').trim(); if (!id) return; const list = resourceTabMru[role] || (resourceTabMru[role] = []); const index = list.indexOf(id); if (index >= 0) list.splice(index, 1); list.unshift(id); if (list.length > resourceTabMruMax) list.length = resourceTabMruMax; }; const setResourceTabLastActive = (key) => { const entry = resourceTabRegistry.get(String(key || '').trim()); if (!entry) return; const timestamp = Date.now(); entry.lastActiveAt = timestamp; if (entry.session) entry.session.lastActiveAt = timestamp; }; const removeFromResourceTabMru = (paneRole, key) => { const list = resourceTabMru[normalizePaneRole(paneRole)] || []; const index = list.indexOf(key); if (index >= 0) list.splice(index, 1); }; const lastActiveResourceTabKey = (paneRole = 'primary') => { const list = resourceTabMru[normalizePaneRole(paneRole)] || []; for (const key of list) { if (resourceTabRegistry.has(key)) return key; } return ''; }; const resourceTabCloseGuardReason = (session) => { if (!session) return ''; const bufferDirtyState = String(session.bufferDirtyState || '').trim(); if (bufferDirtyState === 'Dirty' || bufferDirtyState === 'Stale' || bufferDirtyState === 'Deleted' || bufferDirtyState === 'ExternalModified') { return bufferDirtyState; } const hasUnsavedChanges = session.dirty || Boolean(session.saveTimer) || sessionHasRecentLocalInput(session) || (session.currentSerialized && session.currentSerialized !== session.lastPersistedSerialized); if (hasUnsavedChanges) return 'dirty'; if (session.saving) return 'saving'; if (session.hasExternalConflict) return 'hasExternalConflict'; return ''; }; const documentSessionForPane = (paneRole, documentId) => { const id = String(documentId || '').trim(); if (!id || !documentSessionRegistry || typeof documentSessionRegistry.values !== 'function') return null; const role = normalizePaneRole(paneRole); return Array.from(documentSessionRegistry.values()).find((session) => ( session && session.sessionKind !== 'resource' && String(session.documentId || '').trim() === id && Array.from(session.views?.values?.() || []).some((view) => normalizePaneRole(view?.runtimeDescriptor?.paneRole) === role) )) || Array.from(documentSessionRegistry.values()).find((session) => ( session && session.sessionKind !== 'resource' && String(session.documentId || '').trim() === id )) || null; }; const documentSessionDirtyState = (session) => { if (!session) return ''; const bufferDirtyState = String(session.bufferDirtyState || '').trim(); if (bufferDirtyState === 'Dirty' || bufferDirtyState === 'Stale' || bufferDirtyState === 'Deleted' || bufferDirtyState === 'ExternalModified') return bufferDirtyState; if (session.hasExternalConflict) return 'ExternalModified'; if (session.dirty || Boolean(session.saveTimer) || sessionHasRecentLocalInput(session)) return 'Dirty'; if (session.saving) return 'Saving'; return ''; }; const resourceTabBufferStateUrl = (entry) => { const session = entry?.session; if (!session || session.sourceKind !== 'local_folder') return null; const relativePath = String(session.resourcePath || entry?.workspacePath?.relativePath || '').trim(); const rootUri = String(session.rootUri || entry?.workspacePath?.rootUri || '').trim(); if (!relativePath || !rootUri) return null; const url = new URL('/api/documents/buffer-state', window.location.origin); url.searchParams.set('documentId', String(session.documentId || entry.objectIdentity || '')); url.searchParams.set('sourceKind', 'local_folder'); url.searchParams.set('rootUri', rootUri); url.searchParams.set('relativePath', relativePath); const workspaceId = String(session.workspaceId || entry?.workspacePath?.workspaceId || '').trim(); if (workspaceId) url.searchParams.set('workspaceId', workspaceId); return url; }; const applyResourceTabCloseGuard = (entry) => { if (!entry?.tab || !(entry.tab instanceof HTMLElement)) return; const reason = resourceTabCloseGuardReason(entry.session); if (reason) { entry.tab.setAttribute(resourceTabCloseGuardAttribute, reason); entry.tab.classList.add('is-close-guarded'); } else { entry.tab.removeAttribute(resourceTabCloseGuardAttribute); entry.tab.classList.remove('is-close-guarded'); } }; const refreshResourceTabBufferState = async (entry) => { const url = resourceTabBufferStateUrl(entry); if (!url || !entry?.session) return null; try { const response = await fetch(url.toString(), { cache: 'no-store', headers: { accept: 'application/json' } }); const payload = await response.json().catch(() => null); if (!response.ok || !payload || payload.ok !== true || !payload.result) return null; const dirtyState = String(payload.result.dirtyState || '').trim(); if (dirtyState) entry.session.bufferDirtyState = dirtyState; if (typeof payload.result.fileVersion === 'string' && payload.result.fileVersion.trim()) { entry.session.fileVersion = payload.result.fileVersion.trim(); } applyResourceTabCloseGuard(entry); return payload.result; } catch (_) { return null; } }; const syncResourceTabCloseGuard = (entry) => { applyResourceTabCloseGuard(entry); void refreshResourceTabBufferState(entry); }; const syncResourceSessionTabGuards = (session) => { if (!session || session.sessionKind !== 'resource') return; resourceTabRegistry.forEach((entry) => { if (entry.session === session) syncResourceTabCloseGuard(entry); }); }; const officeBridgeDebugForEntry = (entry) => { if (!entry?.panel || !(entry.panel instanceof HTMLElement)) return null; const frame = entry.panel.querySelector('iframe.mnote-resource-tab-frame'); if (!(frame instanceof HTMLIFrameElement)) return null; try { const debug = frame.contentWindow?.__MNOTE_ONLYOFFICE_DEBUG__; return debug && typeof debug === 'object' ? debug : null; } catch (_) { return null; } }; const openEditorsSnapshotEntry = (entry, key, generatedAt = Date.now()) => { const active = entry?.tab instanceof HTMLElement ? entry.tab.getAttribute('aria-selected') === 'true' : false; const documentId = String(entry?.documentId || entry?.ownerDocumentId || entry?.session?.ownerDocumentId || '').trim(); const workspaceId = String(entry?.workspaceId || entry?.session?.workspaceId || currentWebShellWorkspaceId() || '').trim(); const sourceKind = String(entry?.sourceKind || entry?.session?.sourceKind || currentWebShellSourceKind() || '').trim(); const rootUri = String(entry?.rootUri || entry?.session?.rootUri || currentWebShellRootUri() || '').trim(); const relativePath = String(entry?.path || entry?.session?.resourcePath || '').trim(); const objectIdentity = String(entry?.objectIdentity || key || '').trim(); const assetId = String(entry?.assetId || entry?.session?.assetId || '').trim(); const kind = normalizeResourceTabKind(entry); const dirtyState = resourceTabCloseGuardReason(entry?.session); const officeBridgeDebug = kind === 'office' ? officeBridgeDebugForEntry(entry) : null; const onlyofficeSessionId = String( officeBridgeDebug?.bridgeSessionId || entry?.onlyofficeSessionId || entry?.bridgeSessionId || '', ).trim(); return { objectIdentity, workspacePath: buildWorkspacePath({ workspaceId, sourceKind, rootUri, relativePath, documentId, objectIdentity, assetId, resourceKind: kind, workspacePath: entry?.workspacePath, }), paneRole: normalizePaneRole(entry?.paneRole), documentId, workspaceId, title: String(entry?.title || '资源').trim() || '资源', kind, editorKind: kind, badgeKind: entry?.tab instanceof HTMLElement ? String(entry.tab.getAttribute('data-mnote-tab-badge-kind') || resourceTabBadgeKind(entry, entry.kind)).trim() : resourceTabBadgeKind(entry, entry?.kind), active, dirtyState, dirtyGuard: dirtyState, assetId, path: relativePath, onlyofficeSessionId, bridgeSessionId: onlyofficeSessionId, bridgeSessionReady: Boolean(onlyofficeSessionId), bridgeDocumentId: String(officeBridgeDebug?.documentId || '').trim(), bridgeAssetId: String(officeBridgeDebug?.assetId || '').trim(), lastActiveAt: Number(entry?.session?.lastActiveAt || entry?.lastActiveAt || 0) || (active ? generatedAt : 0), preview: false, pinned: false, }; }; const buildOpenEditorsSnapshot = () => { const generatedAt = Date.now(); const pageEntries = ['primary', 'secondary'].map((paneRole) => { const nodes = resourceTabHostNodes(paneRole); const documentId = documentIdForPane(paneRole); const workspaceId = String(nodes.pageTab?.getAttribute?.('data-workspace-id') || currentWebShellWorkspaceId() || '').trim(); const sourceKind = currentWebShellSourceKind(); const rootUri = currentWebShellRootUri(); const relativePath = sourceKind === 'local_folder' ? localMarkdownRelativePathFromDocumentId(documentId) : ''; const objectIdentity = `page:${paneRole}`; const workspaceObjectIdentity = { objectKind: 'page', documentId, blockId: null, assetId: null, }; const active = nodes.pageTab instanceof HTMLElement ? nodes.pageTab.getAttribute('aria-selected') === 'true' : false; const pageSession = documentSessionForPane(paneRole, documentId); const dirtyState = documentSessionDirtyState(pageSession); const pageTitle = nodes.pageTab instanceof HTMLElement ? String(nodes.pageTab.querySelector('.mnote-main-tab-title')?.textContent || '页面').trim() : '页面'; return { objectIdentity, workspacePath: buildWorkspacePath({ workspaceId, sourceKind, rootUri, relativePath, documentId, objectIdentity: workspaceObjectIdentity, assetId: '', resourceKind: 'page', }), paneRole, documentId, workspaceId, title: pageTitle || '页面', kind: 'page', editorKind: 'page', badgeKind: 'code', active, dirtyState, dirtyGuard: dirtyState, lastActiveAt: active ? generatedAt : 0, preview: false, pinned: true, }; }).filter((entry) => entry.paneRole === 'primary' || entry.documentId || document.querySelector('[data-document-pane="true"][data-pane-role="secondary"][data-pane-visible="true"]')); const resources = []; resourceTabRegistry.forEach((entry, key) => { resources.push(openEditorsSnapshotEntry(entry, key, generatedAt)); }); const editors = [...pageEntries, ...resources]; const groupForPane = (paneRole) => { const role = normalizePaneRole(paneRole); const groupEditors = editors.filter((entry) => normalizePaneRole(entry.paneRole) === role); const groupResources = resources.filter((entry) => normalizePaneRole(entry.paneRole) === role); const groupActive = groupEditors.find((entry) => entry.active); return { paneRole: role, activeObjectIdentity: groupActive?.objectIdentity || '', editors: groupEditors, resourceEditors: groupResources, }; }; const groups = { primary: groupForPane('primary'), secondary: groupForPane('secondary'), }; const activeObjectIdentity = groups.primary.activeObjectIdentity || groups.secondary.activeObjectIdentity || ''; return { schema: 'mnote.open_editors_snapshot.v1', generatedAt, activeObjectIdentity, editors, resourceEditors: resources, groups, }; }; const syncOpenEditorsSnapshot = () => { const snapshot = buildOpenEditorsSnapshot(); window.__mnoteOpenEditorsSnapshot = snapshot; document.documentElement.setAttribute('data-mnote-open-editors-count', String(snapshot.editors.length)); document.documentElement.setAttribute('data-mnote-active-editor', snapshot.activeObjectIdentity || ''); window.dispatchEvent(new CustomEvent('mnote:open-editors-snapshot', { detail: snapshot })); return snapshot; }; const showResourceTabCloseGuardNotice = (entry, reason) => { const nodes = resourceTabHostNodes(entry?.paneRole || 'primary'); const messages = { dirty: '当前资源有未保存的修改,保存完成后再关闭。', saving: '当前资源正在保存中,请稍后再关闭。', hasExternalConflict: '当前资源存在外部冲突,请先处理冲突。', }; const message = messages[reason] || '当前资源暂时无法关闭。'; let notice = document.getElementById('mnote-resource-close-guard-notice'); if (!notice) { notice = document.createElement('div'); notice.id = 'mnote-resource-close-guard-notice'; notice.className = 'mnote-close-guard-notice'; notice.setAttribute('role', 'status'); notice.setAttribute('aria-live', 'polite'); notice.setAttribute('data-mnote-resource-close-guard', ''); const parent = nodes.strip?.parentNode; if (parent instanceof HTMLElement) { const panels = parent.querySelector('.mnote-main-tab-panels'); if (panels && panels.parentNode === parent) parent.insertBefore(notice, panels); else parent.append(notice); } } notice.textContent = `${entry?.title || '资源'}:${message}`; notice.setAttribute('data-mnote-resource-close-guard', reason || 'blocked'); notice.className = `mnote-close-guard-notice is-${reason || 'blocked'}`; if (notice._mnoteHideTimer) window.clearTimeout(notice._mnoteHideTimer); notice._mnoteHideTimer = window.setTimeout(() => { notice.classList.add('is-hiding'); window.setTimeout(() => { if (notice.parentNode) notice.remove(); }, 260); }, 4000); }; const cssSafe = (value) => { const text = String(value || ''); if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(text); return text.replace(/["\\]/g, '\\$&'); }; const syncActiveResourceFileTreeRow = (activeResource) => { document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-active="true"]').forEach((row) => { if (row instanceof HTMLElement) row.setAttribute('data-active', 'false'); }); const entry = activeResource ? resourceTabRegistry.get(activeResource) : null; if (!entry) return; const assetId = String(entry.assetId || entry.session?.assetId || '').trim(); const path = String(entry.path || entry.session?.resourcePath || '').trim(); const identity = String(entry.objectIdentity || activeResource || '').trim(); if (assetId) { const row = document.querySelector(`#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-asset-id="${cssSafe(assetId)}"]`); if (row instanceof HTMLElement) { row.setAttribute('data-active', 'true'); return; } } const rows = document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-object-identity]'); for (const row of rows) { if (!(row instanceof HTMLElement)) continue; const objectIdentity = row.getAttribute('data-object-identity') || ''; if ((identity && objectIdentity.includes(identity)) || (path && objectIdentity.includes(path))) { row.setAttribute('data-active', 'true'); return; } } }; const syncActiveResourceWidthType = (activeEntry, paneRole = 'primary') => { const role = normalizePaneRole(paneRole); const widthType = role === 'primary' ? resourceTabWidthType(activeEntry) : ''; const targetNodes = [document.documentElement, document.body].filter((node) => node instanceof HTMLElement); targetNodes.forEach((node) => { if (widthType) node.setAttribute('data-mnote-active-resource-width-type', widthType); else node.removeAttribute('data-mnote-active-resource-width-type'); }); const shell = document.querySelector(`.document-pane[data-pane-role="${role}"] .document-shell`); if (shell instanceof HTMLElement) { if (widthType) shell.setAttribute('data-mnote-active-resource-width-type', widthType); else shell.removeAttribute('data-mnote-active-resource-width-type'); } window.dispatchEvent(new CustomEvent('mnote:active-resource-tab-changed', { detail: { paneRole: role, active: Boolean(activeEntry), widthType, objectIdentity: String(activeEntry?.objectIdentity || '') } })); }; const syncActiveResourceUrlState = (activeResource, paneRole = 'primary') => { const role = normalizePaneRole(paneRole); if (role !== 'primary') return; const url = currentUrl(); if (activeResource) { const entry = resourceTabRegistry.get(activeResource); const documentId = String(entry?.ownerDocumentId || entry?.documentId || '').trim(); if (documentId) url.pathname = `/documents/${encodeURIComponent(documentId)}`; url.searchParams.set('resourceTab', activeResource); } else { const documentId = currentWebShellDocumentId(); if (documentId) url.pathname = `/documents/${encodeURIComponent(documentId)}`; url.searchParams.delete('resourceTab'); } replaceUrlState(url); }; const activateMainEditorTab = (objectIdentity, paneRole = 'primary') => { const activeResource = String(objectIdentity || '').trim(); const activeEntry = activeResource ? resourceTabRegistry.get(activeResource) : null; const role = normalizePaneRole(activeEntry?.paneRole || paneRole); const nodes = resourceTabHostNodes(role); if (activeResource) { touchResourceTabMru(role, activeResource); setResourceTabLastActive(activeResource); } if (nodes.pageTab instanceof HTMLElement) { const activePage = !activeResource; nodes.pageTab.classList.toggle('is-active', activePage); nodes.pageTab.setAttribute('aria-selected', activePage ? 'true' : 'false'); nodes.pageTab.setAttribute('tabindex', activePage ? '0' : '-1'); } if (nodes.pagePanel instanceof HTMLElement) nodes.pagePanel.hidden = Boolean(activeResource); if (nodes.host instanceof HTMLElement) nodes.host.hidden = !activeResource; resourceTabRegistry.forEach((entry, key) => { if (normalizePaneRole(entry.paneRole) !== role) return; const active = key === activeResource; if (entry.tab instanceof HTMLElement) { entry.tab.classList.toggle('is-active', active); entry.tab.setAttribute('aria-selected', active ? 'true' : 'false'); entry.tab.setAttribute('tabindex', active ? '0' : '-1'); } if (entry.panel instanceof HTMLElement) entry.panel.hidden = !active; syncResourceTabCloseGuard(entry); }); syncActiveResourceWidthType(activeEntry, role); if (activeEntry) markIntendedSlashRoot(activeEntry); if (!activeEntry && window.__mnoteIntendedSlashRoot instanceof HTMLElement) window.__mnoteIntendedSlashRoot = null; if (role === 'primary') syncActiveResourceFileTreeRow(activeResource); syncActiveResourceUrlState(activeResource, role); syncOpenEditorsSnapshot(); }; const bindMainEditorTabStrip = (paneRole = 'primary') => { const nodes = resourceTabHostNodes(paneRole); if (!(nodes.strip instanceof HTMLElement)) return; if (nodes.strip.getAttribute('data-mnote-tab-strip-bound') === 'true') return; nodes.strip.setAttribute('data-mnote-tab-strip-bound', 'true'); const collectTabs = () => Array.from(nodes.strip.querySelectorAll('[data-mnote-main-tab]')) .filter((tab) => tab instanceof HTMLElement && tab.isConnected); const selectedIndex = (tabs) => tabs.findIndex((tab) => tab.getAttribute('aria-selected') === 'true'); nodes.strip.addEventListener('keydown', (event) => { const tabs = collectTabs(); if (!tabs.length) return; const focusedIndex = tabs.findIndex((tab) => tab === document.activeElement); const baseIndex = focusedIndex >= 0 ? focusedIndex : Math.max(0, selectedIndex(tabs)); let targetIndex = -1; if (event.key === 'ArrowRight') { targetIndex = (baseIndex + 1) % tabs.length; } else if (event.key === 'ArrowLeft') { targetIndex = (baseIndex - 1 + tabs.length) % tabs.length; } else if (event.key === 'Home') { targetIndex = 0; } else if (event.key === 'End') { targetIndex = tabs.length - 1; } else if (event.key === 'Enter' || event.key === ' ') { event.preventDefault(); const tab = tabs[baseIndex]; if (tab instanceof HTMLElement) tab.click(); return; } else { return; } event.preventDefault(); const target = tabs[targetIndex]; if (target instanceof HTMLElement) target.focus(); }); }; const bindMainEditorPageTab = (paneRole = 'primary') => { const role = normalizePaneRole(paneRole); const nodes = resourceTabHostNodes(role); if (!(nodes.pageTab instanceof HTMLElement)) return; bindMainEditorTabStrip(role); if (nodes.pageTab.getAttribute('data-mnote-page-tab-bound') === 'true') return; nodes.pageTab.setAttribute('data-mnote-page-tab-bound', 'true'); nodes.pageTab.addEventListener('click', (event) => { const target = event.target; if (target instanceof HTMLElement && target.closest('[data-mnote-pane-close="secondary"]')) return; event.preventDefault(); activateMainEditorTab('', role); }); syncOpenEditorsSnapshot(); }; const closeResourceTab = async (objectIdentity) => { const key = String(objectIdentity || '').trim(); const entry = resourceTabRegistry.get(key); if (!entry) return; await refreshResourceTabBufferState(entry); const guardReason = resourceTabCloseGuardReason(entry.session); if (guardReason) { console.warn(`mnote resource tab 关闭阻止: ${entry.title} (${guardReason})`); syncResourceTabCloseGuard(entry); showResourceTabCloseGuardNotice(entry, guardReason); return; } removeFromResourceTabMru(entry.paneRole || 'primary', key); if (entry.view) unmountEditorViewBinding(entry.view, { releaseSession: true }); if (entry.mindmapRuntime?.runtime && entry.mindmapRuntime?.mountId != null) { try { entry.mindmapRuntime.runtime.unmount(entry.mindmapRuntime.mountId); } catch (error) { console.warn('mnote mindmap resource tab unmount failed', error); } } if (entry.tab instanceof HTMLElement) entry.tab.remove(); if (entry.panel instanceof HTMLElement) entry.panel.remove(); resourceTabRegistry.delete(key); const role = normalizePaneRole(entry.paneRole); const nextKey = lastActiveResourceTabKey(role); activateMainEditorTab(nextKey, role); const nextTab = nextKey ? resourceTabRegistry.get(nextKey)?.tab : resourceTabHostNodes(role).pageTab; if (nextTab instanceof HTMLElement) nextTab.focus(); }; const markResourceTabError = (entry) => { if (!entry) return; entry.kind = 'error'; if (entry.tab instanceof HTMLElement) { entry.tab.setAttribute('data-mnote-tab-kind', 'error'); entry.tab.setAttribute('data-mnote-tab-badge-kind', 'file'); entry.tab.classList.add('is-error'); } if (entry.panel instanceof HTMLElement) { entry.panel.setAttribute('data-resource-kind', 'error'); entry.panel.innerHTML = '
无法加载此资源,请检查文件路径和访问权限。