refactor: split document mindmap host runtime
This commit is contained in:
+1
-1
@@ -122,7 +122,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::tests::mno
|
|||||||
- [x] B1. `document-pane-runtime.js`:secondary pane URL、pane resize、pane close、pane runtime registry。
|
- [x] B1. `document-pane-runtime.js`:secondary pane URL、pane resize、pane close、pane runtime registry。
|
||||||
- [x] B2. `document-tiptap-conversion-runtime.js`:legacy block / inline content / marks 转 Tiptap document。
|
- [x] B2. `document-tiptap-conversion-runtime.js`:legacy block / inline content / marks 转 Tiptap document。
|
||||||
- [ ] B3. `document-resource-tab-runtime.js`:resource tab registry、MRU、close guard、resource text/image/frame editor mount。
|
- [ ] B3. `document-resource-tab-runtime.js`:resource tab registry、MRU、close guard、resource text/image/frame editor mount。
|
||||||
- [ ] B4. `document-mindmap-host-runtime.js`:primary mindmap object shell、mindmap resource tab mount/unmount。
|
- [x] B4. `document-mindmap-host-runtime.js`:primary mindmap object shell、mindmap resource tab mount/unmount。
|
||||||
- [x] B5. `document-slash-position-runtime.js`:slash menu active root、positioning、mutation observer。
|
- [x] B5. `document-slash-position-runtime.js`:slash menu active root、positioning、mutation observer。
|
||||||
- [ ] B6. entrypoint 只负责读取 bootstrap、加载 runtime、装配各子模块。
|
- [ ] B6. entrypoint 只负责读取 bootstrap、加载 runtime、装配各子模块。
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { createMindmapHostRuntime } from './document-mindmap-host-runtime.js';
|
||||||
import {
|
import {
|
||||||
installGlobalSlashMenuPositioning,
|
installGlobalSlashMenuPositioning,
|
||||||
markIntendedSlashRoot,
|
markIntendedSlashRoot,
|
||||||
@@ -169,7 +170,6 @@ import {
|
|||||||
const documentSessionRegistry = new Map();
|
const documentSessionRegistry = new Map();
|
||||||
const localFolderEventRegistry = new Map();
|
const localFolderEventRegistry = new Map();
|
||||||
const paneViewRegistry = new Map();
|
const paneViewRegistry = new Map();
|
||||||
const mindmapPaneViewRegistry = new Map();
|
|
||||||
const resourceTabRegistry = new Map();
|
const resourceTabRegistry = new Map();
|
||||||
const resourceTabMru = { primary: [], secondary: [] };
|
const resourceTabMru = { primary: [], secondary: [] };
|
||||||
const resourceTabMruMax = 20;
|
const resourceTabMruMax = 20;
|
||||||
@@ -179,6 +179,14 @@ import {
|
|||||||
const treeExternalConflictMessage = '当前页面已在其它窗口更新,请刷新或保存前先处理冲突';
|
const treeExternalConflictMessage = '当前页面已在其它窗口更新,请刷新或保存前先处理冲突';
|
||||||
const SESSION_RELEASE_DELAY_MS = 1200;
|
const SESSION_RELEASE_DELAY_MS = 1200;
|
||||||
const LOCAL_FOLDER_SELF_CHANGE_SUPPRESSIONS_KEY = 'mnote.localFolder.selfChangeSuppressions.v1';
|
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 ensureLocalFolderSelfChangeSuppressions = () => {
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
@@ -206,26 +214,6 @@ import {
|
|||||||
return map;
|
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) => {
|
const parseLocalFolderEventPayload = (event) => {
|
||||||
try {
|
try {
|
||||||
return JSON.parse(String(event?.data || '{}'));
|
return JSON.parse(String(event?.data || '{}'));
|
||||||
@@ -1603,7 +1591,7 @@ import {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const replacePaneDocument = async (paneRole, descriptor, options = {}) => {
|
const replacePaneDocument = async (paneRole, descriptor, options = {}) => {
|
||||||
unmountMindmapPane(paneRole);
|
mindmapHost.unmountMindmapPane(paneRole);
|
||||||
const runtime = await loadRuntime();
|
const runtime = await loadRuntime();
|
||||||
const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`);
|
const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`);
|
||||||
const observability = document.querySelector(`[data-editor-host-observability][data-pane-role="${paneRole}"]`);
|
const observability = document.querySelector(`[data-editor-host-observability][data-pane-role="${paneRole}"]`);
|
||||||
@@ -1733,123 +1721,6 @@ import {
|
|||||||
replaceUrlState(url);
|
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 handleSessionChange = (session, view, event) => {
|
||||||
const payload = normalizeEnvelopePayload(event);
|
const payload = normalizeEnvelopePayload(event);
|
||||||
if (!payload) return;
|
if (!payload) return;
|
||||||
@@ -2659,35 +2530,6 @@ import {
|
|||||||
return true;
|
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 openUnsupportedSideTarget = (input = {}) => {
|
||||||
const url = currentUrl();
|
const url = currentUrl();
|
||||||
secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name));
|
secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name));
|
||||||
@@ -2696,7 +2538,7 @@ import {
|
|||||||
unmountEditorViewBinding(previousView);
|
unmountEditorViewBinding(previousView);
|
||||||
paneViewRegistry.delete('secondary');
|
paneViewRegistry.delete('secondary');
|
||||||
}
|
}
|
||||||
unmountMindmapPane('secondary');
|
mindmapHost.unmountMindmapPane('secondary');
|
||||||
const workspace = document.querySelector('.mnote-document-workspace');
|
const workspace = document.querySelector('.mnote-document-workspace');
|
||||||
if (workspace instanceof HTMLElement) {
|
if (workspace instanceof HTMLElement) {
|
||||||
workspace.setAttribute('data-has-secondary-pane', 'true');
|
workspace.setAttribute('data-has-secondary-pane', 'true');
|
||||||
@@ -2780,7 +2622,7 @@ import {
|
|||||||
activateMainEditorTab(registryKey, paneRole);
|
activateMainEditorTab(registryKey, paneRole);
|
||||||
try {
|
try {
|
||||||
if (entry.kind === 'mindmap') {
|
if (entry.kind === 'mindmap') {
|
||||||
await openMindmapResourceTab(entry, input);
|
await mindmapHost.openMindmapResourceTab(entry, input);
|
||||||
} else if (entry.kind === 'markdown' || entry.kind === 'text' || entry.kind === 'code') {
|
} else if (entry.kind === 'markdown' || entry.kind === 'text' || entry.kind === 'code') {
|
||||||
await openTiptapResourceTab(entry, input);
|
await openTiptapResourceTab(entry, input);
|
||||||
} else {
|
} else {
|
||||||
@@ -2841,7 +2683,7 @@ import {
|
|||||||
const docId = typeof documentId === 'string' ? documentId.trim() : '';
|
const docId = typeof documentId === 'string' ? documentId.trim() : '';
|
||||||
const mapId = typeof mindmapId === 'string' ? mindmapId.trim() : '';
|
const mapId = typeof mindmapId === 'string' ? mindmapId.trim() : '';
|
||||||
if (!docId || !mapId) return false;
|
if (!docId || !mapId) return false;
|
||||||
await replacePrimaryPaneMindmap({
|
await mindmapHost.replacePrimaryPaneMindmap({
|
||||||
documentId: docId,
|
documentId: docId,
|
||||||
mindmapId: mapId,
|
mindmapId: mapId,
|
||||||
workspaceId: typeof workspaceId === 'string' ? workspaceId.trim() : '',
|
workspaceId: typeof workspaceId === 'string' ? workspaceId.trim() : '',
|
||||||
|
|||||||
@@ -0,0 +1,184 @@
|
|||||||
|
export const createMindmapHostRuntime = ({
|
||||||
|
loadRuntime,
|
||||||
|
paneViewRegistry,
|
||||||
|
unmountEditorViewBinding,
|
||||||
|
pushUrlState,
|
||||||
|
rootSelector,
|
||||||
|
currentDocumentId,
|
||||||
|
} = {}) => {
|
||||||
|
const mindmapPaneViewRegistry = new Map();
|
||||||
|
const selector = rootSelector || '[data-testid="mnote-leptos-tiptap-island-editor-root"]';
|
||||||
|
|
||||||
|
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 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(`${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 openMindmapResourceTab = async (entry, input) => {
|
||||||
|
const documentId = String(input.documentId || currentDocumentId?.() || '').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 };
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
fetchMindmapShellBootstrap,
|
||||||
|
openMindmapResourceTab,
|
||||||
|
replacePrimaryPaneMindmap,
|
||||||
|
unmountMindmapPane,
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -146,6 +146,10 @@ pub fn build_router(state: AppState) -> Router {
|
|||||||
"/api/mnote-browser-runtime/document-pane-runtime.js",
|
"/api/mnote-browser-runtime/document-pane-runtime.js",
|
||||||
get(web_shell::document_pane_runtime_asset),
|
get(web_shell::document_pane_runtime_asset),
|
||||||
)
|
)
|
||||||
|
.route(
|
||||||
|
"/api/mnote-browser-runtime/document-mindmap-host-runtime.js",
|
||||||
|
get(web_shell::document_mindmap_host_runtime_asset),
|
||||||
|
)
|
||||||
.route(
|
.route(
|
||||||
"/api/mnote-browser-runtime/document-slash-position-runtime.js",
|
"/api/mnote-browser-runtime/document-slash-position-runtime.js",
|
||||||
get(web_shell::document_slash_position_runtime_asset),
|
get(web_shell::document_slash_position_runtime_asset),
|
||||||
@@ -572,6 +576,7 @@ mod tests {
|
|||||||
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
"/api/mnote-browser-runtime/tree-shell-runtime.js",
|
||||||
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
|
"/api/mnote-browser-runtime/document-conflict-panel-runtime.js",
|
||||||
"/api/mnote-browser-runtime/document-pane-runtime.js",
|
"/api/mnote-browser-runtime/document-pane-runtime.js",
|
||||||
|
"/api/mnote-browser-runtime/document-mindmap-host-runtime.js",
|
||||||
"/api/mnote-browser-runtime/document-slash-position-runtime.js",
|
"/api/mnote-browser-runtime/document-slash-position-runtime.js",
|
||||||
"/api/mnote-browser-runtime/document-tiptap-conversion-runtime.js",
|
"/api/mnote-browser-runtime/document-tiptap-conversion-runtime.js",
|
||||||
"/api/mnote-browser-runtime/document-editor-adapter-runtime.js",
|
"/api/mnote-browser-runtime/document-editor-adapter-runtime.js",
|
||||||
|
|||||||
@@ -869,6 +869,20 @@ pub async fn document_pane_runtime_asset() -> Response {
|
|||||||
.unwrap_or_else(|_| Response::new(Body::empty()))
|
.unwrap_or_else(|_| Response::new(Body::empty()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn document_mindmap_host_runtime_asset() -> Response {
|
||||||
|
const JS: &str = include_str!("../../browser/document-mindmap-host-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 document_slash_position_runtime_asset() -> Response {
|
pub async fn document_slash_position_runtime_asset() -> Response {
|
||||||
const JS: &str = include_str!("../../browser/document-slash-position-runtime.js");
|
const JS: &str = include_str!("../../browser/document-slash-position-runtime.js");
|
||||||
Response::builder()
|
Response::builder()
|
||||||
@@ -1320,6 +1334,8 @@ mod tests {
|
|||||||
|
|
||||||
const DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS: &str =
|
const DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS: &str =
|
||||||
include_str!("../../browser/document-editor-adapter-runtime.js");
|
include_str!("../../browser/document-editor-adapter-runtime.js");
|
||||||
|
const DOCUMENT_MINDMAP_HOST_RUNTIME_JS: &str =
|
||||||
|
include_str!("../../browser/document-mindmap-host-runtime.js");
|
||||||
const DOCUMENT_PANE_RUNTIME_JS: &str = include_str!("../../browser/document-pane-runtime.js");
|
const DOCUMENT_PANE_RUNTIME_JS: &str = include_str!("../../browser/document-pane-runtime.js");
|
||||||
const DOCUMENT_SLASH_POSITION_RUNTIME_JS: &str =
|
const DOCUMENT_SLASH_POSITION_RUNTIME_JS: &str =
|
||||||
include_str!("../../browser/document-slash-position-runtime.js");
|
include_str!("../../browser/document-slash-position-runtime.js");
|
||||||
@@ -1607,9 +1623,10 @@ mod tests {
|
|||||||
assert!(runtime.contains("openResourceInActiveTab"));
|
assert!(runtime.contains("openResourceInActiveTab"));
|
||||||
assert!(runtime.contains("/api/local-folder/resource/read"));
|
assert!(runtime.contains("/api/local-folder/resource/read"));
|
||||||
assert!(runtime.contains("/api/local-folder/resource/write"));
|
assert!(runtime.contains("/api/local-folder/resource/write"));
|
||||||
assert!(runtime.contains("replacePrimaryPaneMindmap"));
|
let mindmap_runtime = DOCUMENT_MINDMAP_HOST_RUNTIME_JS;
|
||||||
assert!(runtime.contains("__MNOTE_MINDMAP_EDITOR_BOOTSTRAP__"));
|
assert!(mindmap_runtime.contains("replacePrimaryPaneMindmap"));
|
||||||
assert!(runtime.contains("data-mnote-object-identity"));
|
assert!(mindmap_runtime.contains("__MNOTE_MINDMAP_EDITOR_BOOTSTRAP__"));
|
||||||
|
assert!(mindmap_runtime.contains("data-mnote-object-identity"));
|
||||||
assert!(runtime.contains("__MNOTE_DOCUMENT_PANES_BOOTSTRAP__"));
|
assert!(runtime.contains("__MNOTE_DOCUMENT_PANES_BOOTSTRAP__"));
|
||||||
assert!(html.contains("__MNOTE_TREE_LIVE_BOOTSTRAP__"));
|
assert!(html.contains("__MNOTE_TREE_LIVE_BOOTSTRAP__"));
|
||||||
assert!(html.contains("mnote.tree_live_bootstrap.v1"));
|
assert!(html.contains("mnote.tree_live_bootstrap.v1"));
|
||||||
@@ -2075,6 +2092,9 @@ mod tests {
|
|||||||
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("PANES_BOOTSTRAP_ID"));
|
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("PANES_BOOTSTRAP_ID"));
|
||||||
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("ROOT_SELECTOR"));
|
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("ROOT_SELECTOR"));
|
||||||
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("BRIDGE_PROTOCOL"));
|
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("BRIDGE_PROTOCOL"));
|
||||||
|
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("document-mindmap-host-runtime.js"));
|
||||||
|
assert!(DOCUMENT_MINDMAP_HOST_RUNTIME_JS.contains("replacePrimaryPaneMindmap"));
|
||||||
|
assert!(DOCUMENT_MINDMAP_HOST_RUNTIME_JS.contains("openMindmapResourceTab"));
|
||||||
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("document-pane-runtime.js"));
|
assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("document-pane-runtime.js"));
|
||||||
assert!(DOCUMENT_PANE_RUNTIME_JS.contains("openDocumentInSecondaryPane"));
|
assert!(DOCUMENT_PANE_RUNTIME_JS.contains("openDocumentInSecondaryPane"));
|
||||||
assert!(DOCUMENT_PANE_RUNTIME_JS.contains("buildPaneRuntime"));
|
assert!(DOCUMENT_PANE_RUNTIME_JS.contains("buildPaneRuntime"));
|
||||||
|
|||||||
Reference in New Issue
Block a user