2026-05-26 01:03:04 +08:00
|
|
|
|
import { createMindmapHostRuntime } from './document-mindmap-host-runtime.js';
|
2026-05-26 01:15:28 +08:00
|
|
|
|
import { createResourceTabRuntime } from './document-resource-tab-runtime.js';
|
2026-05-26 01:23:27 +08:00
|
|
|
|
import { createDocumentSessionRuntime } from './document-session-runtime.js';
|
2026-05-26 00:54:54 +08:00
|
|
|
|
import {
|
|
|
|
|
|
installGlobalSlashMenuPositioning,
|
|
|
|
|
|
markIntendedSlashRoot,
|
|
|
|
|
|
observeSlashMenuPosition,
|
|
|
|
|
|
scheduleSlashMenuPosition,
|
|
|
|
|
|
} from './document-slash-position-runtime.js';
|
2026-05-26 00:47:51 +08:00
|
|
|
|
import {
|
|
|
|
|
|
buildPaneRuntime,
|
|
|
|
|
|
clearSecondaryParams,
|
|
|
|
|
|
createSecondaryPaneWidthController,
|
|
|
|
|
|
currentUrl,
|
|
|
|
|
|
installSecondaryPaneCloseButtons,
|
|
|
|
|
|
installSecondaryPaneOpenListeners,
|
|
|
|
|
|
pushUrlState,
|
|
|
|
|
|
replaceUrlState,
|
|
|
|
|
|
secondaryQueryParamNames,
|
|
|
|
|
|
} from './document-pane-runtime.js';
|
2026-05-26 00:44:07 +08:00
|
|
|
|
import {
|
|
|
|
|
|
conflictDetectionKeyBelongsToSession,
|
|
|
|
|
|
hydrateMindmapAttrsFromDom,
|
|
|
|
|
|
legacyInlineContentToTiptap,
|
|
|
|
|
|
toTiptapDocument,
|
|
|
|
|
|
} from './document-tiptap-conversion-runtime.js';
|
|
|
|
|
|
|
2026-05-26 00:35:35 +08:00
|
|
|
|
(() => {
|
|
|
|
|
|
const PANES_BOOTSTRAP_ID = '__MNOTE_DOCUMENT_PANES_BOOTSTRAP__';
|
|
|
|
|
|
const ROOT_SELECTOR = '[data-testid="mnote-leptos-tiptap-island-editor-root"]';
|
2026-07-21 05:13:05 +08:00
|
|
|
|
// 历史 spike 前缀仍是 island runtime 的默认派发;稳定前缀 `mnote:tiptap-island`
|
|
|
|
|
|
// 由 island 双发(架构收口 21 Phase 3),host 优先监听 spike 事件保持兼容。
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const EVENT_PREFIX = 'mnote:leptos-tiptap-spike';
|
2026-07-21 05:13:05 +08:00
|
|
|
|
const STABLE_EVENT_PREFIX = 'mnote:tiptap-island';
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const CHANGE_EVENT = `${EVENT_PREFIX}:change`;
|
|
|
|
|
|
const SAVE_EVENT = `${EVENT_PREFIX}:save-request`;
|
|
|
|
|
|
const READY_EVENT = `${EVENT_PREFIX}:ready`;
|
|
|
|
|
|
const STATE_EVENT = `${EVENT_PREFIX}:state`;
|
|
|
|
|
|
const ERROR_EVENT = `${EVENT_PREFIX}:error`;
|
|
|
|
|
|
const COMMAND_EVENT = `${EVENT_PREFIX}:command`;
|
|
|
|
|
|
const BRIDGE_PROTOCOL = 'mnote.leptos_tiptap.bridge.v1';
|
2026-05-29 11:13:05 +08:00
|
|
|
|
const DEV_HOT_BUSTER = (() => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return new URL(import.meta.url).searchParams.get('devHot') || '';
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
})();
|
|
|
|
|
|
|
|
|
|
|
|
const withDevHot = (path) => {
|
|
|
|
|
|
const url = new URL(path, window.location.origin);
|
|
|
|
|
|
if (DEV_HOT_BUSTER) url.searchParams.set('devHot', DEV_HOT_BUSTER);
|
|
|
|
|
|
return url.toString();
|
|
|
|
|
|
};
|
2026-05-26 00:35:35 +08:00
|
|
|
|
|
2026-07-21 05:13:05 +08:00
|
|
|
|
const refreshBreadcrumb = (documentId, title, workspaceId) => {
|
|
|
|
|
|
const host = document.querySelector('[data-breadcrumb-pages="true"]');
|
|
|
|
|
|
if (!(host instanceof HTMLElement)) return;
|
|
|
|
|
|
const rows = Array.from(document.querySelectorAll(
|
|
|
|
|
|
'.wolai-page-row[data-node-id], .tree-row[data-shell-mode="page"][data-node-id]',
|
|
|
|
|
|
));
|
|
|
|
|
|
const byId = new Map();
|
|
|
|
|
|
rows.forEach((row) => {
|
|
|
|
|
|
const id = String(row.getAttribute('data-node-id') || row.getAttribute('data-document-id') || '').trim();
|
|
|
|
|
|
if (!id || byId.has(id)) return;
|
|
|
|
|
|
const titleNode = row.querySelector('.wolai-row-title, .tree-link-title');
|
|
|
|
|
|
byId.set(id, {
|
|
|
|
|
|
id,
|
|
|
|
|
|
title: String(titleNode?.textContent || '').trim() || '无标题',
|
|
|
|
|
|
parentId: String(row.getAttribute('data-parent-id') || '').trim(),
|
|
|
|
|
|
href: row instanceof HTMLAnchorElement ? row.href : '',
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
const chain = [];
|
|
|
|
|
|
const seen = new Set();
|
|
|
|
|
|
let cursor = String(documentId || '').trim();
|
|
|
|
|
|
while (cursor && !seen.has(cursor) && chain.length < 64) {
|
|
|
|
|
|
const item = byId.get(cursor);
|
|
|
|
|
|
if (!item) break;
|
|
|
|
|
|
seen.add(cursor);
|
|
|
|
|
|
chain.push(item);
|
|
|
|
|
|
cursor = item.parentId;
|
|
|
|
|
|
}
|
|
|
|
|
|
chain.reverse();
|
|
|
|
|
|
if (!chain.length) return;
|
|
|
|
|
|
const query = new URLSearchParams(window.location.search);
|
|
|
|
|
|
if (workspaceId) query.set('workspaceId', workspaceId);
|
|
|
|
|
|
const children = [];
|
|
|
|
|
|
chain.forEach((item, index) => {
|
|
|
|
|
|
if (index > 0) {
|
|
|
|
|
|
const separator = document.createElement('span');
|
|
|
|
|
|
separator.className = 'wolai-breadcrumb-separator';
|
|
|
|
|
|
separator.setAttribute('aria-hidden', 'true');
|
|
|
|
|
|
separator.textContent = '›';
|
|
|
|
|
|
children.push(separator);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (index === chain.length - 1) {
|
|
|
|
|
|
const current = document.createElement('span');
|
|
|
|
|
|
current.className = 'wolai-breadcrumb-current';
|
|
|
|
|
|
current.dataset.breadcrumbDocumentId = item.id;
|
|
|
|
|
|
const icon = document.createElement('span');
|
|
|
|
|
|
icon.className = 'material-symbols-outlined wolai-home-icon';
|
|
|
|
|
|
icon.dataset.icon = 'home';
|
|
|
|
|
|
icon.setAttribute('aria-hidden', 'true');
|
|
|
|
|
|
const text = document.createElement('span');
|
|
|
|
|
|
text.dataset.pageTitleCurrent = 'true';
|
|
|
|
|
|
text.textContent = title || item.title;
|
|
|
|
|
|
current.append(icon, text);
|
|
|
|
|
|
children.push(current);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
const link = document.createElement('a');
|
|
|
|
|
|
link.className = 'wolai-breadcrumb-link';
|
|
|
|
|
|
link.dataset.breadcrumbDocumentId = item.id;
|
|
|
|
|
|
link.href = item.href || `/documents/${encodeURIComponent(item.id)}?${query.toString()}`;
|
|
|
|
|
|
link.textContent = item.title;
|
|
|
|
|
|
children.push(link);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
host.replaceChildren(...children);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const parseJsonScript = (id) => {
|
|
|
|
|
|
const node = document.getElementById(id);
|
|
|
|
|
|
if (!node) return null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
return JSON.parse(node.textContent || 'null');
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn(`mnote rust-web editor bootstrap JSON 解析失败: ${id}`, error);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const panesBootstrap = parseJsonScript(PANES_BOOTSTRAP_ID);
|
|
|
|
|
|
if (!panesBootstrap || !Array.isArray(panesBootstrap.panes)) return;
|
|
|
|
|
|
|
|
|
|
|
|
if (panesBootstrap.secondaryInvalid === true) clearSecondaryParams();
|
2026-05-26 00:47:51 +08:00
|
|
|
|
installSecondaryPaneOpenListeners();
|
|
|
|
|
|
installSecondaryPaneCloseButtons();
|
2026-05-26 01:04:35 +08:00
|
|
|
|
const { workspace, applyStoredSecondaryWidth } = createSecondaryPaneWidthController();
|
2026-05-26 00:35:35 +08:00
|
|
|
|
applyStoredSecondaryWidth();
|
|
|
|
|
|
|
2026-05-26 00:47:51 +08:00
|
|
|
|
const paneRuntimes = panesBootstrap.panes
|
|
|
|
|
|
.map((paneDescriptor) => buildPaneRuntime(paneDescriptor, ROOT_SELECTOR))
|
|
|
|
|
|
.filter(Boolean);
|
2026-05-26 00:35:35 +08:00
|
|
|
|
|
|
|
|
|
|
const loadRuntime = async () => {
|
|
|
|
|
|
if (window.__mnoteLeptosTiptapRuntimePromise) {
|
|
|
|
|
|
return window.__mnoteLeptosTiptapRuntimePromise;
|
|
|
|
|
|
}
|
|
|
|
|
|
window.__mnoteLeptosTiptapRuntimePromise = (async () => {
|
2026-05-29 11:13:05 +08:00
|
|
|
|
const manifestResponse = await fetch(withDevHot('/api/leptos-tiptap-runtime/manifest.json'));
|
2026-05-26 00:35:35 +08:00
|
|
|
|
if (!manifestResponse.ok) throw new Error(`manifest_failed_${manifestResponse.status}`);
|
|
|
|
|
|
const manifest = await manifestResponse.json();
|
|
|
|
|
|
if (!manifest.entryAssetPath) throw new Error('island manifest 缺少 entryAssetPath');
|
2026-05-29 11:13:05 +08:00
|
|
|
|
const entryUrl = withDevHot(`/api/leptos-tiptap-runtime/${manifest.entryAssetPath}`);
|
|
|
|
|
|
const wasmUrl = manifest.wasmAssetPath ? withDevHot(`/api/leptos-tiptap-runtime/${manifest.wasmAssetPath}`) : undefined;
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const runtime = await import(entryUrl);
|
|
|
|
|
|
if (typeof runtime.default !== 'function' || typeof runtime.mount !== 'function' || typeof runtime.unmount !== 'function') {
|
|
|
|
|
|
throw new Error('island runtime 导出不完整');
|
|
|
|
|
|
}
|
|
|
|
|
|
await runtime.default({ module_or_path: wasmUrl });
|
|
|
|
|
|
if (typeof runtime.mount_mindmap_shell === 'function' && typeof runtime.unmount_mindmap_shell === 'function') {
|
|
|
|
|
|
window.__MNOTE_MINDMAP_RUST_SHELL__ = {
|
|
|
|
|
|
mount: runtime.mount_mindmap_shell,
|
|
|
|
|
|
unmount: runtime.unmount_mindmap_shell,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return runtime;
|
|
|
|
|
|
})();
|
|
|
|
|
|
return window.__mnoteLeptosTiptapRuntimePromise;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 00:44:07 +08:00
|
|
|
|
// Tiptap/legacy conversion helpers live in document-tiptap-conversion-runtime.js.
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const normalizeBridgeValue = (value) => {
|
|
|
|
|
|
if (value instanceof Map) {
|
|
|
|
|
|
const out = {};
|
|
|
|
|
|
for (const [key, item] of value.entries()) out[key] = normalizeBridgeValue(item);
|
|
|
|
|
|
return out;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (Array.isArray(value)) return value.map(normalizeBridgeValue);
|
|
|
|
|
|
if (value && typeof value === 'object') {
|
|
|
|
|
|
return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, normalizeBridgeValue(item)]));
|
|
|
|
|
|
}
|
|
|
|
|
|
return value;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const normalizeEnvelopePayload = (event) => {
|
|
|
|
|
|
const detail = normalizeBridgeValue(event?.detail);
|
|
|
|
|
|
if (!detail || typeof detail !== 'object') return null;
|
|
|
|
|
|
const payload = detail.payload && typeof detail.payload === 'object' ? detail.payload : detail;
|
|
|
|
|
|
return payload && typeof payload === 'object' ? payload : null;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const setStatus = (runtime, status, message) => {
|
|
|
|
|
|
runtime.root.setAttribute('data-runtime-editor-status', status);
|
|
|
|
|
|
if (message) {
|
|
|
|
|
|
runtime.root.setAttribute('data-runtime-editor-error', message);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
runtime.root.removeAttribute('data-runtime-editor-error');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (runtime.observability instanceof HTMLElement) {
|
|
|
|
|
|
runtime.observability.setAttribute('data-editor-host-status', status);
|
|
|
|
|
|
runtime.observability.setAttribute('data-editor-host-active', 'leptos_tiptap_island');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 09:44:35 +08:00
|
|
|
|
const enhanceEditorAttachmentLinksSoon = () => {
|
|
|
|
|
|
const run = () => {
|
|
|
|
|
|
if (typeof window.__mnoteEnhanceEditorAttachmentLinks === 'function') {
|
|
|
|
|
|
window.__mnoteEnhanceEditorAttachmentLinks();
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
run();
|
|
|
|
|
|
[120, 500, 1200].forEach((delayMs) => window.setTimeout(run, delayMs));
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const pageAggregateUrl = (bootstrap) => {
|
|
|
|
|
|
const url = new URL(`/api/page-aggregate/${encodeURIComponent(bootstrap.documentId)}`, window.location.origin);
|
|
|
|
|
|
url.searchParams.set('sourceKind', bootstrap.sourceKind || 'local_folder');
|
|
|
|
|
|
if (bootstrap.workspaceId) url.searchParams.set('workspaceId', bootstrap.workspaceId);
|
|
|
|
|
|
if (bootstrap.rootUri) url.searchParams.set('rootUri', bootstrap.rootUri);
|
|
|
|
|
|
return url;
|
|
|
|
|
|
};
|
|
|
|
|
|
const pageAggregateUrlFromDescriptor = (descriptor) => {
|
|
|
|
|
|
const url = new URL(`/api/page-aggregate/${encodeURIComponent(descriptor.documentId)}`, window.location.origin);
|
2026-05-28 22:01:44 +08:00
|
|
|
|
url.searchParams.set('sourceKind', descriptor.sourceKind || 'local_folder');
|
2026-05-26 00:35:35 +08:00
|
|
|
|
if (descriptor.workspaceId) url.searchParams.set('workspaceId', descriptor.workspaceId);
|
|
|
|
|
|
if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
|
|
|
|
|
|
return url;
|
|
|
|
|
|
};
|
|
|
|
|
|
const buildBootstrapFromAggregate = (aggregate, descriptor, paneRole) => ({
|
|
|
|
|
|
schema: 'mnote.editor_bootstrap.v1',
|
|
|
|
|
|
documentId: aggregate?.identity?.documentId || aggregate?.identity?.document_id || descriptor.documentId,
|
|
|
|
|
|
workspaceId: aggregate?.identity?.workspaceId || aggregate?.identity?.workspace_id || descriptor.workspaceId || '',
|
|
|
|
|
|
paneRole,
|
2026-05-28 22:01:44 +08:00
|
|
|
|
sourceKind: descriptor.sourceKind || 'local_folder',
|
2026-05-26 00:35:35 +08:00
|
|
|
|
rootUri: descriptor.rootUri || '',
|
|
|
|
|
|
pageAggregateScriptId: paneRole === 'secondary' ? '__MNOTE_SECONDARY_PAGE_AGGREGATE__' : '__MNOTE_PAGE_AGGREGATE__',
|
2026-05-28 22:01:44 +08:00
|
|
|
|
saveEndpoint: (descriptor.sourceKind || 'local_folder') === 'local_folder'
|
2026-05-26 00:35:35 +08:00
|
|
|
|
? '/api/page-body/write'
|
|
|
|
|
|
: '/api/documents/save',
|
|
|
|
|
|
titleEndpoint: '/api/documents/title',
|
|
|
|
|
|
editorHostKind: 'leptos_tiptap_island',
|
|
|
|
|
|
});
|
2026-05-26 17:24:05 +08:00
|
|
|
|
const aggregateDocumentId = (aggregate) => {
|
|
|
|
|
|
return String(aggregate?.identity?.documentId || aggregate?.identity?.document_id || aggregate?.pageId || aggregate?.page_id || '').trim();
|
|
|
|
|
|
};
|
|
|
|
|
|
const aggregatePageOptions = (aggregate) => {
|
|
|
|
|
|
return aggregate?.layout?.pageOptions || aggregate?.layout?.page_options || aggregate?.layoutOptions || aggregate?.layout_options || null;
|
|
|
|
|
|
};
|
|
|
|
|
|
const withPageOptions = (aggregate, pageOptions) => {
|
|
|
|
|
|
if (!aggregate || !pageOptions) return aggregate;
|
|
|
|
|
|
const next = { ...aggregate };
|
|
|
|
|
|
next.layout = next.layout && typeof next.layout === 'object' ? { ...next.layout } : {};
|
|
|
|
|
|
next.layout.pageOptions = { ...pageOptions };
|
|
|
|
|
|
next.layout_options = { ...pageOptions };
|
|
|
|
|
|
next.layoutOptions = { ...pageOptions };
|
|
|
|
|
|
return next;
|
|
|
|
|
|
};
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const syncPageAggregateScript = (session, aggregate) => {
|
|
|
|
|
|
if (!session || !aggregate) return;
|
|
|
|
|
|
const scriptId = session.pageAggregateScriptId || '__MNOTE_PAGE_AGGREGATE__';
|
2026-05-26 16:47:03 +08:00
|
|
|
|
let node = document.getElementById(scriptId);
|
|
|
|
|
|
if (!node) {
|
|
|
|
|
|
node = document.createElement('script');
|
|
|
|
|
|
node.id = scriptId;
|
|
|
|
|
|
node.type = 'application/json';
|
|
|
|
|
|
document.body.appendChild(node);
|
|
|
|
|
|
}
|
2026-05-26 00:35:35 +08:00
|
|
|
|
try {
|
2026-05-26 17:24:05 +08:00
|
|
|
|
let existingAggregate = null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
existingAggregate = JSON.parse(node.textContent || 'null');
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
existingAggregate = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
const localWriteAt = Number(node.getAttribute('data-mnote-page-options-local-write-at') || 0);
|
|
|
|
|
|
const preserveRecentLocalOptions = localWriteAt > 0
|
|
|
|
|
|
&& Date.now() - localWriteAt < 15000
|
|
|
|
|
|
&& aggregateDocumentId(existingAggregate) === aggregateDocumentId(aggregate);
|
|
|
|
|
|
const nextAggregate = preserveRecentLocalOptions
|
|
|
|
|
|
? withPageOptions(aggregate, aggregatePageOptions(existingAggregate))
|
|
|
|
|
|
: aggregate;
|
|
|
|
|
|
node.textContent = JSON.stringify(nextAggregate);
|
2026-05-26 00:35:35 +08:00
|
|
|
|
node.setAttribute('data-mnote-page-aggregate-synced-at', String(Date.now()));
|
2026-05-26 17:24:05 +08:00
|
|
|
|
window.dispatchEvent(new CustomEvent('mnote:page-aggregate-synced', {
|
|
|
|
|
|
detail: { scriptId, documentId: aggregateDocumentId(nextAggregate) }
|
|
|
|
|
|
}));
|
2026-05-29 11:13:05 +08:00
|
|
|
|
enhanceEditorAttachmentLinksSoon();
|
2026-05-26 00:35:35 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('mnote Page Aggregate script 同步失败', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const paneViewRegistry = new Map();
|
2026-07-28 17:04:27 +08:00
|
|
|
|
/** 每 pane 导航世代:快速连点时丢弃过期 replacePaneDocument 结果,保证 last-click-wins。 */
|
|
|
|
|
|
const paneReplaceGeneration = new Map();
|
2026-05-26 01:15:28 +08:00
|
|
|
|
const currentWebShellWorkspaceId = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return currentUrl().searchParams.get('workspaceId') || '';
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const currentWebShellDocumentId = () => {
|
|
|
|
|
|
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 '';
|
|
|
|
|
|
};
|
2026-05-28 22:01:44 +08:00
|
|
|
|
const currentWebShellSourceKind = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return currentUrl().searchParams.get('sourceKind') || '';
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
const currentWebShellRootUri = () => {
|
|
|
|
|
|
try {
|
|
|
|
|
|
return currentUrl().searchParams.get('rootUri') || '';
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-06-04 18:51:16 +08:00
|
|
|
|
const cssSafe = (value) => {
|
|
|
|
|
|
const text = String(value || '');
|
|
|
|
|
|
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(text);
|
|
|
|
|
|
return text.replace(/["\\]/g, '\\$&');
|
|
|
|
|
|
};
|
2026-06-07 01:10:31 +08:00
|
|
|
|
const localFileOpenUrl = (rootUri, path) => {
|
|
|
|
|
|
const normalizedRootUri = String(rootUri || '').trim();
|
|
|
|
|
|
const normalizedPath = String(path || '').trim();
|
|
|
|
|
|
if (!normalizedRootUri || !normalizedPath) return '';
|
|
|
|
|
|
const url = new URL('/api/local-folder/files/open', window.location.origin);
|
|
|
|
|
|
url.searchParams.set('rootUri', normalizedRootUri);
|
|
|
|
|
|
url.searchParams.set('path', normalizedPath);
|
|
|
|
|
|
return url.toString();
|
|
|
|
|
|
};
|
|
|
|
|
|
const resourceHrefFromUrlState = (rootUri, path, title) => {
|
|
|
|
|
|
const fileUrl = localFileOpenUrl(rootUri, path);
|
|
|
|
|
|
if (!fileUrl) return '';
|
2026-06-08 20:35:49 +08:00
|
|
|
|
const name = String(title || path || '');
|
|
|
|
|
|
if (/\.pdf$/i.test(name)) {
|
2026-06-07 01:10:31 +08:00
|
|
|
|
const url = new URL('/pdf-preview', window.location.origin);
|
|
|
|
|
|
url.searchParams.set('fileUrl', fileUrl);
|
|
|
|
|
|
url.searchParams.set('fileName', title || path);
|
|
|
|
|
|
return url.toString();
|
|
|
|
|
|
}
|
2026-06-08 20:35:49 +08:00
|
|
|
|
const officeMatch = name.match(/\.([a-z0-9]+)$/i);
|
|
|
|
|
|
const officeType = officeMatch ? officeMatch[1].toLowerCase() : '';
|
|
|
|
|
|
if (['doc', 'docx', 'odt', 'rtf', 'xls', 'xlsx', 'ods', 'csv', 'ppt', 'pptx', 'odp'].indexOf(officeType) >= 0) {
|
|
|
|
|
|
const url = new URL('/office-preview', window.location.origin);
|
|
|
|
|
|
url.searchParams.set('fileUrl', fileUrl);
|
|
|
|
|
|
url.searchParams.set('fileName', title || path);
|
|
|
|
|
|
url.searchParams.set('fileType', officeType);
|
|
|
|
|
|
const workspaceId = currentWebShellWorkspaceId();
|
|
|
|
|
|
const sourceKind = currentWebShellSourceKind() || 'local_folder';
|
|
|
|
|
|
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
|
|
|
|
|
|
if (sourceKind) url.searchParams.set('sourceKind', sourceKind);
|
|
|
|
|
|
if (rootUri) url.searchParams.set('rootUri', rootUri);
|
|
|
|
|
|
return url.toString();
|
|
|
|
|
|
}
|
2026-06-07 01:10:31 +08:00
|
|
|
|
return fileUrl;
|
|
|
|
|
|
};
|
2026-06-04 18:51:16 +08:00
|
|
|
|
const applyDocumentEvidenceLocatorFromUrl = (root) => {
|
|
|
|
|
|
if (!(root instanceof HTMLElement)) return;
|
|
|
|
|
|
const url = currentUrl();
|
|
|
|
|
|
const blockId = String(url.searchParams.get('blockId') || url.searchParams.get('evidenceBlockId') || '').trim();
|
|
|
|
|
|
const lineRange = String(url.searchParams.get('lineRange') || '').trim();
|
|
|
|
|
|
if (!blockId && !lineRange) return;
|
|
|
|
|
|
root.setAttribute('data-mnote-evidence-open', 'true');
|
|
|
|
|
|
if (blockId) root.setAttribute('data-mnote-evidence-block-id', blockId);
|
|
|
|
|
|
if (lineRange) root.setAttribute('data-mnote-evidence-line-range', lineRange);
|
2026-06-05 23:00:53 +08:00
|
|
|
|
let target = blockId ? root.querySelector(`[data-block-id="${cssSafe(blockId)}"]`) : null;
|
|
|
|
|
|
if (!(target instanceof HTMLElement) && lineRange) {
|
|
|
|
|
|
const start = Number(String(lineRange).split('-')[0] || 0);
|
|
|
|
|
|
if (Number.isFinite(start) && start > 0) {
|
|
|
|
|
|
const blocks = Array.from(root.querySelectorAll('.ProseMirror [data-block-id]'))
|
|
|
|
|
|
.filter((node) => node instanceof HTMLElement);
|
|
|
|
|
|
target = blocks[Math.max(0, Math.min(blocks.length - 1, start - 1))] || null;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-06-04 18:51:16 +08:00
|
|
|
|
if (target instanceof HTMLElement) {
|
|
|
|
|
|
root.querySelectorAll('[data-mnote-evidence-text-highlight="true"]').forEach((node) => {
|
|
|
|
|
|
if (node instanceof HTMLElement) node.removeAttribute('data-mnote-evidence-text-highlight');
|
|
|
|
|
|
});
|
|
|
|
|
|
target.setAttribute('data-mnote-evidence-text-highlight', 'true');
|
|
|
|
|
|
target.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
2026-05-28 22:01:44 +08:00
|
|
|
|
|
|
|
|
|
|
const restoreResourceTabInputFromUrl = () => {
|
|
|
|
|
|
const url = currentUrl();
|
|
|
|
|
|
const raw = String(url.searchParams.get('resourceTab') || '').trim();
|
|
|
|
|
|
if (!raw) return null;
|
2026-06-07 01:10:31 +08:00
|
|
|
|
const objectIdentity = raw.startsWith('resource:') ? raw : (raw.includes('::') ? raw.slice(raw.indexOf('::') + 2) : raw);
|
2026-05-28 22:01:44 +08:00
|
|
|
|
if (!objectIdentity.startsWith('resource:file:')) return null;
|
|
|
|
|
|
const rootUri = currentWebShellRootUri();
|
|
|
|
|
|
if (!rootUri) return null;
|
|
|
|
|
|
const rest = objectIdentity.slice('resource:file:'.length);
|
|
|
|
|
|
const prefix = `${rootUri}:`;
|
|
|
|
|
|
if (!rest.startsWith(prefix)) return null;
|
|
|
|
|
|
const path = rest.slice(prefix.length).replace(/^\/+/, '');
|
|
|
|
|
|
if (!path) return null;
|
|
|
|
|
|
const title = path.split('/').filter(Boolean).pop() || path;
|
2026-06-07 01:10:31 +08:00
|
|
|
|
const href = resourceHrefFromUrlState(rootUri, path, title);
|
2026-05-28 22:01:44 +08:00
|
|
|
|
return {
|
|
|
|
|
|
objectIdentity,
|
|
|
|
|
|
documentId: currentWebShellDocumentId(),
|
|
|
|
|
|
workspaceId: currentWebShellWorkspaceId(),
|
|
|
|
|
|
sourceKind: currentWebShellSourceKind() || 'local_folder',
|
|
|
|
|
|
rootUri,
|
|
|
|
|
|
path,
|
|
|
|
|
|
assetId: `local:asset:${path}`,
|
|
|
|
|
|
title,
|
|
|
|
|
|
fileName: title,
|
2026-06-07 01:10:31 +08:00
|
|
|
|
href,
|
|
|
|
|
|
officeUrl: href,
|
2026-05-28 22:01:44 +08:00
|
|
|
|
openTarget: 'active-tab',
|
2026-06-05 23:00:53 +08:00
|
|
|
|
page: url.searchParams.get('page') || undefined,
|
|
|
|
|
|
bbox: url.searchParams.get('bbox') || undefined,
|
|
|
|
|
|
sourceMapPath: String(url.searchParams.get('sourceMapPath') || '').trim(),
|
|
|
|
|
|
blockId: String(url.searchParams.get('blockId') || '').trim(),
|
2026-06-09 09:20:56 +08:00
|
|
|
|
paragraphOrdinal: String(url.searchParams.get('paragraphOrdinal') || '').trim(),
|
|
|
|
|
|
paraIdStart: String(url.searchParams.get('paraIdStart') || '').trim(),
|
|
|
|
|
|
paraIdEnd: String(url.searchParams.get('paraIdEnd') || '').trim(),
|
|
|
|
|
|
textFingerprint: String(url.searchParams.get('textFingerprint') || '').trim(),
|
2026-06-08 20:35:49 +08:00
|
|
|
|
evidenceText: String(url.searchParams.get('evidenceText') || '').trim(),
|
2026-06-05 23:00:53 +08:00
|
|
|
|
lineRange: url.searchParams.get('lineRange') || null,
|
|
|
|
|
|
charRange: url.searchParams.get('charRange') || null,
|
2026-05-28 22:01:44 +08:00
|
|
|
|
};
|
|
|
|
|
|
};
|
2026-05-26 01:15:28 +08:00
|
|
|
|
|
2026-05-26 00:35:35 +08:00
|
|
|
|
let nextViewId = 1;
|
2026-05-26 01:03:04 +08:00
|
|
|
|
const mindmapHost = createMindmapHostRuntime({
|
|
|
|
|
|
loadRuntime,
|
|
|
|
|
|
paneViewRegistry,
|
|
|
|
|
|
unmountEditorViewBinding: (...args) => unmountEditorViewBinding(...args),
|
|
|
|
|
|
pushUrlState,
|
|
|
|
|
|
rootSelector: ROOT_SELECTOR,
|
|
|
|
|
|
currentDocumentId: () => currentWebShellDocumentId(),
|
|
|
|
|
|
});
|
2026-05-26 00:35:35 +08:00
|
|
|
|
|
2026-05-26 01:15:28 +08:00
|
|
|
|
let resourceTabs = null;
|
|
|
|
|
|
const syncResourceSessionTabGuards = (session) => {
|
|
|
|
|
|
if (resourceTabs) resourceTabs.syncResourceSessionTabGuards(session);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 01:23:27 +08:00
|
|
|
|
const documentSessions = createDocumentSessionRuntime({
|
|
|
|
|
|
bridgeProtocol: BRIDGE_PROTOCOL,
|
|
|
|
|
|
commandEvent: COMMAND_EVENT,
|
|
|
|
|
|
normalizeEnvelopePayload,
|
|
|
|
|
|
pageAggregateUrl,
|
|
|
|
|
|
setStatus,
|
|
|
|
|
|
syncPageAggregateScript,
|
|
|
|
|
|
syncResourceSessionTabGuards,
|
2026-05-26 00:35:35 +08:00
|
|
|
|
});
|
2026-05-26 01:23:27 +08:00
|
|
|
|
const documentSessionRegistry = documentSessions.documentSessionRegistry;
|
|
|
|
|
|
const externalConflictMessage = documentSessions.externalConflictMessage;
|
|
|
|
|
|
const sessionViews = (...args) => documentSessions.sessionViews(...args);
|
|
|
|
|
|
const releaseDocumentSession = (...args) => documentSessions.releaseDocumentSession(...args);
|
|
|
|
|
|
const scheduleDocumentSessionRelease = (...args) => documentSessions.scheduleDocumentSessionRelease(...args);
|
|
|
|
|
|
const cancelDocumentSessionRelease = (...args) => documentSessions.cancelDocumentSessionRelease(...args);
|
|
|
|
|
|
const currentEditorText = (...args) => documentSessions.currentEditorText(...args);
|
|
|
|
|
|
const clearEmbeddedLocalDraft = (...args) => documentSessions.clearEmbeddedLocalDraft(...args);
|
|
|
|
|
|
const setSessionStatus = (...args) => documentSessions.setSessionStatus(...args);
|
|
|
|
|
|
const dispatchSessionContentToView = (...args) => documentSessions.dispatchSessionContentToView(...args);
|
|
|
|
|
|
const broadcastSessionContent = (...args) => documentSessions.broadcastSessionContent(...args);
|
|
|
|
|
|
const sessionHasRecentExternalSignal = (...args) => documentSessions.sessionHasRecentExternalSignal(...args);
|
|
|
|
|
|
const sessionHasRecentLocalInput = (...args) => documentSessions.sessionHasRecentLocalInput(...args);
|
|
|
|
|
|
const clearSessionConflictSurface = (...args) => documentSessions.clearSessionConflictSurface(...args);
|
|
|
|
|
|
const markSessionExternalConflict = (...args) => documentSessions.markSessionExternalConflict(...args);
|
|
|
|
|
|
const queueSessionSave = (...args) => documentSessions.queueSessionSave(...args);
|
|
|
|
|
|
const refreshSessionFromExternalChange = (...args) => documentSessions.refreshSessionFromExternalChange(...args);
|
2026-05-28 22:01:44 +08:00
|
|
|
|
const ensureLocalFolderEventChannel = (...args) => documentSessions.ensureLocalFolderEventChannel(...args);
|
2026-05-26 01:23:27 +08:00
|
|
|
|
const sessionMatchesDocumentWorkspace = (...args) => documentSessions.sessionMatchesDocumentWorkspace(...args);
|
|
|
|
|
|
const getOrCreateDocumentSession = (...args) => documentSessions.getOrCreateDocumentSession(...args);
|
2026-07-21 05:13:05 +08:00
|
|
|
|
const setDocumentSessionUserReadOnlyMode = (...args) => documentSessions.setDocumentSessionUserReadOnlyMode(...args);
|
|
|
|
|
|
|
|
|
|
|
|
const syncDocumentEditModeChrome = (session) => {
|
|
|
|
|
|
if (!session) return;
|
|
|
|
|
|
sessionViews(session).forEach((view) => {
|
|
|
|
|
|
const pane = view.runtimeDescriptor.root.closest('[data-document-pane="true"]');
|
|
|
|
|
|
const shell = view.runtimeDescriptor.root.closest('.document-shell');
|
|
|
|
|
|
if (shell instanceof HTMLElement) {
|
|
|
|
|
|
shell.setAttribute('data-document-user-readonly-mode', String(session.userReadOnlyMode !== false));
|
|
|
|
|
|
shell.setAttribute('data-document-effective-readonly', String(session.readOnly === true));
|
|
|
|
|
|
}
|
|
|
|
|
|
const titleInput = pane?.querySelector('[data-page-title-input="true"]');
|
|
|
|
|
|
if (titleInput instanceof HTMLTextAreaElement) {
|
|
|
|
|
|
titleInput.readOnly = session.readOnly === true;
|
|
|
|
|
|
titleInput.setAttribute('data-document-user-readonly-mode', String(session.userReadOnlyMode !== false));
|
|
|
|
|
|
}
|
|
|
|
|
|
const toggle = pane?.querySelector('[data-document-edit-mode-toggle]');
|
|
|
|
|
|
if (toggle instanceof HTMLButtonElement) {
|
|
|
|
|
|
const editing = session.readOnly !== true;
|
|
|
|
|
|
toggle.textContent = editing ? '退出编辑' : '开始编辑';
|
|
|
|
|
|
toggle.title = editing ? '切回只读浏览' : '进入编辑模式';
|
|
|
|
|
|
toggle.setAttribute('aria-pressed', String(editing));
|
|
|
|
|
|
toggle.setAttribute('data-document-editing', String(editing));
|
|
|
|
|
|
toggle.disabled = session.permissionReadOnly === true && session.userReadOnlyMode === false;
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
2026-05-26 00:35:35 +08:00
|
|
|
|
|
|
|
|
|
|
const updatePaneChrome = (runtimeDescriptor) => {
|
|
|
|
|
|
const pane = runtimeDescriptor.root.closest('[data-document-pane="true"]');
|
|
|
|
|
|
if (!(pane instanceof HTMLElement)) return;
|
2026-05-28 22:01:44 +08:00
|
|
|
|
if (runtimeDescriptor.paneRole === 'primary') {
|
|
|
|
|
|
document.querySelectorAll('[data-mnote-navigation-page-placeholder][data-pane-role="primary"]').forEach((node) => {
|
|
|
|
|
|
if (node instanceof HTMLElement) node.hidden = true;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const aggregate = runtimeDescriptor.aggregate || {};
|
|
|
|
|
|
const bootstrap = runtimeDescriptor.bootstrap || {};
|
|
|
|
|
|
const title = aggregate?.head?.title || '无标题';
|
|
|
|
|
|
const documentId = bootstrap.documentId || aggregate?.identity?.documentId || '';
|
|
|
|
|
|
const workspaceId = bootstrap.workspaceId || aggregate?.identity?.workspaceId || '';
|
|
|
|
|
|
pane.setAttribute('data-pane-document-id', documentId);
|
|
|
|
|
|
pane.setAttribute('data-pane-workspace-id', workspaceId);
|
|
|
|
|
|
pane.setAttribute('data-pane-visible', 'true');
|
|
|
|
|
|
pane.removeAttribute('data-mnote-side-target');
|
|
|
|
|
|
pane.hidden = false;
|
|
|
|
|
|
const shell = pane.querySelector('.document-shell');
|
|
|
|
|
|
if (shell instanceof HTMLElement) {
|
|
|
|
|
|
shell.setAttribute('data-document-id', documentId);
|
|
|
|
|
|
shell.setAttribute('data-workspace-id', workspaceId);
|
|
|
|
|
|
const options = aggregate?.layout?.pageOptions || {};
|
|
|
|
|
|
shell.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
|
|
|
|
|
|
shell.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
|
|
|
|
|
|
shell.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
|
|
|
|
|
|
shell.setAttribute('data-page-font', String(options.pageFont || 'default'));
|
|
|
|
|
|
shell.setAttribute('data-page-show-heading-numbers', String(Boolean(options.showHeadingNumbers)));
|
2026-05-26 16:24:00 +08:00
|
|
|
|
const hideTitleHeader = Boolean(options.hideTitleHeader || options.hide_title_header);
|
|
|
|
|
|
const header = shell.querySelector('.document-shell-header');
|
|
|
|
|
|
if (header instanceof HTMLElement) {
|
|
|
|
|
|
header.hidden = hideTitleHeader;
|
|
|
|
|
|
header.setAttribute('data-page-title-hidden', String(hideTitleHeader));
|
|
|
|
|
|
}
|
2026-05-26 00:35:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
pane.querySelectorAll('[data-page-title-input="true"]').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`;
|
|
|
|
|
|
});
|
|
|
|
|
|
pane.querySelectorAll('[data-page-title-current="true"]').forEach((node) => {
|
|
|
|
|
|
if (node instanceof HTMLElement) node.textContent = title;
|
|
|
|
|
|
});
|
|
|
|
|
|
const pageTab = document.querySelector(`[data-mnote-main-tab="page"][data-pane-role="${runtimeDescriptor.paneRole}"]`);
|
|
|
|
|
|
if (pageTab instanceof HTMLElement) {
|
|
|
|
|
|
pageTab.setAttribute('data-document-id', documentId);
|
|
|
|
|
|
pageTab.setAttribute('data-workspace-id', workspaceId);
|
|
|
|
|
|
const pageTabTitle = pageTab.querySelector('.mnote-main-tab-title');
|
|
|
|
|
|
if (pageTabTitle instanceof HTMLElement) pageTabTitle.textContent = title;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (runtimeDescriptor.paneRole === 'primary') {
|
|
|
|
|
|
document.body.dataset.documentId = documentId;
|
|
|
|
|
|
document.body.dataset.mnoteShell = 'document';
|
|
|
|
|
|
delete document.body.dataset.mindmapId;
|
|
|
|
|
|
document.title = title;
|
2026-07-21 05:13:05 +08:00
|
|
|
|
refreshBreadcrumb(documentId, title, workspaceId);
|
2026-05-26 00:35:35 +08:00
|
|
|
|
window.dispatchEvent(new CustomEvent('mnote:primary-document-activated', {
|
2026-07-15 23:12:15 +08:00
|
|
|
|
detail: {
|
|
|
|
|
|
documentId,
|
|
|
|
|
|
workspaceId,
|
|
|
|
|
|
title,
|
|
|
|
|
|
sourceKind: bootstrap.sourceKind || '',
|
|
|
|
|
|
rootUri: bootstrap.rootUri || '',
|
|
|
|
|
|
pageBlockNavigation: runtimeDescriptor.pageBlockNavigation === true,
|
|
|
|
|
|
}
|
2026-05-26 00:35:35 +08:00
|
|
|
|
}));
|
|
|
|
|
|
document.documentElement.removeAttribute('data-mnote-side-target-unsupported');
|
|
|
|
|
|
document.documentElement.removeAttribute('data-mnote-side-target-asset-id');
|
2026-07-15 23:12:15 +08:00
|
|
|
|
const activeTreeTab = document.querySelector('[data-mnote-sidebar-tree-tab][aria-selected="true"]');
|
|
|
|
|
|
const activeTreeMode = activeTreeTab instanceof HTMLElement
|
|
|
|
|
|
&& activeTreeTab.getAttribute('data-mnote-sidebar-tree-tab') === 'filetree'
|
|
|
|
|
|
? 'filetree'
|
|
|
|
|
|
: 'page';
|
|
|
|
|
|
const activeTreeRows = activeTreeMode === 'filetree'
|
|
|
|
|
|
? '#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'
|
|
|
|
|
|
: '#sidebar-tree-root .tree-row[data-shell-mode="page"]';
|
|
|
|
|
|
document.querySelectorAll(`${activeTreeRows}[data-active="true"], ${activeTreeRows}[data-selected="true"]`).forEach((row) => {
|
2026-05-26 00:35:35 +08:00
|
|
|
|
if (row instanceof HTMLElement) {
|
|
|
|
|
|
row.setAttribute('data-active', 'false');
|
|
|
|
|
|
row.setAttribute('data-selected', 'false');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
const escapedId = window.CSS && typeof window.CSS.escape === 'function' ? window.CSS.escape(documentId) : String(documentId).replace(/["\\]/g, '\\$&');
|
2026-07-15 23:12:15 +08:00
|
|
|
|
document.querySelectorAll(`${activeTreeRows}[data-node-id="${escapedId}"], ${activeTreeRows}[data-document-id="${escapedId}"], ${activeTreeRows}[data-doc-id="${escapedId}"]`).forEach((row) => {
|
2026-05-26 00:35:35 +08:00
|
|
|
|
if (!(row instanceof HTMLElement)) return;
|
|
|
|
|
|
if (row.getAttribute('data-shell-mode') === 'filetree') {
|
|
|
|
|
|
row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === `doc:${documentId}`));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
row.setAttribute('data-active', 'true');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
runtimeDescriptor.root.removeAttribute('data-mnote-object-editor');
|
|
|
|
|
|
runtimeDescriptor.root.removeAttribute('data-mnote-object-identity');
|
|
|
|
|
|
runtimeDescriptor.root.removeAttribute('data-mnote-mindmap-id');
|
|
|
|
|
|
runtimeDescriptor.root.removeAttribute('data-mnote-side-target-unsupported');
|
|
|
|
|
|
runtimeDescriptor.root.removeAttribute('data-mnote-side-target-asset-id');
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const fetchPageAggregateForPane = async (descriptor) => {
|
|
|
|
|
|
const response = await fetch(pageAggregateUrlFromDescriptor(descriptor).toString(), {
|
|
|
|
|
|
cache: 'no-store',
|
|
|
|
|
|
headers: { accept: 'application/json' },
|
|
|
|
|
|
});
|
|
|
|
|
|
if (!response.ok) throw new Error(`page_aggregate_failed_${response.status}`);
|
|
|
|
|
|
const payload = await response.json();
|
|
|
|
|
|
if (!payload?.result) throw new Error('page_aggregate_missing_result');
|
|
|
|
|
|
return payload.result;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-28 22:01:44 +08:00
|
|
|
|
const ensureLazyPrimaryPaneRoot = () => {
|
|
|
|
|
|
let root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="primary"]`);
|
|
|
|
|
|
if (root instanceof HTMLElement) return root;
|
|
|
|
|
|
const pagePanel = document.querySelector('[data-mnote-page-tab-panel][data-pane-role="primary"]');
|
|
|
|
|
|
if (!(pagePanel instanceof HTMLElement)) return null;
|
|
|
|
|
|
const pane = document.createElement('section');
|
|
|
|
|
|
pane.className = 'document-pane';
|
|
|
|
|
|
pane.setAttribute('data-document-pane', 'true');
|
|
|
|
|
|
pane.setAttribute('data-pane-role', 'primary');
|
|
|
|
|
|
pane.setAttribute('data-pane-document-id', '');
|
|
|
|
|
|
pane.setAttribute('data-pane-workspace-id', currentWebShellWorkspaceId());
|
|
|
|
|
|
pane.setAttribute('data-pane-visible', 'false');
|
|
|
|
|
|
pane.hidden = true;
|
|
|
|
|
|
|
|
|
|
|
|
const shell = document.createElement('main');
|
|
|
|
|
|
shell.className = 'document-shell';
|
|
|
|
|
|
shell.setAttribute('data-editor-host', 'leptos_tiptap_island');
|
|
|
|
|
|
shell.setAttribute('data-document-id', '');
|
|
|
|
|
|
shell.setAttribute('data-workspace-id', currentWebShellWorkspaceId());
|
|
|
|
|
|
shell.setAttribute('data-pane-role', 'primary');
|
|
|
|
|
|
shell.setAttribute('data-page-wide-layout', 'false');
|
|
|
|
|
|
shell.setAttribute('data-page-small-text', 'false');
|
|
|
|
|
|
shell.setAttribute('data-layout-density', 'normal');
|
|
|
|
|
|
shell.setAttribute('data-page-font', 'default');
|
|
|
|
|
|
shell.setAttribute('data-page-show-heading-numbers', 'false');
|
|
|
|
|
|
|
|
|
|
|
|
const header = document.createElement('header');
|
|
|
|
|
|
header.className = 'document-shell-header';
|
|
|
|
|
|
header.setAttribute('data-page-title-hidden', 'true');
|
|
|
|
|
|
header.hidden = true;
|
|
|
|
|
|
const titleHeading = document.createElement('h1');
|
|
|
|
|
|
titleHeading.className = 'document-title-heading';
|
|
|
|
|
|
const titleInput = document.createElement('textarea');
|
|
|
|
|
|
titleInput.id = 'mnote-page-title-input';
|
|
|
|
|
|
titleInput.className = 'document-title-input';
|
|
|
|
|
|
titleInput.setAttribute('aria-label', '页面标题');
|
|
|
|
|
|
titleInput.setAttribute('data-page-title-input', 'true');
|
|
|
|
|
|
titleInput.setAttribute('data-document-id', '');
|
|
|
|
|
|
titleInput.setAttribute('data-workspace-id', currentWebShellWorkspaceId());
|
|
|
|
|
|
titleInput.setAttribute('data-pane-role', 'primary');
|
|
|
|
|
|
titleInput.setAttribute('data-title-endpoint', '/api/documents/title');
|
|
|
|
|
|
titleInput.rows = 1;
|
|
|
|
|
|
titleHeading.appendChild(titleInput);
|
2026-07-21 05:13:05 +08:00
|
|
|
|
const modeToggle = document.createElement('button');
|
|
|
|
|
|
modeToggle.type = 'button';
|
|
|
|
|
|
modeToggle.className = 'document-edit-mode-toggle';
|
|
|
|
|
|
modeToggle.setAttribute('data-document-edit-mode-toggle', 'true');
|
|
|
|
|
|
modeToggle.setAttribute('aria-pressed', 'false');
|
|
|
|
|
|
modeToggle.setAttribute('data-document-editing', 'false');
|
|
|
|
|
|
modeToggle.textContent = '开始编辑';
|
2026-05-28 22:01:44 +08:00
|
|
|
|
const meta = document.createElement('div');
|
|
|
|
|
|
meta.className = 'document-shell-meta';
|
|
|
|
|
|
meta.setAttribute('aria-label', '页面元信息');
|
|
|
|
|
|
const currentTitle = document.createElement('span');
|
|
|
|
|
|
currentTitle.setAttribute('data-page-title-current', 'true');
|
|
|
|
|
|
meta.appendChild(currentTitle);
|
2026-07-21 05:13:05 +08:00
|
|
|
|
header.append(titleHeading, modeToggle, meta);
|
2026-05-28 22:01:44 +08:00
|
|
|
|
|
|
|
|
|
|
const aggregateMarker = document.createElement('section');
|
|
|
|
|
|
aggregateMarker.setAttribute('data-page-aggregate-snapshot', 'mnote.page_aggregate.v1');
|
|
|
|
|
|
const subtree = document.createElement('section');
|
|
|
|
|
|
subtree.setAttribute('data-testid', 'mnote-page-subtree');
|
|
|
|
|
|
subtree.setAttribute('data-page-tree-source', 'page_aggregate.tree.pageSubtree');
|
|
|
|
|
|
subtree.setAttribute('data-page-subtree-present', 'false');
|
|
|
|
|
|
subtree.setAttribute('data-pane-role', 'primary');
|
|
|
|
|
|
|
|
|
|
|
|
const island = document.createElement('section');
|
|
|
|
|
|
island.id = 'mnote-editor-island';
|
|
|
|
|
|
island.setAttribute('data-editor-host', 'leptos_tiptap_island');
|
|
|
|
|
|
island.setAttribute('data-pane-role', 'primary');
|
|
|
|
|
|
root = document.createElement('div');
|
|
|
|
|
|
root.id = 'mnote-leptos-tiptap-island-editor-root';
|
|
|
|
|
|
root.setAttribute('data-testid', 'mnote-leptos-tiptap-island-editor-root');
|
|
|
|
|
|
root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
|
|
|
|
|
|
root.setAttribute('data-runtime-editor-status', 'booting');
|
|
|
|
|
|
root.setAttribute('data-pane-role', 'primary');
|
|
|
|
|
|
const observability = document.createElement('div');
|
|
|
|
|
|
observability.className = 'sr-only';
|
|
|
|
|
|
observability.setAttribute('data-editor-host-observability', 'rust-web-inline-island');
|
|
|
|
|
|
observability.setAttribute('data-editor-host-active', 'leptos_tiptap_island');
|
|
|
|
|
|
observability.setAttribute('data-editor-host-requested', 'leptos_tiptap_island');
|
|
|
|
|
|
observability.setAttribute('data-editor-host-status', 'booting');
|
|
|
|
|
|
observability.setAttribute('data-pane-role', 'primary');
|
|
|
|
|
|
island.append(root, observability);
|
|
|
|
|
|
shell.append(header, aggregateMarker, subtree, island);
|
|
|
|
|
|
pane.appendChild(shell);
|
|
|
|
|
|
pagePanel.appendChild(pane);
|
|
|
|
|
|
return root;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const replacePaneDocument = async (paneRole, descriptor, options = {}) => {
|
2026-07-28 17:04:27 +08:00
|
|
|
|
const generation = (paneReplaceGeneration.get(paneRole) || 0) + 1;
|
|
|
|
|
|
paneReplaceGeneration.set(paneRole, generation);
|
2026-05-26 01:03:04 +08:00
|
|
|
|
mindmapHost.unmountMindmapPane(paneRole);
|
2026-05-28 22:01:44 +08:00
|
|
|
|
const root = paneRole === 'primary'
|
|
|
|
|
|
? ensureLazyPrimaryPaneRoot()
|
|
|
|
|
|
: document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`);
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const observability = document.querySelector(`[data-editor-host-observability][data-pane-role="${paneRole}"]`);
|
|
|
|
|
|
if (!(root instanceof HTMLElement)) throw new Error(`pane_root_missing_${paneRole}`);
|
2026-05-27 11:31:12 +08:00
|
|
|
|
const runtimePromise = loadRuntime();
|
|
|
|
|
|
const aggregatePromise = options.aggregate
|
|
|
|
|
|
? Promise.resolve(options.aggregate)
|
|
|
|
|
|
: fetchPageAggregateForPane(descriptor);
|
|
|
|
|
|
const [runtime, aggregate] = await Promise.all([runtimePromise, aggregatePromise]);
|
2026-07-28 17:04:27 +08:00
|
|
|
|
// 过期导航:丢弃结果,避免覆盖更新的 last-click View 并泄漏中间实例。
|
|
|
|
|
|
if (paneReplaceGeneration.get(paneRole) !== generation) {
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
const previousView = paneViewRegistry.get(paneRole);
|
2026-05-26 00:35:35 +08:00
|
|
|
|
if (previousView) {
|
|
|
|
|
|
unmountEditorViewBinding(previousView);
|
|
|
|
|
|
paneViewRegistry.delete(paneRole);
|
|
|
|
|
|
}
|
|
|
|
|
|
const bootstrap = buildBootstrapFromAggregate(aggregate, descriptor, paneRole);
|
2026-05-26 16:47:03 +08:00
|
|
|
|
syncPageAggregateScript({ pageAggregateScriptId: bootstrap.pageAggregateScriptId }, aggregate);
|
2026-07-15 23:12:15 +08:00
|
|
|
|
const runtimeDescriptor = { paneRole, root, observability, aggregate, bootstrap, pageBlockNavigation: descriptor.pageBlockNavigation === true };
|
2026-05-26 00:35:35 +08:00
|
|
|
|
updatePaneChrome(runtimeDescriptor);
|
|
|
|
|
|
if (paneRole === 'secondary' && workspace instanceof HTMLElement) {
|
|
|
|
|
|
workspace.setAttribute('data-has-secondary-pane', 'true');
|
|
|
|
|
|
setSecondaryEditorHostVisible(true);
|
|
|
|
|
|
const resizerNode = document.querySelector('[data-document-pane-resizer="true"]');
|
|
|
|
|
|
if (resizerNode instanceof HTMLElement) resizerNode.hidden = false;
|
|
|
|
|
|
applyStoredSecondaryWidth();
|
|
|
|
|
|
}
|
|
|
|
|
|
const session = getOrCreateDocumentSession(runtimeDescriptor);
|
|
|
|
|
|
const view = createEditorViewBinding(session, runtime, runtimeDescriptor);
|
2026-07-21 05:13:05 +08:00
|
|
|
|
syncDocumentEditModeChrome(session);
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const mountOptions = {
|
|
|
|
|
|
documentId: session.documentId,
|
|
|
|
|
|
workspaceId: session.workspaceId,
|
|
|
|
|
|
title: session.title,
|
|
|
|
|
|
content: session.currentTiptapDocument,
|
|
|
|
|
|
revision: session.revision,
|
|
|
|
|
|
conflictDetectionKey: session.conflictDetectionKey,
|
|
|
|
|
|
readOnly: session.readOnly,
|
|
|
|
|
|
editable: !session.readOnly,
|
|
|
|
|
|
pageOptions: runtimeDescriptor.aggregate.layout?.pageOptions || {},
|
|
|
|
|
|
};
|
|
|
|
|
|
setStatus(runtimeDescriptor, 'loading-assets');
|
|
|
|
|
|
clearEmbeddedLocalDraft(runtimeDescriptor);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const mountId = runtime.mount(runtimeDescriptor.root, mountOptions);
|
2026-07-28 17:04:27 +08:00
|
|
|
|
if (paneReplaceGeneration.get(paneRole) !== generation) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (typeof runtime.unmount === 'function') runtime.unmount(mountId);
|
|
|
|
|
|
} catch (_) { /* ignore stale unmount */ }
|
|
|
|
|
|
unmountEditorViewBinding(view);
|
|
|
|
|
|
return null;
|
|
|
|
|
|
}
|
2026-05-26 00:35:35 +08:00
|
|
|
|
view.mountId = mountId;
|
|
|
|
|
|
runtimeDescriptor.root.setAttribute('data-runtime-mount-id', String(mountId));
|
|
|
|
|
|
runtimeDescriptor.root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
|
|
|
|
|
|
setStatus(runtimeDescriptor, 'mounting-editor');
|
2026-05-26 09:44:35 +08:00
|
|
|
|
enhanceEditorAttachmentLinksSoon();
|
2026-05-26 00:35:35 +08:00
|
|
|
|
paneViewRegistry.set(paneRole, view);
|
2026-07-21 05:13:05 +08:00
|
|
|
|
syncDocumentEditModeChrome(session);
|
2026-05-26 00:35:35 +08:00
|
|
|
|
return view;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
unmountEditorViewBinding(view);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const descriptorFromCurrentUrl = (paneRole, documentId, explicit = {}) => {
|
|
|
|
|
|
const url = currentUrl();
|
|
|
|
|
|
if (paneRole === 'secondary') {
|
|
|
|
|
|
return {
|
|
|
|
|
|
documentId,
|
|
|
|
|
|
workspaceId: explicit.workspaceId || url.searchParams.get('workspaceId') || '',
|
2026-05-28 22:01:44 +08:00
|
|
|
|
sourceKind: explicit.sourceKind || url.searchParams.get('secondarySourceKind') || url.searchParams.get('sourceKind') || 'local_folder',
|
2026-05-26 00:35:35 +08:00
|
|
|
|
rootUri: explicit.rootUri || url.searchParams.get('secondaryRootUri') || url.searchParams.get('rootUri') || '',
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
return {
|
|
|
|
|
|
documentId,
|
|
|
|
|
|
workspaceId: explicit.workspaceId || url.searchParams.get('workspaceId') || '',
|
2026-05-28 22:01:44 +08:00
|
|
|
|
sourceKind: explicit.sourceKind || url.searchParams.get('sourceKind') || 'local_folder',
|
2026-05-26 00:35:35 +08:00
|
|
|
|
rootUri: explicit.rootUri || url.searchParams.get('rootUri') || '',
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const updatePrimaryUrl = (descriptor, urlFromCaller) => {
|
|
|
|
|
|
const url = urlFromCaller instanceof URL
|
|
|
|
|
|
? urlFromCaller
|
|
|
|
|
|
: new URL(`/documents/${encodeURIComponent(descriptor.documentId)}`, window.location.origin);
|
|
|
|
|
|
if (!url.searchParams.get('workspaceId') && descriptor.workspaceId) url.searchParams.set('workspaceId', descriptor.workspaceId);
|
2026-07-29 01:49:53 +08:00
|
|
|
|
if (descriptor.sourceKind) url.searchParams.set('sourceKind', descriptor.sourceKind);
|
2026-05-26 00:35:35 +08:00
|
|
|
|
if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
|
|
|
|
|
|
pushUrlState(url);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-07-15 23:12:15 +08:00
|
|
|
|
const openPrimaryDocumentFromPageBlockHref = (href) => {
|
|
|
|
|
|
const raw = typeof href === 'string' ? href.trim() : '';
|
|
|
|
|
|
if (!raw) return false;
|
|
|
|
|
|
let target = null;
|
|
|
|
|
|
try {
|
|
|
|
|
|
target = new URL(raw, window.location.href);
|
|
|
|
|
|
} catch (_) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (target.origin !== window.location.origin || !target.pathname.startsWith('/documents/')) return false;
|
|
|
|
|
|
const encodedId = target.pathname.slice('/documents/'.length).split('/')[0] || '';
|
|
|
|
|
|
const documentId = decodeURIComponent(encodedId).trim();
|
|
|
|
|
|
if (!documentId || typeof window.__mnoteDocumentPaneRuntime?.openPrimaryDocument !== 'function') return false;
|
|
|
|
|
|
const current = currentUrl();
|
|
|
|
|
|
const workspaceId = target.searchParams.get('workspaceId') || current.searchParams.get('workspaceId') || '';
|
|
|
|
|
|
const sourceKind = target.searchParams.get('sourceKind') || current.searchParams.get('sourceKind') || '';
|
|
|
|
|
|
const rootUri = target.searchParams.get('rootUri') || current.searchParams.get('rootUri') || '';
|
|
|
|
|
|
void window.__mnoteDocumentPaneRuntime.openPrimaryDocument({
|
|
|
|
|
|
documentId,
|
|
|
|
|
|
workspaceId,
|
|
|
|
|
|
sourceKind,
|
|
|
|
|
|
rootUri,
|
|
|
|
|
|
url: target,
|
|
|
|
|
|
pageBlockNavigation: true,
|
|
|
|
|
|
}).catch((error) => {
|
|
|
|
|
|
console.warn('mnote page block pane navigation failed, fallback to full navigation', error);
|
|
|
|
|
|
window.location.assign(target.pathname + target.search + target.hash);
|
|
|
|
|
|
});
|
|
|
|
|
|
return true;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const installPageBlockOpenCapture = () => {
|
|
|
|
|
|
if (window.__mnotePageBlockOpenCaptureInstalled === true) return;
|
|
|
|
|
|
window.__mnotePageBlockOpenCaptureInstalled = true;
|
|
|
|
|
|
document.addEventListener('mousedown', (event) => {
|
|
|
|
|
|
if (event.button !== 0) return;
|
|
|
|
|
|
const target = event.target;
|
|
|
|
|
|
const anchor = target instanceof Element ? target.closest('a.mnote-page-block-link') : null;
|
|
|
|
|
|
if (!(anchor instanceof HTMLAnchorElement)) return;
|
|
|
|
|
|
const href = anchor.getAttribute('href') || '';
|
|
|
|
|
|
if (!href) return;
|
|
|
|
|
|
// Tiptap's Link extension handles mouse down before the click bubble. Keep
|
|
|
|
|
|
// the target aside so that it cannot trigger a native document navigation.
|
|
|
|
|
|
anchor.setAttribute('data-mnote-page-block-href', href);
|
|
|
|
|
|
anchor.removeAttribute('href');
|
|
|
|
|
|
}, true);
|
|
|
|
|
|
document.addEventListener('click', (event) => {
|
2026-07-21 05:13:05 +08:00
|
|
|
|
const editToggle = event.target instanceof Element
|
|
|
|
|
|
? event.target.closest('[data-document-edit-mode-toggle]')
|
|
|
|
|
|
: null;
|
|
|
|
|
|
if (editToggle instanceof HTMLButtonElement) {
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
const pane = editToggle.closest('[data-document-pane="true"]');
|
|
|
|
|
|
const documentId = pane?.getAttribute('data-pane-document-id') || document.body?.dataset.documentId || '';
|
|
|
|
|
|
const session = Array.from(documentSessionRegistry.values())
|
|
|
|
|
|
.find((item) => item && item.documentId === documentId);
|
|
|
|
|
|
if (!session) return;
|
|
|
|
|
|
setDocumentSessionUserReadOnlyMode(
|
|
|
|
|
|
session,
|
|
|
|
|
|
!(session.userReadOnlyMode !== false),
|
|
|
|
|
|
'mnote-web-document-edit-mode-toggle',
|
|
|
|
|
|
);
|
|
|
|
|
|
syncDocumentEditModeChrome(session);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-07-15 23:12:15 +08:00
|
|
|
|
const target = event.target;
|
|
|
|
|
|
const anchor = target instanceof Element ? target.closest('a.mnote-page-block-link') : null;
|
|
|
|
|
|
if (!(anchor instanceof HTMLAnchorElement)) return;
|
|
|
|
|
|
const href = anchor.getAttribute('data-mnote-page-block-href') || anchor.getAttribute('href') || '';
|
|
|
|
|
|
if (!href) return;
|
|
|
|
|
|
anchor.removeAttribute('data-mnote-page-block-href');
|
|
|
|
|
|
anchor.removeAttribute('href');
|
|
|
|
|
|
if (!openPrimaryDocumentFromPageBlockHref(href)) {
|
|
|
|
|
|
anchor.setAttribute('href', href);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
event.preventDefault();
|
|
|
|
|
|
event.stopPropagation();
|
|
|
|
|
|
if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation();
|
|
|
|
|
|
}, true);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const setSecondaryEditorHostVisible = (visible) => {
|
|
|
|
|
|
const host = document.querySelector('[data-testid="mnote-secondary-editor-tab-host"]');
|
|
|
|
|
|
if (host instanceof HTMLElement) host.hidden = !visible;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const closeSecondaryPane = (url) => {
|
|
|
|
|
|
const view = paneViewRegistry.get('secondary');
|
|
|
|
|
|
if (view) {
|
|
|
|
|
|
unmountEditorViewBinding(view);
|
|
|
|
|
|
paneViewRegistry.delete('secondary');
|
|
|
|
|
|
}
|
|
|
|
|
|
const pane = document.querySelector('[data-document-pane="true"][data-pane-role="secondary"]');
|
|
|
|
|
|
if (pane instanceof HTMLElement) {
|
|
|
|
|
|
pane.hidden = true;
|
|
|
|
|
|
pane.setAttribute('data-pane-visible', 'false');
|
|
|
|
|
|
pane.removeAttribute('data-mnote-side-target');
|
|
|
|
|
|
pane.removeAttribute('data-pane-document-id');
|
|
|
|
|
|
pane.removeAttribute('data-pane-workspace-id');
|
|
|
|
|
|
}
|
|
|
|
|
|
const root = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="secondary"]`);
|
|
|
|
|
|
if (root instanceof HTMLElement) {
|
|
|
|
|
|
root.replaceChildren();
|
|
|
|
|
|
root.removeAttribute('data-mnote-side-target-unsupported');
|
|
|
|
|
|
root.removeAttribute('data-mnote-side-target-asset-id');
|
|
|
|
|
|
}
|
2026-05-26 01:15:28 +08:00
|
|
|
|
if (resourceTabs) resourceTabs.closeResourceTabsForPane('secondary');
|
2026-05-26 00:35:35 +08:00
|
|
|
|
if (workspace instanceof HTMLElement) {
|
|
|
|
|
|
workspace.setAttribute('data-has-secondary-pane', 'false');
|
|
|
|
|
|
workspace.style.removeProperty('grid-template-columns');
|
|
|
|
|
|
}
|
|
|
|
|
|
setSecondaryEditorHostVisible(false);
|
|
|
|
|
|
const resizerNode = document.querySelector('[data-document-pane-resizer="true"]');
|
|
|
|
|
|
if (resizerNode instanceof HTMLElement) resizerNode.hidden = true;
|
|
|
|
|
|
document.documentElement.removeAttribute('data-mnote-side-target-unsupported');
|
|
|
|
|
|
document.documentElement.removeAttribute('data-mnote-side-target-asset-id');
|
|
|
|
|
|
replaceUrlState(url);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const handleSessionChange = (session, view, event) => {
|
|
|
|
|
|
const payload = normalizeEnvelopePayload(event);
|
|
|
|
|
|
if (!payload) return;
|
|
|
|
|
|
const pendingExternalChange = session.sourceKind === 'local_folder' && session.externalChangePending;
|
|
|
|
|
|
const recentExternalChange = sessionHasRecentExternalSignal(session);
|
|
|
|
|
|
const recentLocalInput = sessionHasRecentLocalInput(session);
|
|
|
|
|
|
const tiptapDocument = hydrateMindmapAttrsFromDom(toTiptapDocument(
|
|
|
|
|
|
payload?.tiptapDocument || payload?.editorDocument || payload?.content,
|
|
|
|
|
|
currentEditorText(view),
|
|
|
|
|
|
), view.runtimeDescriptor.root);
|
|
|
|
|
|
const serialized = JSON.stringify(tiptapDocument);
|
|
|
|
|
|
if (view.suppressedSerialized && view.suppressedSerialized === serialized) {
|
|
|
|
|
|
view.suppressedSerialized = null;
|
|
|
|
|
|
view.lastKnownSerialized = serialized;
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const previousSerialized = session.currentSerialized;
|
|
|
|
|
|
session.currentTiptapDocument = tiptapDocument;
|
|
|
|
|
|
session.currentSerialized = serialized;
|
|
|
|
|
|
view.lastKnownSerialized = serialized;
|
|
|
|
|
|
if (typeof payload?.title === 'string' && payload.title.trim()) {
|
|
|
|
|
|
session.title = payload.title.trim();
|
|
|
|
|
|
}
|
|
|
|
|
|
if (payload?.meta && typeof payload.meta === 'object') {
|
|
|
|
|
|
if (Number.isInteger(payload.meta.revision)) session.revision = payload.meta.revision;
|
|
|
|
|
|
if (typeof payload.meta.conflictDetectionKey === 'string' && payload.meta.conflictDetectionKey.trim()) {
|
|
|
|
|
|
const nextMetaConflictKey = payload.meta.conflictDetectionKey.trim();
|
|
|
|
|
|
if (conflictDetectionKeyBelongsToSession(session, nextMetaConflictKey)) {
|
|
|
|
|
|
session.conflictDetectionKey = nextMetaConflictKey;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof payload.meta.readOnly === 'boolean') {
|
|
|
|
|
|
session.readOnly = payload.meta.readOnly;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
session.dirty = session.currentSerialized !== session.lastPersistedSerialized;
|
|
|
|
|
|
syncResourceSessionTabGuards(session);
|
|
|
|
|
|
if (serialized !== previousSerialized) {
|
|
|
|
|
|
broadcastSessionContent(session, view);
|
|
|
|
|
|
}
|
|
|
|
|
|
if ((pendingExternalChange || recentExternalChange) && (session.dirty || recentLocalInput)) {
|
2026-05-28 22:01:44 +08:00
|
|
|
|
if (session.sourceKind === 'local_folder' && session.sessionKind !== 'resource') {
|
|
|
|
|
|
session.externalChangePending = true;
|
|
|
|
|
|
if (session.dirty) queueSessionSave(session);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2026-05-26 00:35:35 +08:00
|
|
|
|
markSessionExternalConflict(session, externalConflictMessage);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (session.hasExternalConflict) {
|
|
|
|
|
|
setSessionStatus(session, 'external-change-conflict', externalConflictMessage);
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (session.dirty) {
|
|
|
|
|
|
queueSessionSave(session);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setSessionStatus(session, 'saved');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const unmountEditorViewBinding = (view, options = {}) => {
|
|
|
|
|
|
if (!view || view.disposed) return;
|
|
|
|
|
|
view.disposed = true;
|
|
|
|
|
|
const scheduleRelease = options.scheduleRelease !== false;
|
|
|
|
|
|
const root = view.runtimeDescriptor.root;
|
|
|
|
|
|
if (typeof view.disconnectObserver === 'function') {
|
|
|
|
|
|
view.disconnectObserver();
|
|
|
|
|
|
view.disconnectObserver = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (view.onReady) root.removeEventListener(READY_EVENT, view.onReady);
|
|
|
|
|
|
if (view.onError) root.removeEventListener(ERROR_EVENT, view.onError);
|
|
|
|
|
|
if (view.onChange) root.removeEventListener(CHANGE_EVENT, view.onChange);
|
|
|
|
|
|
if (view.onSave) root.removeEventListener(SAVE_EVENT, view.onSave);
|
|
|
|
|
|
if (view.onState) root.removeEventListener(STATE_EVENT, view.onState);
|
|
|
|
|
|
if (view.onKeydown) root.removeEventListener('keydown', view.onKeydown, true);
|
|
|
|
|
|
if (view.onInput) root.removeEventListener('input', view.onInput);
|
|
|
|
|
|
if (view.disconnectSlashObserver) {
|
|
|
|
|
|
view.disconnectSlashObserver();
|
|
|
|
|
|
view.disconnectSlashObserver = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
clearSessionConflictSurface(view.session);
|
|
|
|
|
|
if (view.mountId != null && typeof view.runtime?.unmount === 'function') {
|
|
|
|
|
|
try {
|
|
|
|
|
|
view.runtime.unmount(view.mountId);
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
console.warn('mnote editor view unmount failed', error);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
view.mountId = null;
|
|
|
|
|
|
root.removeAttribute('data-runtime-mount-id');
|
|
|
|
|
|
if (view.session.views.get(view.id) === view) {
|
|
|
|
|
|
view.session.views.delete(view.id);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (scheduleRelease) {
|
|
|
|
|
|
scheduleDocumentSessionRelease(view.session);
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
const observeEditorViewBinding = (view) => {
|
|
|
|
|
|
if (typeof MutationObserver !== 'function' || !(document.body instanceof HTMLElement)) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
const observer = new MutationObserver(() => {
|
|
|
|
|
|
if (!view.disposed && !view.runtimeDescriptor.root.isConnected) {
|
|
|
|
|
|
unmountEditorViewBinding(view);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
observer.observe(document.body, { childList: true, subtree: true });
|
|
|
|
|
|
view.disconnectObserver = () => observer.disconnect();
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
installGlobalSlashMenuPositioning();
|
|
|
|
|
|
|
|
|
|
|
|
const createEditorViewBinding = (session, runtime, runtimeDescriptor) => {
|
|
|
|
|
|
cancelDocumentSessionRelease(session);
|
|
|
|
|
|
const view = {
|
|
|
|
|
|
id: nextViewId++,
|
|
|
|
|
|
mountId: null,
|
|
|
|
|
|
ready: false,
|
|
|
|
|
|
disposed: false,
|
|
|
|
|
|
suppressedSerialized: null,
|
|
|
|
|
|
lastKnownSerialized: session.currentSerialized,
|
|
|
|
|
|
runtime,
|
|
|
|
|
|
runtimeDescriptor,
|
|
|
|
|
|
session,
|
|
|
|
|
|
disconnectObserver: null,
|
|
|
|
|
|
onReady: null,
|
|
|
|
|
|
onError: null,
|
|
|
|
|
|
onChange: null,
|
|
|
|
|
|
onSave: null,
|
|
|
|
|
|
onState: null,
|
|
|
|
|
|
onKeydown: null,
|
|
|
|
|
|
onInput: null,
|
|
|
|
|
|
disconnectSlashObserver: null,
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
view.onReady = () => {
|
|
|
|
|
|
view.ready = true;
|
|
|
|
|
|
if (view.lastKnownSerialized !== session.currentSerialized) {
|
|
|
|
|
|
dispatchSessionContentToView(session, view, 'mnote-web-document-session-ready-sync');
|
|
|
|
|
|
}
|
|
|
|
|
|
if (session.status === 'dirty' || session.status === 'saving' || session.status === 'saved' || session.status === 'error' || session.status === 'external-change-conflict' || session.status === 'synced-external-change') {
|
|
|
|
|
|
setStatus(runtimeDescriptor, session.status, session.error);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
setStatus(runtimeDescriptor, 'ready');
|
|
|
|
|
|
}
|
2026-05-28 22:01:44 +08:00
|
|
|
|
if (session.pageBodySource) runtimeDescriptor.root.setAttribute('data-mnote-page-body-source', session.pageBodySource);
|
2026-06-07 10:35:21 +08:00
|
|
|
|
runtimeDescriptor.root.setAttribute('data-mnote-page-body-local-compat-fallback', session.pageBodyLocalCompatFallback ? 'true' : 'false');
|
|
|
|
|
|
if (session.pageBodyHardGuard) runtimeDescriptor.root.setAttribute('data-mnote-page-body-hard-guard', session.pageBodyHardGuard);
|
2026-05-28 22:01:44 +08:00
|
|
|
|
if (session.projectionSource) runtimeDescriptor.root.setAttribute('data-mnote-projection-source', session.projectionSource);
|
|
|
|
|
|
if (session.blockProjectionVersion) runtimeDescriptor.root.setAttribute('data-mnote-block-projection-version', session.blockProjectionVersion);
|
2026-05-26 09:44:35 +08:00
|
|
|
|
enhanceEditorAttachmentLinksSoon();
|
2026-06-04 18:51:16 +08:00
|
|
|
|
applyDocumentEvidenceLocatorFromUrl(runtimeDescriptor.root);
|
2026-05-26 00:35:35 +08:00
|
|
|
|
};
|
|
|
|
|
|
view.onError = (event) => {
|
|
|
|
|
|
const payload = normalizeEnvelopePayload(event);
|
|
|
|
|
|
setStatus(runtimeDescriptor, 'error', payload?.message || 'runtime_error');
|
|
|
|
|
|
};
|
|
|
|
|
|
view.onChange = (event) => {
|
|
|
|
|
|
scheduleSlashMenuPosition(runtimeDescriptor.root);
|
|
|
|
|
|
handleSessionChange(session, view, event);
|
2026-05-26 09:44:35 +08:00
|
|
|
|
enhanceEditorAttachmentLinksSoon();
|
2026-05-26 00:35:35 +08:00
|
|
|
|
};
|
|
|
|
|
|
view.onSave = (event) => {
|
|
|
|
|
|
handleSessionChange(session, view, event);
|
2026-05-26 09:44:35 +08:00
|
|
|
|
enhanceEditorAttachmentLinksSoon();
|
2026-05-26 00:35:35 +08:00
|
|
|
|
};
|
|
|
|
|
|
view.onState = () => {
|
|
|
|
|
|
scheduleSlashMenuPosition(runtimeDescriptor.root);
|
2026-05-26 09:44:35 +08:00
|
|
|
|
enhanceEditorAttachmentLinksSoon();
|
2026-05-26 00:35:35 +08:00
|
|
|
|
};
|
|
|
|
|
|
view.onKeydown = (event) => {
|
|
|
|
|
|
if (event.key === '/') scheduleSlashMenuPosition(runtimeDescriptor.root);
|
|
|
|
|
|
};
|
|
|
|
|
|
view.onInput = (event) => {
|
|
|
|
|
|
const target = event.target;
|
|
|
|
|
|
if (!(target instanceof Element) || !target.closest('.ProseMirror')) return;
|
|
|
|
|
|
session.lastUserInputAt = Date.now();
|
|
|
|
|
|
scheduleSlashMenuPosition(runtimeDescriptor.root);
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
runtimeDescriptor.root.addEventListener(READY_EVENT, view.onReady);
|
|
|
|
|
|
runtimeDescriptor.root.addEventListener(ERROR_EVENT, view.onError);
|
|
|
|
|
|
runtimeDescriptor.root.addEventListener(CHANGE_EVENT, view.onChange);
|
|
|
|
|
|
runtimeDescriptor.root.addEventListener(SAVE_EVENT, view.onSave);
|
|
|
|
|
|
runtimeDescriptor.root.addEventListener(STATE_EVENT, view.onState);
|
|
|
|
|
|
runtimeDescriptor.root.addEventListener('keydown', view.onKeydown, true);
|
|
|
|
|
|
runtimeDescriptor.root.addEventListener('input', view.onInput);
|
|
|
|
|
|
session.views.set(view.id, view);
|
|
|
|
|
|
observeSlashMenuPosition(view);
|
|
|
|
|
|
observeEditorViewBinding(view);
|
|
|
|
|
|
return view;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 01:15:28 +08:00
|
|
|
|
resourceTabs = createResourceTabRuntime({
|
|
|
|
|
|
loadRuntime,
|
|
|
|
|
|
createEditorViewBinding,
|
2026-05-28 22:01:44 +08:00
|
|
|
|
ensureLocalFolderEventChannel,
|
2026-05-26 01:15:28 +08:00
|
|
|
|
unmountEditorViewBinding,
|
|
|
|
|
|
setStatus,
|
|
|
|
|
|
documentSessionRegistry,
|
|
|
|
|
|
sessionHasRecentLocalInput,
|
|
|
|
|
|
currentUrl,
|
|
|
|
|
|
replaceUrlState,
|
|
|
|
|
|
applyStoredSecondaryWidth,
|
|
|
|
|
|
workspace,
|
|
|
|
|
|
setSecondaryEditorHostVisible,
|
|
|
|
|
|
markIntendedSlashRoot,
|
|
|
|
|
|
toTiptapDocument,
|
|
|
|
|
|
openMindmapResourceTab: (...args) => mindmapHost.openMindmapResourceTab(...args),
|
|
|
|
|
|
unmountMindmapPane: (...args) => mindmapHost.unmountMindmapPane(...args),
|
|
|
|
|
|
paneViewRegistry,
|
|
|
|
|
|
rootSelector: ROOT_SELECTOR,
|
|
|
|
|
|
currentDocumentId: () => currentWebShellDocumentId(),
|
|
|
|
|
|
currentWorkspaceId: () => currentWebShellWorkspaceId(),
|
|
|
|
|
|
secondaryQueryParamNames,
|
2026-05-26 00:35:35 +08:00
|
|
|
|
});
|
2026-05-26 01:15:28 +08:00
|
|
|
|
const normalizePaneRole = (...args) => resourceTabs.normalizePaneRole(...args);
|
|
|
|
|
|
const resolveResourceOpen = (...args) => resourceTabs.resolveResourceOpen(...args);
|
|
|
|
|
|
const openResourceInActiveTab = (...args) => resourceTabs.openResourceInActiveTab(...args);
|
|
|
|
|
|
const activateMainEditorTab = (...args) => resourceTabs.activateMainEditorTab(...args);
|
|
|
|
|
|
const bindMainEditorPageTab = (...args) => resourceTabs.bindMainEditorPageTab(...args);
|
|
|
|
|
|
const buildOpenEditorsSnapshot = (...args) => resourceTabs.buildOpenEditorsSnapshot(...args);
|
2026-05-26 00:35:35 +08:00
|
|
|
|
|
|
|
|
|
|
const mountPane = async (runtimeDescriptor) => {
|
|
|
|
|
|
const runtime = await loadRuntime();
|
|
|
|
|
|
const session = getOrCreateDocumentSession(runtimeDescriptor);
|
|
|
|
|
|
const view = createEditorViewBinding(session, runtime, runtimeDescriptor);
|
2026-07-21 05:13:05 +08:00
|
|
|
|
syncDocumentEditModeChrome(session);
|
2026-05-26 00:35:35 +08:00
|
|
|
|
|
|
|
|
|
|
const mountOptions = {
|
|
|
|
|
|
documentId: session.documentId,
|
|
|
|
|
|
workspaceId: session.workspaceId,
|
|
|
|
|
|
title: session.title,
|
|
|
|
|
|
content: session.currentTiptapDocument,
|
|
|
|
|
|
revision: session.revision,
|
|
|
|
|
|
conflictDetectionKey: session.conflictDetectionKey,
|
|
|
|
|
|
readOnly: session.readOnly,
|
|
|
|
|
|
editable: !session.readOnly,
|
|
|
|
|
|
pageOptions: runtimeDescriptor.aggregate.layout?.pageOptions || {},
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
setStatus(runtimeDescriptor, 'loading-assets');
|
|
|
|
|
|
clearEmbeddedLocalDraft(runtimeDescriptor);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const mountId = runtime.mount(runtimeDescriptor.root, mountOptions);
|
|
|
|
|
|
view.mountId = mountId;
|
|
|
|
|
|
runtimeDescriptor.root.setAttribute('data-runtime-mount-id', String(mountId));
|
|
|
|
|
|
runtimeDescriptor.root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
|
|
|
|
|
|
setStatus(runtimeDescriptor, 'mounting-editor');
|
2026-05-26 09:44:35 +08:00
|
|
|
|
enhanceEditorAttachmentLinksSoon();
|
2026-05-26 00:35:35 +08:00
|
|
|
|
paneViewRegistry.set(runtimeDescriptor.paneRole, view);
|
2026-07-21 05:13:05 +08:00
|
|
|
|
syncDocumentEditModeChrome(session);
|
2026-05-26 00:35:35 +08:00
|
|
|
|
} catch (error) {
|
|
|
|
|
|
unmountEditorViewBinding(view);
|
|
|
|
|
|
throw error;
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
window.__mnoteDocumentPaneRuntime = {
|
2026-07-15 23:12:15 +08:00
|
|
|
|
openPrimaryDocument: async ({ documentId, workspaceId, sourceKind, rootUri, url, pageBlockNavigation } = {}) => {
|
2026-05-26 00:35:35 +08:00
|
|
|
|
const id = typeof documentId === 'string' ? documentId.trim() : '';
|
|
|
|
|
|
if (!id) return false;
|
|
|
|
|
|
const descriptor = descriptorFromCurrentUrl('primary', id, { workspaceId, sourceKind, rootUri });
|
2026-07-15 23:12:15 +08:00
|
|
|
|
descriptor.pageBlockNavigation = pageBlockNavigation === true;
|
2026-07-28 17:04:27 +08:00
|
|
|
|
const view = await replacePaneDocument('primary', descriptor);
|
|
|
|
|
|
// 过期导航(快速连点被 generation 丢弃)不得改 URL / 激活 tab。
|
|
|
|
|
|
if (!view) return false;
|
2026-05-26 00:35:35 +08:00
|
|
|
|
activateMainEditorTab('', 'primary');
|
|
|
|
|
|
updatePrimaryUrl(descriptor, url instanceof URL ? url : null);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
},
|
|
|
|
|
|
openPrimaryMindmap: async ({ documentId, mindmapId, workspaceId, url } = {}) => {
|
|
|
|
|
|
const docId = typeof documentId === 'string' ? documentId.trim() : '';
|
|
|
|
|
|
const mapId = typeof mindmapId === 'string' ? mindmapId.trim() : '';
|
|
|
|
|
|
if (!docId || !mapId) return false;
|
2026-05-26 01:03:04 +08:00
|
|
|
|
await mindmapHost.replacePrimaryPaneMindmap({
|
2026-05-26 00:35:35 +08:00
|
|
|
|
documentId: docId,
|
|
|
|
|
|
mindmapId: mapId,
|
|
|
|
|
|
workspaceId: typeof workspaceId === 'string' ? workspaceId.trim() : '',
|
|
|
|
|
|
url,
|
|
|
|
|
|
});
|
|
|
|
|
|
return true;
|
|
|
|
|
|
},
|
|
|
|
|
|
openSecondaryDocument: async ({ documentId, workspaceId, sourceKind, rootUri, url } = {}) => {
|
|
|
|
|
|
const id = typeof documentId === 'string' ? documentId.trim() : '';
|
|
|
|
|
|
if (!id) return false;
|
|
|
|
|
|
const descriptor = descriptorFromCurrentUrl('secondary', id, { workspaceId, sourceKind, rootUri });
|
2026-07-28 17:04:27 +08:00
|
|
|
|
const view = await replacePaneDocument('secondary', descriptor);
|
|
|
|
|
|
if (!view) return false;
|
2026-05-26 00:35:35 +08:00
|
|
|
|
activateMainEditorTab('', 'secondary');
|
|
|
|
|
|
if (url instanceof URL) replaceUrlState(url);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
},
|
|
|
|
|
|
resolveResourceOpen: (input) => resolveResourceOpen(input),
|
|
|
|
|
|
openResourceInActiveTab: openResourceInActiveTab,
|
|
|
|
|
|
activatePageTab: ({ paneRole } = {}) => {
|
|
|
|
|
|
const role = normalizePaneRole(paneRole || 'primary');
|
|
|
|
|
|
bindMainEditorPageTab(role);
|
|
|
|
|
|
activateMainEditorTab('', role);
|
|
|
|
|
|
return true;
|
|
|
|
|
|
},
|
|
|
|
|
|
openResourceAsSideTarget: async (input = {}) => {
|
|
|
|
|
|
return openResourceInActiveTab({ ...input, paneRole: 'secondary', openTarget: 'active-tab' });
|
|
|
|
|
|
},
|
|
|
|
|
|
closeSecondaryDocument: ({ url } = {}) => {
|
|
|
|
|
|
closeSecondaryPane(url instanceof URL ? url : currentUrl());
|
|
|
|
|
|
return true;
|
|
|
|
|
|
},
|
|
|
|
|
|
getOpenEditorsSnapshot: () => buildOpenEditorsSnapshot(),
|
|
|
|
|
|
refreshDocument: async ({ documentId, workspaceId, source } = {}) => {
|
|
|
|
|
|
const targets = Array.from(documentSessionRegistry.values()).filter((session) => (
|
|
|
|
|
|
sessionMatchesDocumentWorkspace(session, documentId, workspaceId)
|
|
|
|
|
|
));
|
|
|
|
|
|
await Promise.all(targets.map((session) => refreshSessionFromExternalChange(
|
|
|
|
|
|
session,
|
|
|
|
|
|
source || 'mnote-web-programmatic-refresh',
|
|
|
|
|
|
)));
|
|
|
|
|
|
return targets.length;
|
|
|
|
|
|
},
|
|
|
|
|
|
refreshPrimaryDocument: async ({ documentId, workspaceId, source } = {}) => {
|
|
|
|
|
|
const primary = paneViewRegistry.get('primary');
|
|
|
|
|
|
if (!primary?.session) return 0;
|
|
|
|
|
|
if (!sessionMatchesDocumentWorkspace(primary.session, documentId, workspaceId)) return 0;
|
|
|
|
|
|
await refreshSessionFromExternalChange(primary.session, source || 'mnote-web-programmatic-refresh');
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
2026-07-15 23:12:15 +08:00
|
|
|
|
installPageBlockOpenCapture();
|
2026-05-26 00:35:35 +08:00
|
|
|
|
|
|
|
|
|
|
bindMainEditorPageTab('primary');
|
|
|
|
|
|
bindMainEditorPageTab('secondary');
|
|
|
|
|
|
|
2026-05-26 16:24:00 +08:00
|
|
|
|
const shouldPrewarmPrimaryRuntime = paneRuntimes.length === 0
|
|
|
|
|
|
&& document.querySelector('.mnote-workspace-empty-editor-host')
|
|
|
|
|
|
&& document.querySelector(`${ROOT_SELECTOR}[data-pane-role="primary"]`);
|
|
|
|
|
|
if (shouldPrewarmPrimaryRuntime) {
|
|
|
|
|
|
const prewarmRuntime = () => {
|
|
|
|
|
|
loadRuntime().catch((error) => {
|
|
|
|
|
|
console.warn('mnote editor runtime prewarm failed', error);
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
if (typeof window.requestIdleCallback === 'function') {
|
|
|
|
|
|
window.requestIdleCallback(prewarmRuntime, { timeout: 2000 });
|
|
|
|
|
|
} else {
|
|
|
|
|
|
window.setTimeout(prewarmRuntime, 800);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-26 00:35:35 +08:00
|
|
|
|
window.addEventListener('pagehide', () => {
|
|
|
|
|
|
Array.from(documentSessionRegistry.values()).forEach((session) => {
|
|
|
|
|
|
sessionViews(session).forEach((view) => {
|
|
|
|
|
|
unmountEditorViewBinding(view, { scheduleRelease: false });
|
|
|
|
|
|
});
|
|
|
|
|
|
releaseDocumentSession(session);
|
|
|
|
|
|
});
|
|
|
|
|
|
}, { once: true });
|
|
|
|
|
|
|
2026-06-07 01:10:31 +08:00
|
|
|
|
const restoreResourceTabFromUrl = () => {
|
|
|
|
|
|
const restoreInput = restoreResourceTabInputFromUrl();
|
|
|
|
|
|
if (!restoreInput) return Promise.resolve(false);
|
|
|
|
|
|
return openResourceInActiveTab(restoreInput).catch((error) => {
|
|
|
|
|
|
console.warn('mnote resource tab URL 恢复失败', error);
|
|
|
|
|
|
return false;
|
|
|
|
|
|
});
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2026-05-26 00:35:35 +08:00
|
|
|
|
if (paneRuntimes.length) {
|
2026-05-28 22:01:44 +08:00
|
|
|
|
Promise.all(paneRuntimes.map((paneRuntime) => mountPane(paneRuntime))).then(() => {
|
2026-06-07 01:10:31 +08:00
|
|
|
|
return restoreResourceTabFromUrl();
|
2026-05-28 22:01:44 +08:00
|
|
|
|
}).catch((error) => {
|
2026-05-26 00:35:35 +08:00
|
|
|
|
console.error('mnote multi-pane editor mount failed', error);
|
|
|
|
|
|
});
|
2026-06-07 01:10:31 +08:00
|
|
|
|
} else {
|
|
|
|
|
|
void restoreResourceTabFromUrl();
|
2026-05-26 00:35:35 +08:00
|
|
|
|
}
|
|
|
|
|
|
})();
|