refactor: split document mindmap host runtime

This commit is contained in:
lix-2026
2026-05-26 01:03:04 +08:00
parent 77244eec7e
commit ee5aeb45be
5 changed files with 226 additions and 175 deletions
@@ -1,3 +1,4 @@
import { createMindmapHostRuntime } from './document-mindmap-host-runtime.js';
import {
installGlobalSlashMenuPositioning,
markIntendedSlashRoot,
@@ -169,7 +170,6 @@ import {
const documentSessionRegistry = new Map();
const localFolderEventRegistry = new Map();
const paneViewRegistry = new Map();
const mindmapPaneViewRegistry = new Map();
const resourceTabRegistry = new Map();
const resourceTabMru = { primary: [], secondary: [] };
const resourceTabMruMax = 20;
@@ -179,6 +179,14 @@ import {
const treeExternalConflictMessage = '当前页面已在其它窗口更新,请刷新或保存前先处理冲突';
const SESSION_RELEASE_DELAY_MS = 1200;
const LOCAL_FOLDER_SELF_CHANGE_SUPPRESSIONS_KEY = 'mnote.localFolder.selfChangeSuppressions.v1';
const mindmapHost = createMindmapHostRuntime({
loadRuntime,
paneViewRegistry,
unmountEditorViewBinding: (...args) => unmountEditorViewBinding(...args),
pushUrlState,
rootSelector: ROOT_SELECTOR,
currentDocumentId: () => currentWebShellDocumentId(),
});
const ensureLocalFolderSelfChangeSuppressions = () => {
const now = Date.now();
@@ -206,26 +214,6 @@ import {
return map;
};
const unmountMindmapPane = (paneRole) => {
const view = mindmapPaneViewRegistry.get(paneRole);
if (!view) return;
mindmapPaneViewRegistry.delete(paneRole);
if (view.mountId != null && view.runtime && typeof view.runtime.unmount === 'function') {
try {
view.runtime.unmount(view.mountId);
} catch (error) {
console.warn('mnote mindmap pane unmount failed', error);
}
}
if (view.root instanceof HTMLElement) {
view.root.removeAttribute('data-runtime-mount-id');
view.root.removeAttribute('data-mnote-object-editor');
view.root.removeAttribute('data-mnote-object-identity');
view.root.removeAttribute('data-mnote-mindmap-id');
view.root.replaceChildren();
}
};
const parseLocalFolderEventPayload = (event) => {
try {
return JSON.parse(String(event?.data || '{}'));
@@ -1603,7 +1591,7 @@ import {
};
const replacePaneDocument = async (paneRole, descriptor, options = {}) => {
unmountMindmapPane(paneRole);
mindmapHost.unmountMindmapPane(paneRole);
const runtime = await loadRuntime();
const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`);
const observability = document.querySelector(`[data-editor-host-observability][data-pane-role="${paneRole}"]`);
@@ -1733,123 +1721,6 @@ import {
replaceUrlState(url);
};
const parseJsonScriptFromDocument = (doc, id) => {
const node = doc?.getElementById?.(id);
if (!node) return null;
try {
return JSON.parse(node.textContent || 'null');
} catch (error) {
console.warn(`mnote mindmap shell JSON 解析失败: ${id}`, error);
return null;
}
};
const fetchMindmapShellBootstrap = async (url) => {
const response = await fetch(url.toString(), {
cache: 'no-store',
credentials: 'include',
headers: { accept: 'text/html' },
});
if (!response.ok) throw new Error(`mindmap_shell_failed_${response.status}`);
const html = await response.text();
const parsed = new DOMParser().parseFromString(html, 'text/html');
const bootstrap = parseJsonScriptFromDocument(parsed, '__MNOTE_MINDMAP_EDITOR_BOOTSTRAP__');
if (!bootstrap || typeof bootstrap !== 'object') throw new Error('mindmap_shell_missing_bootstrap');
const contract = parseJsonScriptFromDocument(parsed, '__MNOTE_MINDMAP_SHELL__') || {};
const title = String(bootstrap.title || parsed.querySelector('title')?.textContent || '思维导图').trim() || '思维导图';
return { bootstrap, contract, title };
};
const setPrimaryMindmapSelection = (documentId, mindmapId) => {
const cssEscape = window.CSS && typeof window.CSS.escape === 'function' ? window.CSS.escape : (value) => String(value).replace(/["\\]/g, '\\$&');
const escapedDocId = cssEscape(documentId);
const escapedMindmapId = cssEscape(mindmapId);
document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach((row) => {
if (!(row instanceof HTMLElement)) return;
row.setAttribute('data-active', 'false');
row.setAttribute('data-selected', 'false');
});
document.querySelectorAll(`.tree-row[data-shell-mode="page"][data-node-id="${escapedDocId}"]`).forEach((row) => {
if (row instanceof HTMLElement) row.setAttribute('data-active', 'true');
});
document.querySelectorAll(`.tree-row[data-shell-mode="filetree"][data-asset-id="${escapedMindmapId}"]`).forEach((row) => {
if (row instanceof HTMLElement) row.setAttribute('data-selected', 'true');
});
};
const updatePrimaryMindmapChrome = ({ documentId, mindmapId, workspaceId, title, root }) => {
const pane = root.closest('[data-document-pane="true"]');
if (pane instanceof HTMLElement) {
pane.setAttribute('data-pane-document-id', `__mindmap_object__:${documentId}:${mindmapId}`);
pane.setAttribute('data-pane-workspace-id', workspaceId || '');
pane.setAttribute('data-pane-visible', 'true');
pane.hidden = false;
}
const shell = root.closest('.document-shell');
if (shell instanceof HTMLElement) {
shell.setAttribute('data-editor-host', 'mindmap_object');
shell.setAttribute('data-document-id', documentId);
shell.setAttribute('data-workspace-id', workspaceId || '');
shell.setAttribute('data-mindmap-id', mindmapId);
}
document.body.dataset.documentId = documentId;
document.body.dataset.mindmapId = mindmapId;
document.body.dataset.mnoteShell = 'mindmap';
document.title = title;
document.querySelectorAll('[data-page-title-input="true"][data-pane-role="primary"]').forEach((node) => {
if (!(node instanceof HTMLTextAreaElement)) return;
node.value = title;
node.setAttribute('data-document-id', documentId);
node.setAttribute('data-workspace-id', workspaceId || '');
node.setAttribute('data-title-last-saved', title);
node.setAttribute('data-title-save-status', 'saved');
node.style.height = 'auto';
node.style.height = `${Math.max(48, node.scrollHeight)}px`;
});
document.querySelectorAll('[data-page-title-current="true"]').forEach((node) => {
if (node instanceof HTMLElement) node.textContent = title;
});
const topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
root.setAttribute('data-mnote-object-editor', 'mindmap');
root.setAttribute('data-mnote-object-identity', `resource:mindmap:${documentId}:${mindmapId}`);
root.setAttribute('data-mnote-mindmap-id', mindmapId);
setPrimaryMindmapSelection(documentId, mindmapId);
};
const replacePrimaryPaneMindmap = async ({ documentId, mindmapId, workspaceId, url }) => {
const targetUrl = url instanceof URL
? url
: new URL(`/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`, window.location.origin);
const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="primary"]`);
const observability = document.querySelector('[data-editor-host-observability][data-pane-role="primary"]');
if (!(root instanceof HTMLElement)) throw new Error('primary_pane_root_missing');
const runtime = await loadRuntime();
const { bootstrap, title } = await fetchMindmapShellBootstrap(targetUrl);
const previousView = paneViewRegistry.get('primary');
if (previousView) {
unmountEditorViewBinding(previousView);
paneViewRegistry.delete('primary');
}
unmountMindmapPane('primary');
if (observability instanceof HTMLElement) {
observability.setAttribute('data-editor-host-active', 'mindmap_object');
observability.setAttribute('data-editor-host-status', 'mounting');
}
root.replaceChildren();
root.setAttribute('data-runtime-editor-status', 'booting');
updatePrimaryMindmapChrome({ documentId, mindmapId, workspaceId, title, root });
const mountId = runtime.mount(root, bootstrap);
root.setAttribute('data-runtime-mount-id', String(mountId));
root.setAttribute('data-editor-host-kind', 'mindmap_object');
if (observability instanceof HTMLElement) {
observability.setAttribute('data-editor-host-status', 'mounted');
}
mindmapPaneViewRegistry.set('primary', { paneRole: 'primary', root, runtime, mountId });
pushUrlState(targetUrl);
return true;
};
const handleSessionChange = (session, view, event) => {
const payload = normalizeEnvelopePayload(event);
if (!payload) return;
@@ -2659,35 +2530,6 @@ import {
return true;
};
const openMindmapResourceTab = async (entry, input) => {
const documentId = String(input.documentId || currentWebShellDocumentId() || '').trim();
const mindmapId = String(input.mindmapId || input.assetId || '').trim();
if (!documentId || !mindmapId) throw new Error('mindmap_resource_identity_missing');
const targetUrl = new URL(`/mindmap/${encodeURIComponent(documentId)}/${encodeURIComponent(mindmapId)}`, window.location.origin);
const runtime = await loadRuntime();
const { bootstrap, title } = await fetchMindmapShellBootstrap(targetUrl);
entry.title = String(input.title || title || '思维导图').trim() || '思维导图';
const titleNode = entry.tab?.querySelector?.('.mnote-main-tab-title');
if (titleNode) titleNode.textContent = entry.title;
entry.panel.innerHTML = '<main class="document-shell mnote-resource-tab-mindmap-shell" data-editor-host="mindmap_resource_tab"><div class="mnote-resource-tab-mindmap-root" data-testid="mnote-mindmap-editor-root" data-editor-host-kind="mindmap_resource_tab" data-runtime-editor-status="booting" data-pane-role="resource"></div></main>';
entry.panel.setAttribute('data-mnote-object-editor', 'mindmap');
entry.panel.setAttribute('data-mnote-object-identity', entry.objectIdentity);
entry.panel.setAttribute('data-mnote-mindmap-id', mindmapId);
const root = entry.panel.querySelector('[data-testid="mnote-mindmap-editor-root"]');
if (!(root instanceof HTMLElement)) throw new Error('mindmap_resource_root_missing');
root.setAttribute('data-mnote-object-editor', 'mindmap');
root.setAttribute('data-mnote-object-identity', entry.objectIdentity);
root.setAttribute('data-mnote-mindmap-id', mindmapId);
root.setAttribute('data-document-id', documentId);
const mountId = runtime.mount(root, bootstrap);
root.setAttribute('data-runtime-mount-id', String(mountId));
root.setAttribute('data-editor-host-kind', 'mindmap_resource_tab');
root.setAttribute('data-runtime-editor-status', 'mounted');
entry.view = null;
entry.session = null;
entry.mindmapRuntime = { runtime, mountId, root };
};
const openUnsupportedSideTarget = (input = {}) => {
const url = currentUrl();
secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name));
@@ -2696,7 +2538,7 @@ import {
unmountEditorViewBinding(previousView);
paneViewRegistry.delete('secondary');
}
unmountMindmapPane('secondary');
mindmapHost.unmountMindmapPane('secondary');
const workspace = document.querySelector('.mnote-document-workspace');
if (workspace instanceof HTMLElement) {
workspace.setAttribute('data-has-secondary-pane', 'true');
@@ -2780,7 +2622,7 @@ import {
activateMainEditorTab(registryKey, paneRole);
try {
if (entry.kind === 'mindmap') {
await openMindmapResourceTab(entry, input);
await mindmapHost.openMindmapResourceTab(entry, input);
} else if (entry.kind === 'markdown' || entry.kind === 'text' || entry.kind === 'code') {
await openTiptapResourceTab(entry, input);
} else {
@@ -2841,7 +2683,7 @@ import {
const docId = typeof documentId === 'string' ? documentId.trim() : '';
const mapId = typeof mindmapId === 'string' ? mindmapId.trim() : '';
if (!docId || !mapId) return false;
await replacePrimaryPaneMindmap({
await mindmapHost.replacePrimaryPaneMindmap({
documentId: docId,
mindmapId: mapId,
workspaceId: typeof workspaceId === 'string' ? workspaceId.trim() : '',