align workbench open editors tabs

This commit is contained in:
lix-2026
2026-05-21 05:57:19 +08:00
parent eba1010191
commit fd94792ea6
12 changed files with 897 additions and 2 deletions
+106 -2
View File
@@ -3343,6 +3343,62 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
});
};
const openEditorsSnapshotEntry = (entry, key) => ({
objectIdentity: String(entry?.objectIdentity || key || '').trim(),
title: String(entry?.title || '资源').trim() || '资源',
kind: normalizeResourceTabKind(entry),
badgeKind: entry?.tab instanceof HTMLElement
? String(entry.tab.getAttribute('data-mnote-tab-badge-kind') || resourceTabBadgeKind(entry, entry.kind)).trim()
: resourceTabBadgeKind(entry, entry?.kind),
active: entry?.tab instanceof HTMLElement
? entry.tab.getAttribute('aria-selected') === 'true'
: false,
dirtyGuard: resourceTabCloseGuardReason(entry?.session),
assetId: String(entry?.assetId || entry?.session?.assetId || '').trim(),
path: String(entry?.path || entry?.session?.resourcePath || '').trim(),
});
const buildOpenEditorsSnapshot = () => {
const nodes = resourceTabHostNodes();
const pageTitle = nodes.pageTab instanceof HTMLElement
? String(nodes.pageTab.querySelector('.mnote-main-tab-title')?.textContent || '页面').trim()
: '页面';
const pageEntry = {
objectIdentity: 'page',
documentId: String(nodes.pageTab?.getAttribute?.('data-document-id') || currentDocumentId() || '').trim(),
workspaceId: String(nodes.pageTab?.getAttribute?.('data-workspace-id') || currentWebShellWorkspaceId() || '').trim(),
title: pageTitle || '页面',
kind: 'page',
badgeKind: 'code',
active: nodes.pageTab instanceof HTMLElement
? nodes.pageTab.getAttribute('aria-selected') === 'true'
: false,
dirtyGuard: '',
};
const resources = [];
resourceTabRegistry.forEach((entry, key) => {
resources.push(openEditorsSnapshotEntry(entry, key));
});
return {
schema: 'mnote.open_editors_snapshot.v1',
generatedAt: Date.now(),
activeObjectIdentity: pageEntry.active
? 'page'
: (resources.find((entry) => entry.active)?.objectIdentity || ''),
editors: [pageEntry, ...resources],
resourceEditors: resources,
};
};
const syncOpenEditorsSnapshot = () => {
const snapshot = buildOpenEditorsSnapshot();
window.__mnoteOpenEditorsSnapshot = snapshot;
document.documentElement.setAttribute('data-mnote-open-editors-count', String(snapshot.editors.length));
document.documentElement.setAttribute('data-mnote-active-editor', snapshot.activeObjectIdentity || '');
window.dispatchEvent(new CustomEvent('mnote:open-editors-snapshot', { detail: snapshot }));
return snapshot;
};
const showResourceTabCloseGuardNotice = (entry, reason) => {
const nodes = resourceTabHostNodes();
const messages = {
@@ -3445,17 +3501,56 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
});
syncActiveResourceFileTreeRow(activeResource);
syncActiveResourceUrlState(activeResource);
syncOpenEditorsSnapshot();
};
const bindMainEditorTabStrip = () => {
const nodes = resourceTabHostNodes();
if (!(nodes.strip instanceof HTMLElement)) return;
if (nodes.strip.getAttribute('data-mnote-tab-strip-bound') === 'true') return;
nodes.strip.setAttribute('data-mnote-tab-strip-bound', 'true');
const collectTabs = () => Array.from(nodes.strip.querySelectorAll('[data-mnote-main-tab]'))
.filter((tab) => tab instanceof HTMLElement && tab.isConnected);
const selectedIndex = (tabs) => tabs.findIndex((tab) => tab.getAttribute('aria-selected') === 'true');
nodes.strip.addEventListener('keydown', (event) => {
const tabs = collectTabs();
if (!tabs.length) return;
const focusedIndex = tabs.findIndex((tab) => tab === document.activeElement);
const baseIndex = focusedIndex >= 0 ? focusedIndex : Math.max(0, selectedIndex(tabs));
let targetIndex = -1;
if (event.key === 'ArrowRight') {
targetIndex = (baseIndex + 1) % tabs.length;
} else if (event.key === 'ArrowLeft') {
targetIndex = (baseIndex - 1 + tabs.length) % tabs.length;
} else if (event.key === 'Home') {
targetIndex = 0;
} else if (event.key === 'End') {
targetIndex = tabs.length - 1;
} else if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
const tab = tabs[baseIndex];
if (tab instanceof HTMLElement) tab.click();
return;
} else {
return;
}
event.preventDefault();
const target = tabs[targetIndex];
if (target instanceof HTMLElement) target.focus();
});
};
const bindMainEditorPageTab = () => {
const nodes = resourceTabHostNodes();
if (!(nodes.pageTab instanceof HTMLElement)) return;
bindMainEditorTabStrip();
if (nodes.pageTab.getAttribute('data-mnote-page-tab-bound') === 'true') return;
nodes.pageTab.setAttribute('data-mnote-page-tab-bound', 'true');
nodes.pageTab.addEventListener('click', (event) => {
event.preventDefault();
activateMainEditorTab('');
});
syncOpenEditorsSnapshot();
};
const closeResourceTab = (objectIdentity) => {
@@ -3481,7 +3576,10 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
if (entry.tab instanceof HTMLElement) entry.tab.remove();
if (entry.panel instanceof HTMLElement) entry.panel.remove();
resourceTabRegistry.delete(key);
activateMainEditorTab(lastActiveResourceTabKey());
const nextKey = lastActiveResourceTabKey();
activateMainEditorTab(nextKey);
const nextTab = nextKey ? resourceTabRegistry.get(nextKey)?.tab : resourceTabHostNodes().pageTab;
if (nextTab instanceof HTMLElement) nextTab.focus();
};
const markResourceTabError = (entry) => {
@@ -3861,6 +3959,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
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)
@@ -4542,6 +4641,10 @@ mod tests {
assert!(html.contains("pageTab.setAttribute('data-document-id', documentId);"));
assert!(html.contains("data-mnote-tab-badge-kind"));
assert!(html.contains("resourceTabBadgeKind(input, kind)"));
assert!(html.contains("mnote.open_editors_snapshot.v1"));
assert!(html.contains("getOpenEditorsSnapshot"));
assert!(html.contains("bindMainEditorTabStrip"));
assert!(html.contains("data-mnote-tab-strip-bound"));
assert!(html.contains("positionSlashMenuForRoot"));
assert!(html.contains("menu.style.position = 'fixed';"));
assert!(html.contains("installGlobalSlashMenuPositioning();"));
@@ -4592,7 +4695,8 @@ mod tests {
assert!(html.contains("resourceTabCloseGuardReason"));
assert!(html.contains("closeResourceTab"));
assert!(html.contains("resourceTabRegistry.delete(key)"));
assert!(html.contains("activateMainEditorTab(lastActiveResourceTabKey())"));
assert!(html.contains("const nextKey = lastActiveResourceTabKey();"));
assert!(html.contains("if (nextTab instanceof HTMLElement) nextTab.focus();"));
assert!(html.contains("resourceTabBadgeKind(input, kind)"));
assert!(html.contains("const refreshExistingOfficeResourceTab = (entry, input) =>"));
assert!(html.contains("if (!entry || entry.kind !== 'office') return false;"));