Align resource tabs with workbench review

This commit is contained in:
lix-2026
2026-05-20 19:04:05 +08:00
parent 4c62ea7e35
commit acf1305a7e
14 changed files with 964 additions and 13 deletions
+72 -2
View File
@@ -1393,6 +1393,9 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
const paneViewRegistry = new Map();
const mindmapPaneViewRegistry = new Map();
const resourceTabRegistry = new Map();
const resourceTabMru = [];
const resourceTabMruMax = 20;
const resourceTabCloseGuardAttribute = 'data-resource-tab-close-guarded';
let nextViewId = 1;
const externalConflictMessage = '本地 Markdown 文件已在外部更新,请刷新或保存前先处理冲突';
const treeExternalConflictMessage = '当前页面已在其它窗口更新,请刷新或保存前先处理冲突';
@@ -2137,9 +2140,11 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
setSessionStatus(session, 'dirty');
queueSessionSave(session);
}
syncResourceSessionTabGuards(session);
} catch (error) {
session.saving = false;
setSessionStatus(session, 'error', error instanceof Error ? error.message : String(error));
syncResourceSessionTabGuards(session);
}
};
@@ -2868,6 +2873,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
}
}
session.dirty = session.currentSerialized !== session.lastPersistedSerialized;
syncResourceSessionTabGuards(session);
if (serialized !== previousSerialized) {
broadcastSessionContent(session, view);
}
@@ -3151,7 +3157,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
if (/\.(xls|xlsx|ods|csv)$/i.test(title)) return 'sheet';
return 'word';
}
if (kind === 'pdf') return 'ppt';
if (kind === 'pdf') return 'pdf';
if (kind === 'markdown' || kind === 'text' || kind === 'code') return 'code';
if (kind === 'image') return 'image';
return 'file';
@@ -3178,13 +3184,67 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
return 'file';
};
const touchResourceTabMru = (key) => {
const id = String(key || '').trim();
if (!id) return;
const index = resourceTabMru.indexOf(id);
if (index >= 0) resourceTabMru.splice(index, 1);
resourceTabMru.unshift(id);
if (resourceTabMru.length > resourceTabMruMax) resourceTabMru.length = resourceTabMruMax;
};
const removeFromResourceTabMru = (key) => {
const index = resourceTabMru.indexOf(key);
if (index >= 0) resourceTabMru.splice(index, 1);
};
const lastActiveResourceTabKey = () => {
for (const key of resourceTabMru) {
if (resourceTabRegistry.has(key)) return key;
}
return '';
};
const resourceTabCloseGuardReason = (session) => {
if (!session) return '';
const hasUnsavedChanges = session.dirty
|| Boolean(session.saveTimer)
|| sessionHasRecentLocalInput(session)
|| (session.currentSerialized && session.currentSerialized !== session.lastPersistedSerialized);
if (hasUnsavedChanges) return 'dirty';
if (session.saving) return 'saving';
if (session.hasExternalConflict) return 'hasExternalConflict';
return '';
};
const syncResourceTabCloseGuard = (entry) => {
if (!entry?.tab || !(entry.tab instanceof HTMLElement)) return;
const reason = resourceTabCloseGuardReason(entry.session);
if (reason) {
entry.tab.setAttribute(resourceTabCloseGuardAttribute, reason);
entry.tab.classList.add('is-close-guarded');
} else {
entry.tab.removeAttribute(resourceTabCloseGuardAttribute);
entry.tab.classList.remove('is-close-guarded');
}
};
const syncResourceSessionTabGuards = (session) => {
if (!session || session.sessionKind !== 'resource') return;
resourceTabRegistry.forEach((entry) => {
if (entry.session === session) syncResourceTabCloseGuard(entry);
});
};
const activateMainEditorTab = (objectIdentity) => {
const nodes = resourceTabHostNodes();
const activeResource = String(objectIdentity || '').trim();
if (activeResource) touchResourceTabMru(activeResource);
if (nodes.pageTab instanceof HTMLElement) {
const activePage = !activeResource;
nodes.pageTab.classList.toggle('is-active', activePage);
nodes.pageTab.setAttribute('aria-selected', activePage ? 'true' : 'false');
nodes.pageTab.setAttribute('tabindex', activePage ? '0' : '-1');
}
if (nodes.pagePanel instanceof HTMLElement) nodes.pagePanel.hidden = Boolean(activeResource);
if (nodes.host instanceof HTMLElement) nodes.host.hidden = !activeResource;
@@ -3193,8 +3253,10 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
if (entry.tab instanceof HTMLElement) {
entry.tab.classList.toggle('is-active', active);
entry.tab.setAttribute('aria-selected', active ? 'true' : 'false');
entry.tab.setAttribute('tabindex', active ? '0' : '-1');
}
if (entry.panel instanceof HTMLElement) entry.panel.hidden = !active;
syncResourceTabCloseGuard(entry);
});
};
@@ -3213,11 +3275,18 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
const key = String(objectIdentity || '').trim();
const entry = resourceTabRegistry.get(key);
if (!entry) return;
const guardReason = resourceTabCloseGuardReason(entry.session);
if (guardReason) {
console.warn(`mnote resource tab 关闭阻止: ${entry.title} (${guardReason})`);
syncResourceTabCloseGuard(entry);
return;
}
removeFromResourceTabMru(key);
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('');
activateMainEditorTab(lastActiveResourceTabKey());
};
const createResourceTabDom = (input) => {
@@ -3233,6 +3302,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
tab.setAttribute('data-mnote-main-tab', objectIdentity);
tab.setAttribute('data-mnote-tab-kind', kind);
tab.setAttribute('data-mnote-tab-badge-kind', resourceTabBadgeKind(input, kind));
tab.setAttribute('tabindex', '-1');
tab.innerHTML = '<span class="mnote-main-tab-badge" aria-hidden="true"></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;