feat: add main editor resource tabs
This commit is contained in:
@@ -1391,6 +1391,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
const localFolderEventRegistry = new Map();
|
||||
const paneViewRegistry = new Map();
|
||||
const mindmapPaneViewRegistry = new Map();
|
||||
const resourceTabRegistry = new Map();
|
||||
let nextViewId = 1;
|
||||
const externalConflictMessage = '本地 Markdown 文件已在外部更新,请刷新或保存前先处理冲突';
|
||||
const treeExternalConflictMessage = '当前页面已在其它窗口更新,请刷新或保存前先处理冲突';
|
||||
@@ -1675,11 +1676,32 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
return nextAggregate;
|
||||
};
|
||||
|
||||
const fetchLatestResourceSnapshot = async (session) => {
|
||||
const url = new URL('/api/local-folder/resource/read', window.location.origin);
|
||||
url.searchParams.set('rootUri', session.rootUri || '');
|
||||
url.searchParams.set('path', session.resourcePath || '');
|
||||
const response = await fetch(url.toString(), {
|
||||
cache: 'no-store',
|
||||
headers: { accept: 'application/json' },
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok || !payload || payload.ok !== true) throw new Error('resource_conflict_latest_fetch_failed_' + response.status);
|
||||
const nextResource = payload.result;
|
||||
if (!nextResource || typeof nextResource !== 'object') throw new Error('resource_conflict_latest_missing_snapshot');
|
||||
return nextResource;
|
||||
};
|
||||
|
||||
const aggregatePlainText = (aggregate) => {
|
||||
const body = aggregate?.body || {};
|
||||
return flattenText(pageBodyTiptapDocument(body)).replace(/\n{3,}/g, '\n\n').trim();
|
||||
};
|
||||
|
||||
const resourceSnapshotPlainText = (snapshot) => (
|
||||
String(snapshot?.text || flattenText(toTiptapDocument(snapshot?.content, '')) || '')
|
||||
.replace(/\n{3,}/g, '\n\n')
|
||||
.trim()
|
||||
);
|
||||
|
||||
const shouldRetryTransientEmptyLocalAggregate = (session, nextAggregate) => {
|
||||
if (!session || session.sourceKind !== 'local_folder') return false;
|
||||
if (!sessionHasRecentExternalSignal(session)) return false;
|
||||
@@ -1746,6 +1768,29 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
setSessionStatus(session, 'synced-external-change');
|
||||
};
|
||||
|
||||
const applyResourceSnapshotToSession = (session, nextResource, source) => {
|
||||
const nextConflictKey = String(nextResource?.fileVersion || nextResource?.conflictDetectionKey || '').trim();
|
||||
const nextTiptapDocument = toTiptapDocument(nextResource?.content, nextResource?.text || '');
|
||||
const nextSerialized = JSON.stringify(nextTiptapDocument);
|
||||
session.currentTiptapDocument = nextTiptapDocument;
|
||||
session.currentSerialized = nextSerialized;
|
||||
session.lastPersistedSerialized = nextSerialized;
|
||||
if (nextConflictKey) {
|
||||
session.conflictDetectionKey = nextConflictKey;
|
||||
session.lastExternalConflictDetectionKey = nextConflictKey;
|
||||
session.fileVersion = nextConflictKey;
|
||||
}
|
||||
session.dirty = false;
|
||||
session.hasExternalConflict = false;
|
||||
session.externalChangePending = false;
|
||||
session.lastUserInputAt = 0;
|
||||
clearSessionConflictSurface(session);
|
||||
sessionViews(session).forEach((view) => {
|
||||
if (view.mountId != null) dispatchSessionContentToView(session, view, source || 'mnote-web-resource-conflict-resolved');
|
||||
});
|
||||
setSessionStatus(session, 'synced-external-change');
|
||||
};
|
||||
|
||||
const openConflictDiffPanel = async (session, panel) => {
|
||||
const diffPanel = panel.querySelector('[data-testid="mnote-conflict-diff-panel"]');
|
||||
if (!(diffPanel instanceof HTMLElement)) return;
|
||||
@@ -1756,14 +1801,19 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
loading.textContent = '正在读取磁盘版本...';
|
||||
diffPanel.appendChild(loading);
|
||||
try {
|
||||
const latest = await fetchLatestSessionAggregate(session);
|
||||
const latest = session.sessionKind === 'resource'
|
||||
? await fetchLatestResourceSnapshot(session)
|
||||
: await fetchLatestSessionAggregate(session);
|
||||
const latestText = session.sessionKind === 'resource'
|
||||
? resourceSnapshotPlainText(latest)
|
||||
: aggregatePlainText(latest);
|
||||
diffPanel.replaceChildren();
|
||||
const current = document.createElement('pre');
|
||||
current.setAttribute('data-testid', 'mnote-conflict-current-text');
|
||||
current.textContent = sessionPlainText(session) || '(当前编辑器为空)';
|
||||
const disk = document.createElement('pre');
|
||||
disk.setAttribute('data-testid', 'mnote-conflict-disk-text');
|
||||
disk.textContent = aggregatePlainText(latest) || '(磁盘版本为空)';
|
||||
disk.textContent = latestText || '(磁盘版本为空)';
|
||||
const currentTitle = document.createElement('h3');
|
||||
currentTitle.textContent = '当前编辑器版本';
|
||||
const diskTitle = document.createElement('h3');
|
||||
@@ -1776,7 +1826,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
mergeTitle.textContent = '合并结果';
|
||||
const mergeText = document.createElement('textarea');
|
||||
mergeText.setAttribute('data-testid', 'mnote-conflict-merge-text');
|
||||
mergeText.value = sessionPlainText(session) || aggregatePlainText(latest) || '';
|
||||
mergeText.value = sessionPlainText(session) || latestText || '';
|
||||
const mergeActions = document.createElement('div');
|
||||
mergeActions.className = 'mnote-conflict-actions';
|
||||
const useCurrent = document.createElement('button');
|
||||
@@ -1800,7 +1850,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
mergeText.value = sessionPlainText(session) || '';
|
||||
});
|
||||
useDisk.addEventListener('click', () => {
|
||||
mergeText.value = aggregatePlainText(latest) || '';
|
||||
mergeText.value = latestText || '';
|
||||
});
|
||||
saveMerge.addEventListener('click', () => {
|
||||
writeMergedConflictResult(session, panel, mergeText.value).catch((error) => {
|
||||
@@ -1816,13 +1866,20 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
|
||||
const acceptDiskVersion = async (session) => {
|
||||
setSessionStatus(session, 'conflict-resolving', '正在接受磁盘版本...');
|
||||
if (session.sessionKind === 'resource') {
|
||||
const latestResource = await fetchLatestResourceSnapshot(session);
|
||||
applyResourceSnapshotToSession(session, latestResource, 'mnote-web-resource-conflict-accept-disk');
|
||||
return;
|
||||
}
|
||||
const latest = await fetchLatestSessionAggregate(session);
|
||||
applyAggregateSnapshotToSession(session, latest, 'mnote-web-conflict-accept-disk');
|
||||
};
|
||||
|
||||
const keepCurrentEditorVersion = async (session) => {
|
||||
setSessionStatus(session, 'conflict-resolving', '正在保留当前编辑器版本...');
|
||||
const latest = await fetchLatestSessionAggregate(session);
|
||||
const latest = session.sessionKind === 'resource'
|
||||
? await fetchLatestResourceSnapshot(session)
|
||||
: await fetchLatestSessionAggregate(session);
|
||||
const hydrateView = sessionViews(session).find((item) => item.mountId != null) || sessionViews(session)[0];
|
||||
if (hydrateView) {
|
||||
const liveText = normalizePlainText(currentEditorText(hydrateView));
|
||||
@@ -1834,7 +1891,9 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
}
|
||||
session.currentSerialized = JSON.stringify(session.currentTiptapDocument);
|
||||
}
|
||||
const nextKey = conflictDetectionKeyFromBody(latest.body || {});
|
||||
const nextKey = session.sessionKind === 'resource'
|
||||
? String(latest?.fileVersion || latest?.conflictDetectionKey || '').trim()
|
||||
: conflictDetectionKeyFromBody(latest.body || {});
|
||||
if (nextKey) {
|
||||
session.conflictDetectionKey = nextKey;
|
||||
session.lastExternalConflictDetectionKey = nextKey;
|
||||
@@ -1849,8 +1908,12 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
|
||||
const writeMergedConflictResult = async (session, panel, mergedText) => {
|
||||
setSessionStatus(session, 'conflict-resolving', '正在写回合并结果...');
|
||||
const latest = await fetchLatestSessionAggregate(session);
|
||||
const nextKey = conflictDetectionKeyFromBody(latest.body || {});
|
||||
const latest = session.sessionKind === 'resource'
|
||||
? await fetchLatestResourceSnapshot(session)
|
||||
: await fetchLatestSessionAggregate(session);
|
||||
const nextKey = session.sessionKind === 'resource'
|
||||
? String(latest?.fileVersion || latest?.conflictDetectionKey || '').trim()
|
||||
: conflictDetectionKeyFromBody(latest.body || {});
|
||||
if (nextKey) {
|
||||
session.conflictDetectionKey = nextKey;
|
||||
session.lastExternalConflictDetectionKey = nextKey;
|
||||
@@ -1882,7 +1945,10 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
text.textContent = message || externalConflictMessage;
|
||||
const meta = document.createElement('div');
|
||||
meta.className = 'mnote-conflict-meta';
|
||||
meta.textContent = `文件:${session.rootUri || session.documentId} · 来源:${conflictSourceLabel(session) || '本地文件变更'}`;
|
||||
const fileLabel = session.sessionKind === 'resource'
|
||||
? `${session.rootUri || ''}/${session.resourcePath || session.documentId}`
|
||||
: (session.rootUri || session.documentId);
|
||||
meta.textContent = `文件:${fileLabel} · 来源:${conflictSourceLabel(session) || '本地文件变更'}`;
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'mnote-conflict-actions';
|
||||
const acceptDisk = document.createElement('button');
|
||||
@@ -1969,21 +2035,33 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
const editorDocument = editorDocumentFromTiptapDocument({ documentId: session.documentId }, session.currentTiptapDocument);
|
||||
const content = legacyBlocksFromEditorDocument(editorDocument);
|
||||
const saveEndpoint = session.saveEndpoint || '/api/documents/save';
|
||||
const savePayload = {
|
||||
documentId: session.documentId,
|
||||
workspaceId: session.workspaceId,
|
||||
sourceKind: session.sourceKind,
|
||||
rootUri: session.rootUri,
|
||||
revision: session.revision,
|
||||
expectedFileVersion: session.conflictDetectionKey,
|
||||
contentFormat: 'editorBlocks',
|
||||
editorSource: 'tiptap',
|
||||
editorDocument,
|
||||
content,
|
||||
tiptapDocument: session.currentTiptapDocument,
|
||||
blockCount: editorDocument.blocks.length,
|
||||
};
|
||||
if (session.sourceKind !== 'local_folder') {
|
||||
const savePayload = session.sessionKind === 'resource'
|
||||
? {
|
||||
rootUri: session.rootUri,
|
||||
path: session.resourcePath,
|
||||
expectedFileVersion: session.conflictDetectionKey,
|
||||
contentFormat: 'editorBlocks',
|
||||
editorSource: 'tiptap-resource-tab',
|
||||
editorDocument,
|
||||
content,
|
||||
tiptapDocument: session.currentTiptapDocument,
|
||||
blockCount: editorDocument.blocks.length,
|
||||
}
|
||||
: {
|
||||
documentId: session.documentId,
|
||||
workspaceId: session.workspaceId,
|
||||
sourceKind: session.sourceKind,
|
||||
rootUri: session.rootUri,
|
||||
revision: session.revision,
|
||||
expectedFileVersion: session.conflictDetectionKey,
|
||||
contentFormat: 'editorBlocks',
|
||||
editorSource: 'tiptap',
|
||||
editorDocument,
|
||||
content,
|
||||
tiptapDocument: session.currentTiptapDocument,
|
||||
blockCount: editorDocument.blocks.length,
|
||||
};
|
||||
if (session.sourceKind !== 'local_folder' && session.sessionKind !== 'resource') {
|
||||
savePayload.conflictDetectionKey = session.conflictDetectionKey;
|
||||
}
|
||||
const response = await fetch(saveEndpoint, {
|
||||
@@ -2021,7 +2099,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
syncSessionMetaToViews(session);
|
||||
session.lastPersistedSerialized = serialized;
|
||||
session.saving = false;
|
||||
if (typeof window.__mnoteRecordPageHistorySnapshot === 'function') {
|
||||
if (session.sessionKind !== 'resource' && typeof window.__mnoteRecordPageHistorySnapshot === 'function') {
|
||||
const primaryView = sessionViews(session).find((view) => view.runtimeDescriptor.paneRole === 'primary') || sessionViews(session)[0];
|
||||
if (primaryView) {
|
||||
const plainText = sessionPlainText(session);
|
||||
@@ -2887,6 +2965,252 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
return view;
|
||||
};
|
||||
|
||||
const resourceTabHostNodes = () => ({
|
||||
strip: document.querySelector('[data-mnote-main-tab-strip]'),
|
||||
pageTab: document.querySelector('[data-mnote-main-tab="page"]'),
|
||||
pagePanel: document.querySelector('[data-mnote-page-tab-panel]'),
|
||||
host: document.querySelector('[data-mnote-resource-tab-host]'),
|
||||
panelRoot: document.querySelector('[data-mnote-resource-tab-panel-root]'),
|
||||
});
|
||||
|
||||
const resourceIconForKind = (kind) => {
|
||||
if (kind === 'office') return 'article';
|
||||
if (kind === 'pdf') return 'picture_as_pdf';
|
||||
if (kind === 'image') return 'image';
|
||||
if (kind === 'markdown') return 'notes';
|
||||
if (kind === 'code') return 'code';
|
||||
return 'draft';
|
||||
};
|
||||
|
||||
const currentWebShellWorkspaceId = () => {
|
||||
try {
|
||||
return currentUrl().searchParams.get('workspaceId') || '';
|
||||
} catch (_) {
|
||||
return '';
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeResourceTabKind = (input) => {
|
||||
const kind = String(input?.kind || '').trim().toLowerCase();
|
||||
const title = String(input?.title || input?.fileName || input?.path || '').trim().toLowerCase();
|
||||
if (kind === 'office' || kind === 'pdf' || kind === 'image' || kind === 'markdown' || kind === 'text' || kind === 'code') return kind;
|
||||
if (/\.(doc|docx|ppt|pptx|xls|xlsx|odt|odp|ods)$/i.test(title)) return 'office';
|
||||
if (/\.pdf$/i.test(title)) return 'pdf';
|
||||
if (/\.(png|jpg|jpeg|gif|webp|svg)$/i.test(title)) return 'image';
|
||||
if (/\.(md|markdown)$/i.test(title)) return 'markdown';
|
||||
if (/\.(txt|log)$/i.test(title)) return 'text';
|
||||
if (/\.(rs|ts|tsx|js|jsx|json|css|scss|html|xml|py|go|java|kt|swift|c|h|cpp|hpp|sh|bash|zsh|toml|yaml|yml|sql)$/i.test(title)) return 'code';
|
||||
return 'file';
|
||||
};
|
||||
|
||||
const activateMainEditorTab = (objectIdentity) => {
|
||||
const nodes = resourceTabHostNodes();
|
||||
const activeResource = String(objectIdentity || '').trim();
|
||||
if (nodes.pageTab instanceof HTMLElement) {
|
||||
const activePage = !activeResource;
|
||||
nodes.pageTab.classList.toggle('is-active', activePage);
|
||||
nodes.pageTab.setAttribute('aria-selected', activePage ? 'true' : 'false');
|
||||
}
|
||||
if (nodes.pagePanel instanceof HTMLElement) nodes.pagePanel.hidden = Boolean(activeResource);
|
||||
if (nodes.host instanceof HTMLElement) nodes.host.hidden = !activeResource;
|
||||
resourceTabRegistry.forEach((entry, key) => {
|
||||
const active = key === activeResource;
|
||||
if (entry.tab instanceof HTMLElement) {
|
||||
entry.tab.classList.toggle('is-active', active);
|
||||
entry.tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
||||
}
|
||||
if (entry.panel instanceof HTMLElement) entry.panel.hidden = !active;
|
||||
});
|
||||
};
|
||||
|
||||
const closeResourceTab = (objectIdentity) => {
|
||||
const key = String(objectIdentity || '').trim();
|
||||
const entry = resourceTabRegistry.get(key);
|
||||
if (!entry) return;
|
||||
if (entry.view) unmountEditorViewBinding(entry.view, { releaseSession: true });
|
||||
if (entry.tab instanceof HTMLElement) entry.tab.remove();
|
||||
if (entry.panel instanceof HTMLElement) entry.panel.remove();
|
||||
resourceTabRegistry.delete(key);
|
||||
activateMainEditorTab('');
|
||||
};
|
||||
|
||||
const createResourceTabDom = (input) => {
|
||||
const nodes = resourceTabHostNodes();
|
||||
if (!(nodes.strip instanceof HTMLElement) || !(nodes.panelRoot instanceof HTMLElement)) return null;
|
||||
const objectIdentity = String(input.objectIdentity || '').trim();
|
||||
const title = String(input.title || input.fileName || input.path || '资源').trim() || '资源';
|
||||
const kind = normalizeResourceTabKind(input);
|
||||
const tab = document.createElement('button');
|
||||
tab.type = 'button';
|
||||
tab.className = 'mnote-main-tab';
|
||||
tab.setAttribute('role', 'tab');
|
||||
tab.setAttribute('data-mnote-main-tab', objectIdentity);
|
||||
tab.setAttribute('data-mnote-tab-kind', kind);
|
||||
tab.innerHTML = '<span class="material-symbols-outlined" aria-hidden="true">' + resourceIconForKind(kind) + '</span><span class="mnote-main-tab-title"></span><span class="mnote-main-tab-close" role="button" aria-label="关闭标签页">×</span>';
|
||||
const titleNode = tab.querySelector('.mnote-main-tab-title');
|
||||
if (titleNode) titleNode.textContent = title;
|
||||
tab.addEventListener('click', (event) => {
|
||||
const target = event.target;
|
||||
if (target instanceof HTMLElement && target.closest('.mnote-main-tab-close')) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closeResourceTab(objectIdentity);
|
||||
return;
|
||||
}
|
||||
activateMainEditorTab(objectIdentity);
|
||||
});
|
||||
const panel = document.createElement('section');
|
||||
panel.className = 'mnote-resource-tab-panel';
|
||||
panel.setAttribute('data-mnote-resource-tab-panel', objectIdentity);
|
||||
panel.setAttribute('data-resource-kind', kind);
|
||||
panel.hidden = true;
|
||||
nodes.strip.append(tab);
|
||||
nodes.panelRoot.append(panel);
|
||||
return { objectIdentity, title, kind, tab, panel, view: null, session: null };
|
||||
};
|
||||
|
||||
const localResourceReadUrl = (rootUri, path) => {
|
||||
const url = new URL('/api/local-folder/resource/read', window.location.origin);
|
||||
url.searchParams.set('rootUri', rootUri || '');
|
||||
url.searchParams.set('path', path || '');
|
||||
return url.toString();
|
||||
};
|
||||
|
||||
const createResourceSession = (entry, input, readResult) => {
|
||||
const tiptapDocument = toTiptapDocument(readResult?.content, readResult?.text || '');
|
||||
const conflictDetectionKey = String(readResult?.fileVersion || readResult?.conflictDetectionKey || '').trim();
|
||||
const session = {
|
||||
key: `resource:${input.rootUri || ''}:${input.path || entry.objectIdentity}`,
|
||||
sessionKind: 'resource',
|
||||
documentId: entry.objectIdentity,
|
||||
workspaceId: String(input.workspaceId || currentWebShellWorkspaceId() || ''),
|
||||
sourceKind: 'local_folder',
|
||||
rootUri: String(input.rootUri || ''),
|
||||
resourcePath: String(input.path || ''),
|
||||
saveEndpoint: '/api/local-folder/resource/write',
|
||||
pageAggregateScriptId: '',
|
||||
latestAggregate: null,
|
||||
title: entry.title,
|
||||
currentTiptapDocument: tiptapDocument,
|
||||
currentSerialized: JSON.stringify(tiptapDocument),
|
||||
lastPersistedSerialized: JSON.stringify(tiptapDocument),
|
||||
revision: null,
|
||||
conflictDetectionKey,
|
||||
fileVersion: conflictDetectionKey,
|
||||
lastExternalConflictDetectionKey: conflictDetectionKey,
|
||||
readOnly: false,
|
||||
dirty: false,
|
||||
saving: false,
|
||||
hasExternalConflict: false,
|
||||
externalChangePending: false,
|
||||
externalRefreshSource: '',
|
||||
lastExternalChangeSignalAt: 0,
|
||||
lastSelfSaveSignalAt: 0,
|
||||
lastExternalWriteSource: '',
|
||||
lastExternalWriteRunId: '',
|
||||
lastUserInputAt: 0,
|
||||
saveTimer: 0,
|
||||
externalRefreshTimer: 0,
|
||||
releaseTimer: 0,
|
||||
views: new Map(),
|
||||
localFolderChannel: null,
|
||||
status: 'ready',
|
||||
error: null,
|
||||
};
|
||||
documentSessionRegistry.set(session.key, session);
|
||||
return session;
|
||||
};
|
||||
|
||||
const openTiptapResourceTab = async (entry, input) => {
|
||||
const runtime = await loadRuntime();
|
||||
entry.panel.innerHTML = '<main class="document-shell mnote-resource-tab-text-shell" data-editor-host="leptos_tiptap_resource"><div class="mnote-resource-tab-editor-root" data-testid="mnote-leptos-tiptap-island-editor-root" data-editor-host-kind="leptos_tiptap_resource" data-runtime-editor-status="booting" data-pane-role="resource"></div><div class="sr-only" data-editor-host-observability="rust-web-resource-tab" data-pane-role="resource"></div></main>';
|
||||
const root = entry.panel.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
|
||||
const observability = entry.panel.querySelector('[data-editor-host-observability]');
|
||||
if (!(root instanceof HTMLElement)) throw new Error('resource_tab_root_missing');
|
||||
const response = await fetch(localResourceReadUrl(input.rootUri, input.path), { cache: 'no-store', headers: { accept: 'application/json' } });
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok || !payload || payload.ok !== true) throw new Error(payload?.error?.message || `resource_read_failed_${response.status}`);
|
||||
const readResult = payload.result || {};
|
||||
const runtimeDescriptor = {
|
||||
paneRole: 'resource',
|
||||
root,
|
||||
observability,
|
||||
aggregate: { layout: { pageOptions: {} } },
|
||||
bootstrap: {
|
||||
documentId: entry.objectIdentity,
|
||||
workspaceId: String(input.workspaceId || currentWebShellWorkspaceId() || ''),
|
||||
sourceKind: 'local_folder',
|
||||
rootUri: String(input.rootUri || ''),
|
||||
saveEndpoint: '/api/local-folder/resource/write',
|
||||
},
|
||||
};
|
||||
const session = createResourceSession(entry, input, readResult);
|
||||
const view = createEditorViewBinding(session, runtime, runtimeDescriptor);
|
||||
const mountId = runtime.mount(root, {
|
||||
documentId: session.documentId,
|
||||
workspaceId: session.workspaceId,
|
||||
title: session.title,
|
||||
content: session.currentTiptapDocument,
|
||||
revision: session.revision,
|
||||
conflictDetectionKey: session.conflictDetectionKey,
|
||||
readOnly: false,
|
||||
editable: true,
|
||||
pageOptions: {},
|
||||
});
|
||||
view.mountId = mountId;
|
||||
root.setAttribute('data-runtime-mount-id', String(mountId));
|
||||
root.setAttribute('data-editor-host-kind', 'leptos_tiptap_resource');
|
||||
setStatus(runtimeDescriptor, 'mounting-editor');
|
||||
entry.view = view;
|
||||
entry.session = session;
|
||||
};
|
||||
|
||||
const openPassiveResourceTab = (entry, input) => {
|
||||
const href = String(input.officeUrl || input.href || '').trim();
|
||||
if (entry.kind === 'image') {
|
||||
entry.panel.innerHTML = '<img class="mnote-resource-tab-image" alt="">';
|
||||
const img = entry.panel.querySelector('img');
|
||||
if (img instanceof HTMLImageElement) {
|
||||
img.src = href;
|
||||
img.alt = entry.title;
|
||||
}
|
||||
return;
|
||||
}
|
||||
entry.panel.innerHTML = '<iframe class="mnote-resource-tab-frame" title=""></iframe>';
|
||||
const frame = entry.panel.querySelector('iframe');
|
||||
if (frame instanceof HTMLIFrameElement) {
|
||||
frame.title = entry.title;
|
||||
frame.src = href;
|
||||
}
|
||||
};
|
||||
|
||||
const openResourceInActiveTab = async (input = {}) => {
|
||||
const objectIdentity = String(input.objectIdentity || input.assetId || input.href || '').trim();
|
||||
if (!objectIdentity) return false;
|
||||
const existing = resourceTabRegistry.get(objectIdentity);
|
||||
if (existing) {
|
||||
activateMainEditorTab(objectIdentity);
|
||||
return true;
|
||||
}
|
||||
const entry = createResourceTabDom({ ...input, objectIdentity });
|
||||
if (!entry) return false;
|
||||
resourceTabRegistry.set(objectIdentity, entry);
|
||||
activateMainEditorTab(objectIdentity);
|
||||
try {
|
||||
if (entry.kind === 'markdown' || entry.kind === 'text' || entry.kind === 'code') {
|
||||
await openTiptapResourceTab(entry, input);
|
||||
} else {
|
||||
openPassiveResourceTab(entry, input);
|
||||
}
|
||||
activateMainEditorTab(objectIdentity);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.warn('mnote resource tab 打开失败', error);
|
||||
entry.panel.innerHTML = '<div class="mnote-resource-tab-text-shell" data-resource-tab-error="true">资源打开失败</div>';
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const mountPane = async (runtimeDescriptor) => {
|
||||
const runtime = await loadRuntime();
|
||||
const session = getOrCreateDocumentSession(runtimeDescriptor);
|
||||
@@ -2948,6 +3272,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
if (url instanceof URL) replaceUrlState(url);
|
||||
return true;
|
||||
},
|
||||
openResourceInActiveTab: openResourceInActiveTab,
|
||||
closeSecondaryDocument: ({ url } = {}) => {
|
||||
closeSecondaryPane(url instanceof URL ? url : currentUrl());
|
||||
return true;
|
||||
@@ -3624,7 +3949,12 @@ mod tests {
|
||||
assert!(html.contains("data-document-pane=\"true\""));
|
||||
assert!(html.contains("data-pane-role=\"primary\""));
|
||||
assert!(html.contains("data-document-pane-resizer=\"true\""));
|
||||
assert!(html.contains("data-mnote-main-tab-strip"));
|
||||
assert!(html.contains("data-mnote-resource-tab-host"));
|
||||
assert!(html.contains("openPrimaryMindmap"));
|
||||
assert!(html.contains("openResourceInActiveTab"));
|
||||
assert!(html.contains("/api/local-folder/resource/read"));
|
||||
assert!(html.contains("/api/local-folder/resource/write"));
|
||||
assert!(html.contains("replacePrimaryPaneMindmap"));
|
||||
assert!(html.contains("__MNOTE_MINDMAP_EDITOR_BOOTSTRAP__"));
|
||||
assert!(html.contains("data-mnote-object-identity"));
|
||||
|
||||
Reference in New Issue
Block a user