Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
1293 lines
59 KiB
JavaScript
1293 lines
59 KiB
JavaScript
import { createMindmapHostRuntime } from './document-mindmap-host-runtime.js';
|
||
import { createResourceTabRuntime } from './document-resource-tab-runtime.js';
|
||
import { createDocumentSessionRuntime } from './document-session-runtime.js';
|
||
import {
|
||
installGlobalSlashMenuPositioning,
|
||
markIntendedSlashRoot,
|
||
observeSlashMenuPosition,
|
||
scheduleSlashMenuPosition,
|
||
} from './document-slash-position-runtime.js';
|
||
import {
|
||
buildPaneRuntime,
|
||
clearSecondaryParams,
|
||
createSecondaryPaneWidthController,
|
||
currentUrl,
|
||
installSecondaryPaneCloseButtons,
|
||
installSecondaryPaneOpenListeners,
|
||
pushUrlState,
|
||
replaceUrlState,
|
||
secondaryQueryParamNames,
|
||
} from './document-pane-runtime.js';
|
||
import {
|
||
conflictDetectionKeyBelongsToSession,
|
||
hydrateMindmapAttrsFromDom,
|
||
legacyInlineContentToTiptap,
|
||
toTiptapDocument,
|
||
} from './document-tiptap-conversion-runtime.js';
|
||
|
||
(() => {
|
||
const PANES_BOOTSTRAP_ID = '__MNOTE_DOCUMENT_PANES_BOOTSTRAP__';
|
||
const ROOT_SELECTOR = '[data-testid="mnote-leptos-tiptap-island-editor-root"]';
|
||
// 历史 spike 前缀仍是 island runtime 的默认派发;稳定前缀 `mnote:tiptap-island`
|
||
// 由 island 双发(架构收口 21 Phase 3),host 优先监听 spike 事件保持兼容。
|
||
const EVENT_PREFIX = 'mnote:leptos-tiptap-spike';
|
||
const STABLE_EVENT_PREFIX = 'mnote:tiptap-island';
|
||
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';
|
||
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();
|
||
};
|
||
|
||
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);
|
||
};
|
||
|
||
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();
|
||
installSecondaryPaneOpenListeners();
|
||
installSecondaryPaneCloseButtons();
|
||
const { workspace, applyStoredSecondaryWidth } = createSecondaryPaneWidthController();
|
||
applyStoredSecondaryWidth();
|
||
|
||
const paneRuntimes = panesBootstrap.panes
|
||
.map((paneDescriptor) => buildPaneRuntime(paneDescriptor, ROOT_SELECTOR))
|
||
.filter(Boolean);
|
||
|
||
const loadRuntime = async () => {
|
||
if (window.__mnoteLeptosTiptapRuntimePromise) {
|
||
return window.__mnoteLeptosTiptapRuntimePromise;
|
||
}
|
||
window.__mnoteLeptosTiptapRuntimePromise = (async () => {
|
||
const manifestResponse = await fetch(withDevHot('/api/leptos-tiptap-runtime/manifest.json'));
|
||
if (!manifestResponse.ok) throw new Error(`manifest_failed_${manifestResponse.status}`);
|
||
const manifest = await manifestResponse.json();
|
||
if (!manifest.entryAssetPath) throw new Error('island manifest 缺少 entryAssetPath');
|
||
const entryUrl = withDevHot(`/api/leptos-tiptap-runtime/${manifest.entryAssetPath}`);
|
||
const wasmUrl = manifest.wasmAssetPath ? withDevHot(`/api/leptos-tiptap-runtime/${manifest.wasmAssetPath}`) : undefined;
|
||
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;
|
||
};
|
||
|
||
// Tiptap/legacy conversion helpers live in document-tiptap-conversion-runtime.js.
|
||
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');
|
||
}
|
||
};
|
||
|
||
const enhanceEditorAttachmentLinksSoon = () => {
|
||
const run = () => {
|
||
if (typeof window.__mnoteEnhanceEditorAttachmentLinks === 'function') {
|
||
window.__mnoteEnhanceEditorAttachmentLinks();
|
||
}
|
||
};
|
||
run();
|
||
[120, 500, 1200].forEach((delayMs) => window.setTimeout(run, delayMs));
|
||
};
|
||
|
||
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);
|
||
url.searchParams.set('sourceKind', descriptor.sourceKind || 'local_folder');
|
||
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,
|
||
sourceKind: descriptor.sourceKind || 'local_folder',
|
||
rootUri: descriptor.rootUri || '',
|
||
pageAggregateScriptId: paneRole === 'secondary' ? '__MNOTE_SECONDARY_PAGE_AGGREGATE__' : '__MNOTE_PAGE_AGGREGATE__',
|
||
saveEndpoint: (descriptor.sourceKind || 'local_folder') === 'local_folder'
|
||
? '/api/page-body/write'
|
||
: '/api/documents/save',
|
||
titleEndpoint: '/api/documents/title',
|
||
editorHostKind: 'leptos_tiptap_island',
|
||
});
|
||
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;
|
||
};
|
||
const syncPageAggregateScript = (session, aggregate) => {
|
||
if (!session || !aggregate) return;
|
||
const scriptId = session.pageAggregateScriptId || '__MNOTE_PAGE_AGGREGATE__';
|
||
let node = document.getElementById(scriptId);
|
||
if (!node) {
|
||
node = document.createElement('script');
|
||
node.id = scriptId;
|
||
node.type = 'application/json';
|
||
document.body.appendChild(node);
|
||
}
|
||
try {
|
||
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);
|
||
node.setAttribute('data-mnote-page-aggregate-synced-at', String(Date.now()));
|
||
window.dispatchEvent(new CustomEvent('mnote:page-aggregate-synced', {
|
||
detail: { scriptId, documentId: aggregateDocumentId(nextAggregate) }
|
||
}));
|
||
enhanceEditorAttachmentLinksSoon();
|
||
} catch (error) {
|
||
console.warn('mnote Page Aggregate script 同步失败', error);
|
||
}
|
||
};
|
||
|
||
const paneViewRegistry = new Map();
|
||
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 '';
|
||
};
|
||
const currentWebShellSourceKind = () => {
|
||
try {
|
||
return currentUrl().searchParams.get('sourceKind') || '';
|
||
} catch (_) {
|
||
return '';
|
||
}
|
||
};
|
||
const currentWebShellRootUri = () => {
|
||
try {
|
||
return currentUrl().searchParams.get('rootUri') || '';
|
||
} catch (_) {
|
||
return '';
|
||
}
|
||
};
|
||
const cssSafe = (value) => {
|
||
const text = String(value || '');
|
||
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(text);
|
||
return text.replace(/["\\]/g, '\\$&');
|
||
};
|
||
const 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 '';
|
||
const name = String(title || path || '');
|
||
if (/\.pdf$/i.test(name)) {
|
||
const url = new URL('/pdf-preview', window.location.origin);
|
||
url.searchParams.set('fileUrl', fileUrl);
|
||
url.searchParams.set('fileName', title || path);
|
||
return url.toString();
|
||
}
|
||
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();
|
||
}
|
||
return fileUrl;
|
||
};
|
||
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);
|
||
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;
|
||
}
|
||
}
|
||
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' });
|
||
}
|
||
};
|
||
|
||
const restoreResourceTabInputFromUrl = () => {
|
||
const url = currentUrl();
|
||
const raw = String(url.searchParams.get('resourceTab') || '').trim();
|
||
if (!raw) return null;
|
||
const objectIdentity = raw.startsWith('resource:') ? raw : (raw.includes('::') ? raw.slice(raw.indexOf('::') + 2) : raw);
|
||
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;
|
||
const href = resourceHrefFromUrlState(rootUri, path, title);
|
||
return {
|
||
objectIdentity,
|
||
documentId: currentWebShellDocumentId(),
|
||
workspaceId: currentWebShellWorkspaceId(),
|
||
sourceKind: currentWebShellSourceKind() || 'local_folder',
|
||
rootUri,
|
||
path,
|
||
assetId: `local:asset:${path}`,
|
||
title,
|
||
fileName: title,
|
||
href,
|
||
officeUrl: href,
|
||
openTarget: 'active-tab',
|
||
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(),
|
||
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(),
|
||
evidenceText: String(url.searchParams.get('evidenceText') || '').trim(),
|
||
lineRange: url.searchParams.get('lineRange') || null,
|
||
charRange: url.searchParams.get('charRange') || null,
|
||
};
|
||
};
|
||
|
||
let nextViewId = 1;
|
||
const mindmapHost = createMindmapHostRuntime({
|
||
loadRuntime,
|
||
paneViewRegistry,
|
||
unmountEditorViewBinding: (...args) => unmountEditorViewBinding(...args),
|
||
pushUrlState,
|
||
rootSelector: ROOT_SELECTOR,
|
||
currentDocumentId: () => currentWebShellDocumentId(),
|
||
});
|
||
|
||
let resourceTabs = null;
|
||
const syncResourceSessionTabGuards = (session) => {
|
||
if (resourceTabs) resourceTabs.syncResourceSessionTabGuards(session);
|
||
};
|
||
|
||
const documentSessions = createDocumentSessionRuntime({
|
||
bridgeProtocol: BRIDGE_PROTOCOL,
|
||
commandEvent: COMMAND_EVENT,
|
||
normalizeEnvelopePayload,
|
||
pageAggregateUrl,
|
||
setStatus,
|
||
syncPageAggregateScript,
|
||
syncResourceSessionTabGuards,
|
||
});
|
||
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);
|
||
const ensureLocalFolderEventChannel = (...args) => documentSessions.ensureLocalFolderEventChannel(...args);
|
||
const sessionMatchesDocumentWorkspace = (...args) => documentSessions.sessionMatchesDocumentWorkspace(...args);
|
||
const getOrCreateDocumentSession = (...args) => documentSessions.getOrCreateDocumentSession(...args);
|
||
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;
|
||
}
|
||
});
|
||
};
|
||
|
||
const updatePaneChrome = (runtimeDescriptor) => {
|
||
const pane = runtimeDescriptor.root.closest('[data-document-pane="true"]');
|
||
if (!(pane instanceof HTMLElement)) return;
|
||
if (runtimeDescriptor.paneRole === 'primary') {
|
||
document.querySelectorAll('[data-mnote-navigation-page-placeholder][data-pane-role="primary"]').forEach((node) => {
|
||
if (node instanceof HTMLElement) node.hidden = true;
|
||
});
|
||
}
|
||
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)));
|
||
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));
|
||
}
|
||
}
|
||
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;
|
||
refreshBreadcrumb(documentId, title, workspaceId);
|
||
window.dispatchEvent(new CustomEvent('mnote:primary-document-activated', {
|
||
detail: {
|
||
documentId,
|
||
workspaceId,
|
||
title,
|
||
sourceKind: bootstrap.sourceKind || '',
|
||
rootUri: bootstrap.rootUri || '',
|
||
pageBlockNavigation: runtimeDescriptor.pageBlockNavigation === true,
|
||
}
|
||
}));
|
||
document.documentElement.removeAttribute('data-mnote-side-target-unsupported');
|
||
document.documentElement.removeAttribute('data-mnote-side-target-asset-id');
|
||
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) => {
|
||
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, '\\$&');
|
||
document.querySelectorAll(`${activeTreeRows}[data-node-id="${escapedId}"], ${activeTreeRows}[data-document-id="${escapedId}"], ${activeTreeRows}[data-doc-id="${escapedId}"]`).forEach((row) => {
|
||
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;
|
||
};
|
||
|
||
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);
|
||
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 = '开始编辑';
|
||
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);
|
||
header.append(titleHeading, modeToggle, meta);
|
||
|
||
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;
|
||
};
|
||
|
||
const replacePaneDocument = async (paneRole, descriptor, options = {}) => {
|
||
mindmapHost.unmountMindmapPane(paneRole);
|
||
const root = paneRole === 'primary'
|
||
? ensureLazyPrimaryPaneRoot()
|
||
: document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`);
|
||
const observability = document.querySelector(`[data-editor-host-observability][data-pane-role="${paneRole}"]`);
|
||
if (!(root instanceof HTMLElement)) throw new Error(`pane_root_missing_${paneRole}`);
|
||
const runtimePromise = loadRuntime();
|
||
const aggregatePromise = options.aggregate
|
||
? Promise.resolve(options.aggregate)
|
||
: fetchPageAggregateForPane(descriptor);
|
||
const previousView = paneViewRegistry.get(paneRole);
|
||
const [runtime, aggregate] = await Promise.all([runtimePromise, aggregatePromise]);
|
||
if (previousView) {
|
||
unmountEditorViewBinding(previousView);
|
||
paneViewRegistry.delete(paneRole);
|
||
}
|
||
const bootstrap = buildBootstrapFromAggregate(aggregate, descriptor, paneRole);
|
||
syncPageAggregateScript({ pageAggregateScriptId: bootstrap.pageAggregateScriptId }, aggregate);
|
||
const runtimeDescriptor = { paneRole, root, observability, aggregate, bootstrap, pageBlockNavigation: descriptor.pageBlockNavigation === true };
|
||
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);
|
||
syncDocumentEditModeChrome(session);
|
||
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');
|
||
enhanceEditorAttachmentLinksSoon();
|
||
paneViewRegistry.set(paneRole, view);
|
||
syncDocumentEditModeChrome(session);
|
||
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') || '',
|
||
sourceKind: explicit.sourceKind || url.searchParams.get('secondarySourceKind') || url.searchParams.get('sourceKind') || 'local_folder',
|
||
rootUri: explicit.rootUri || url.searchParams.get('secondaryRootUri') || url.searchParams.get('rootUri') || '',
|
||
};
|
||
}
|
||
return {
|
||
documentId,
|
||
workspaceId: explicit.workspaceId || url.searchParams.get('workspaceId') || '',
|
||
sourceKind: explicit.sourceKind || url.searchParams.get('sourceKind') || 'local_folder',
|
||
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);
|
||
if (descriptor.sourceKind && descriptor.sourceKind !== 'convex_workspace') url.searchParams.set('sourceKind', descriptor.sourceKind);
|
||
if (descriptor.rootUri) url.searchParams.set('rootUri', descriptor.rootUri);
|
||
pushUrlState(url);
|
||
};
|
||
|
||
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) => {
|
||
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;
|
||
}
|
||
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);
|
||
};
|
||
|
||
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');
|
||
}
|
||
if (resourceTabs) resourceTabs.closeResourceTabsForPane('secondary');
|
||
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)) {
|
||
if (session.sourceKind === 'local_folder' && session.sessionKind !== 'resource') {
|
||
session.externalChangePending = true;
|
||
if (session.dirty) queueSessionSave(session);
|
||
return;
|
||
}
|
||
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');
|
||
}
|
||
if (session.pageBodySource) runtimeDescriptor.root.setAttribute('data-mnote-page-body-source', session.pageBodySource);
|
||
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);
|
||
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);
|
||
enhanceEditorAttachmentLinksSoon();
|
||
applyDocumentEvidenceLocatorFromUrl(runtimeDescriptor.root);
|
||
};
|
||
view.onError = (event) => {
|
||
const payload = normalizeEnvelopePayload(event);
|
||
setStatus(runtimeDescriptor, 'error', payload?.message || 'runtime_error');
|
||
};
|
||
view.onChange = (event) => {
|
||
scheduleSlashMenuPosition(runtimeDescriptor.root);
|
||
handleSessionChange(session, view, event);
|
||
enhanceEditorAttachmentLinksSoon();
|
||
};
|
||
view.onSave = (event) => {
|
||
handleSessionChange(session, view, event);
|
||
enhanceEditorAttachmentLinksSoon();
|
||
};
|
||
view.onState = () => {
|
||
scheduleSlashMenuPosition(runtimeDescriptor.root);
|
||
enhanceEditorAttachmentLinksSoon();
|
||
};
|
||
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;
|
||
};
|
||
|
||
resourceTabs = createResourceTabRuntime({
|
||
loadRuntime,
|
||
createEditorViewBinding,
|
||
ensureLocalFolderEventChannel,
|
||
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,
|
||
});
|
||
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);
|
||
|
||
const mountPane = async (runtimeDescriptor) => {
|
||
const runtime = await loadRuntime();
|
||
const session = getOrCreateDocumentSession(runtimeDescriptor);
|
||
const view = createEditorViewBinding(session, runtime, runtimeDescriptor);
|
||
syncDocumentEditModeChrome(session);
|
||
|
||
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');
|
||
enhanceEditorAttachmentLinksSoon();
|
||
paneViewRegistry.set(runtimeDescriptor.paneRole, view);
|
||
syncDocumentEditModeChrome(session);
|
||
} catch (error) {
|
||
unmountEditorViewBinding(view);
|
||
throw error;
|
||
}
|
||
};
|
||
|
||
window.__mnoteDocumentPaneRuntime = {
|
||
openPrimaryDocument: async ({ documentId, workspaceId, sourceKind, rootUri, url, pageBlockNavigation } = {}) => {
|
||
const id = typeof documentId === 'string' ? documentId.trim() : '';
|
||
if (!id) return false;
|
||
const descriptor = descriptorFromCurrentUrl('primary', id, { workspaceId, sourceKind, rootUri });
|
||
descriptor.pageBlockNavigation = pageBlockNavigation === true;
|
||
await replacePaneDocument('primary', descriptor);
|
||
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;
|
||
await mindmapHost.replacePrimaryPaneMindmap({
|
||
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 });
|
||
await replacePaneDocument('secondary', descriptor);
|
||
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;
|
||
},
|
||
};
|
||
installPageBlockOpenCapture();
|
||
|
||
bindMainEditorPageTab('primary');
|
||
bindMainEditorPageTab('secondary');
|
||
|
||
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);
|
||
}
|
||
}
|
||
|
||
window.addEventListener('pagehide', () => {
|
||
Array.from(documentSessionRegistry.values()).forEach((session) => {
|
||
sessionViews(session).forEach((view) => {
|
||
unmountEditorViewBinding(view, { scheduleRelease: false });
|
||
});
|
||
releaseDocumentSession(session);
|
||
});
|
||
}, { once: true });
|
||
|
||
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;
|
||
});
|
||
};
|
||
|
||
if (paneRuntimes.length) {
|
||
Promise.all(paneRuntimes.map((paneRuntime) => mountPane(paneRuntime))).then(() => {
|
||
return restoreResourceTabFromUrl();
|
||
}).catch((error) => {
|
||
console.error('mnote multi-pane editor mount failed', error);
|
||
});
|
||
} else {
|
||
void restoreResourceTabFromUrl();
|
||
}
|
||
})();
|