From 6197ca5d3af79d8519e41429176c545f87c1b23a Mon Sep 17 00:00:00 2001 From: lix-2026 Date: Tue, 26 May 2026 00:47:51 +0800 Subject: [PATCH] refactor: split document pane runtime --- ...ime-module-maintainability-checklist-v1.md | 2 +- .../document-editor-adapter-runtime.js | 162 ++---------------- .../browser/document-pane-runtime.js | 158 +++++++++++++++++ rust/crates/mnote-web/src/routes/mod.rs | 5 + rust/crates/mnote-web/src/routes/web_shell.rs | 18 ++ 5 files changed, 199 insertions(+), 146 deletions(-) create mode 100644 rust/crates/mnote-web/browser/document-pane-runtime.js diff --git a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md index 70a74b3b..48ddd87e 100644 --- a/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md +++ b/design/10-review/process/16-mnote-web-runtime-module-maintainability-checklist-v1.md @@ -119,7 +119,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::tests::mno 拆分项: -- [ ] B1. `document-pane-runtime.js`:secondary pane URL、pane resize、pane close、pane runtime registry。 +- [x] B1. `document-pane-runtime.js`:secondary pane URL、pane resize、pane close、pane runtime registry。 - [x] B2. `document-tiptap-conversion-runtime.js`:legacy block / inline content / marks 转 Tiptap document。 - [ ] B3. `document-resource-tab-runtime.js`:resource tab registry、MRU、close guard、resource text/image/frame editor mount。 - [ ] B4. `document-mindmap-host-runtime.js`:primary mindmap object shell、mindmap resource tab mount/unmount。 diff --git a/rust/crates/mnote-web/browser/document-editor-adapter-runtime.js b/rust/crates/mnote-web/browser/document-editor-adapter-runtime.js index ee9b02be..c33fb446 100644 --- a/rust/crates/mnote-web/browser/document-editor-adapter-runtime.js +++ b/rust/crates/mnote-web/browser/document-editor-adapter-runtime.js @@ -1,3 +1,14 @@ +import { + buildPaneRuntime, + clearSecondaryParams, + createSecondaryPaneWidthController, + currentUrl, + installSecondaryPaneCloseButtons, + installSecondaryPaneOpenListeners, + pushUrlState, + replaceUrlState, + secondaryQueryParamNames, +} from './document-pane-runtime.js'; import { conflictDetectionKeyBelongsToSession, conflictDetectionKeyFromBody, @@ -37,154 +48,15 @@ import { const panesBootstrap = parseJsonScript(PANES_BOOTSTRAP_ID); if (!panesBootstrap || !Array.isArray(panesBootstrap.panes)) return; - const paneRouteConfig = { - primary: { - sourceKindParam: 'sourceKind', - rootUriParam: 'rootUri', - secondary: false, - }, - secondary: { - documentIdParam: 'secondaryDocumentId', - sourceKindParam: 'secondarySourceKind', - rootUriParam: 'secondaryRootUri', - secondary: true, - }, - }; - const secondaryQueryParamNames = [ - paneRouteConfig.secondary.documentIdParam, - paneRouteConfig.secondary.sourceKindParam, - paneRouteConfig.secondary.rootUriParam, - ]; - const currentUrl = () => new URL(window.location.href); - const replaceUrlState = (url) => { - window.history.replaceState({}, '', url.pathname + url.search + url.hash); - }; - const pushUrlState = (url) => { - window.history.pushState({}, '', url.pathname + url.search + url.hash); - }; - const clearSecondaryParams = () => { - const url = currentUrl(); - secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name)); - replaceUrlState(url); - }; if (panesBootstrap.secondaryInvalid === true) clearSecondaryParams(); - - const secondaryUrlForDocument = (documentId, detail = {}) => { - const url = currentUrl(); - url.searchParams.set(paneRouteConfig.secondary.documentIdParam, documentId); - const detailSourceKind = typeof detail.sourceKind === 'string' ? detail.sourceKind.trim() : ''; - const detailRootUri = typeof detail.rootUri === 'string' ? detail.rootUri.trim() : ''; - const primarySourceKind = (url.searchParams.get(paneRouteConfig.primary.sourceKindParam) || '').trim(); - const primaryRootUri = (url.searchParams.get(paneRouteConfig.primary.rootUriParam) || '').trim(); - const secondarySourceKind = detailSourceKind || primarySourceKind; - const secondaryRootUri = detailRootUri || primaryRootUri; - if (secondarySourceKind) url.searchParams.set(paneRouteConfig.secondary.sourceKindParam, secondarySourceKind); - else url.searchParams.delete(paneRouteConfig.secondary.sourceKindParam); - if (secondaryRootUri) url.searchParams.set(paneRouteConfig.secondary.rootUriParam, secondaryRootUri); - else url.searchParams.delete(paneRouteConfig.secondary.rootUriParam); - return { url, sourceKind: secondarySourceKind, rootUri: secondaryRootUri }; - }; - - const openDocumentInSecondaryPane = (documentId, detail = {}) => { - const id = typeof documentId === 'string' ? documentId.trim() : ''; - if (!id) return false; - const target = secondaryUrlForDocument(id, detail); - if (typeof window.__mnoteDocumentPaneRuntime?.openSecondaryDocument === 'function') { - void window.__mnoteDocumentPaneRuntime.openSecondaryDocument({ - documentId: id, - workspaceId: typeof detail.workspaceId === 'string' ? detail.workspaceId.trim() : '', - sourceKind: target.sourceKind || null, - rootUri: target.rootUri || null, - url: target.url, - }); - return true; - } - window.location.assign(target.url.pathname + target.url.search + target.url.hash); - return true; - }; - - window.addEventListener('tree.page.open-right', (event) => { - const detail = event?.detail && typeof event.detail === 'object' ? event.detail : {}; - const documentId = typeof detail.documentId === 'string' ? detail.documentId.trim() : ''; - openDocumentInSecondaryPane(documentId, detail); - }); - - window.addEventListener('tree.page.open', (event) => { - const detail = event?.detail && typeof event.detail === 'object' ? event.detail : {}; - const openTarget = typeof detail.openTarget === 'string' ? detail.openTarget.trim().toLowerCase() : ''; - if (openTarget !== 'side') return; - const documentId = typeof detail.documentId === 'string' ? detail.documentId.trim() : ''; - openDocumentInSecondaryPane(documentId, detail); - }); - - document.querySelectorAll('[data-mnote-pane-close="secondary"]').forEach((button) => { - button.addEventListener('click', (event) => { - event.preventDefault(); - const url = currentUrl(); - secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name)); - if (typeof window.__mnoteDocumentPaneRuntime?.closeSecondaryDocument === 'function') { - window.__mnoteDocumentPaneRuntime.closeSecondaryDocument({ url }); - return; - } - window.location.assign(url.pathname + url.search + url.hash); - }); - }); - - const workspace = document.querySelector('[data-testid="mnote-document-workspace"]'); - const resizer = document.querySelector('[data-document-pane-resizer="true"]'); - const SECONDARY_WIDTH_KEY = 'mnote.document.secondary.width'; - const applyStoredSecondaryWidth = () => { - if (!(workspace instanceof HTMLElement)) return; - if (workspace.getAttribute('data-has-secondary-pane') !== 'true') { - workspace.style.removeProperty('grid-template-columns'); - return; - } - try { - const raw = window.localStorage ? window.localStorage.getItem(SECONDARY_WIDTH_KEY) : ''; - const width = Number(raw || 0); - if (Number.isFinite(width) && width >= 320) { - workspace.style.gridTemplateColumns = `minmax(0, 1fr) 6px minmax(320px, ${Math.round(width)}px)`; - } - } catch (_) {} - }; + installSecondaryPaneOpenListeners(); + installSecondaryPaneCloseButtons(); + const { applyStoredSecondaryWidth } = createSecondaryPaneWidthController(); applyStoredSecondaryWidth(); - if (workspace instanceof HTMLElement && resizer instanceof HTMLElement) { - resizer.addEventListener('pointerdown', (event) => { - if (workspace.getAttribute('data-has-secondary-pane') !== 'true') return; - event.preventDefault(); - const move = (nextEvent) => { - const rect = workspace.getBoundingClientRect(); - const width = Math.min(Math.max(320, rect.right - nextEvent.clientX), Math.max(420, rect.width - 360)); - workspace.style.gridTemplateColumns = `minmax(0, 1fr) 6px minmax(320px, ${Math.round(width)}px)`; - try { - if (window.localStorage) window.localStorage.setItem(SECONDARY_WIDTH_KEY, String(Math.round(width))); - } catch (_) {} - }; - const up = () => { - window.removeEventListener('pointermove', move); - window.removeEventListener('pointerup', up); - }; - window.addEventListener('pointermove', move); - window.addEventListener('pointerup', up); - }); - } - const buildPaneRuntime = (paneDescriptor) => { - const paneRole = typeof paneDescriptor?.role === 'string' ? paneDescriptor.role : 'primary'; - const paneRoot = document.querySelector(`${ROOT_SELECTOR}[data-pane-role="${paneRole}"]`); - const observability = document.querySelector(`[data-editor-host-observability][data-pane-role="${paneRole}"]`); - if (!(paneRoot instanceof HTMLElement)) return null; - if (!paneDescriptor?.aggregate || !paneDescriptor?.bootstrap) return null; - return { - paneRole, - root: paneRoot, - observability, - aggregate: paneDescriptor.aggregate, - bootstrap: paneDescriptor.bootstrap, - }; - }; - - const paneRuntimes = panesBootstrap.panes.map(buildPaneRuntime).filter(Boolean); + const paneRuntimes = panesBootstrap.panes + .map((paneDescriptor) => buildPaneRuntime(paneDescriptor, ROOT_SELECTOR)) + .filter(Boolean); const loadRuntime = async () => { if (window.__mnoteLeptosTiptapRuntimePromise) { diff --git a/rust/crates/mnote-web/browser/document-pane-runtime.js b/rust/crates/mnote-web/browser/document-pane-runtime.js new file mode 100644 index 00000000..331eaf22 --- /dev/null +++ b/rust/crates/mnote-web/browser/document-pane-runtime.js @@ -0,0 +1,158 @@ +export const paneRouteConfig = { + primary: { + sourceKindParam: 'sourceKind', + rootUriParam: 'rootUri', + secondary: false, + }, + secondary: { + documentIdParam: 'secondaryDocumentId', + sourceKindParam: 'secondarySourceKind', + rootUriParam: 'secondaryRootUri', + secondary: true, + }, +}; + +export const secondaryQueryParamNames = [ + paneRouteConfig.secondary.documentIdParam, + paneRouteConfig.secondary.sourceKindParam, + paneRouteConfig.secondary.rootUriParam, +]; + +export const currentUrl = () => new URL(window.location.href); + +export const replaceUrlState = (url) => { + window.history.replaceState({}, '', url.pathname + url.search + url.hash); +}; + +export const pushUrlState = (url) => { + window.history.pushState({}, '', url.pathname + url.search + url.hash); +}; + +export const clearSecondaryParams = () => { + const url = currentUrl(); + secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name)); + replaceUrlState(url); +}; + +export const secondaryUrlForDocument = (documentId, detail = {}) => { + const url = currentUrl(); + url.searchParams.set(paneRouteConfig.secondary.documentIdParam, documentId); + const detailSourceKind = typeof detail.sourceKind === 'string' ? detail.sourceKind.trim() : ''; + const detailRootUri = typeof detail.rootUri === 'string' ? detail.rootUri.trim() : ''; + const primarySourceKind = (url.searchParams.get(paneRouteConfig.primary.sourceKindParam) || '').trim(); + const primaryRootUri = (url.searchParams.get(paneRouteConfig.primary.rootUriParam) || '').trim(); + const secondarySourceKind = detailSourceKind || primarySourceKind; + const secondaryRootUri = detailRootUri || primaryRootUri; + if (secondarySourceKind) url.searchParams.set(paneRouteConfig.secondary.sourceKindParam, secondarySourceKind); + else url.searchParams.delete(paneRouteConfig.secondary.sourceKindParam); + if (secondaryRootUri) url.searchParams.set(paneRouteConfig.secondary.rootUriParam, secondaryRootUri); + else url.searchParams.delete(paneRouteConfig.secondary.rootUriParam); + return { url, sourceKind: secondarySourceKind, rootUri: secondaryRootUri }; +}; + +export const openDocumentInSecondaryPane = (documentId, detail = {}) => { + const id = typeof documentId === 'string' ? documentId.trim() : ''; + if (!id) return false; + const target = secondaryUrlForDocument(id, detail); + if (typeof window.__mnoteDocumentPaneRuntime?.openSecondaryDocument === 'function') { + void window.__mnoteDocumentPaneRuntime.openSecondaryDocument({ + documentId: id, + workspaceId: typeof detail.workspaceId === 'string' ? detail.workspaceId.trim() : '', + sourceKind: target.sourceKind || null, + rootUri: target.rootUri || null, + url: target.url, + }); + return true; + } + window.location.assign(target.url.pathname + target.url.search + target.url.hash); + return true; +}; + +export const installSecondaryPaneOpenListeners = () => { + window.addEventListener('tree.page.open-right', (event) => { + const detail = event?.detail && typeof event.detail === 'object' ? event.detail : {}; + const documentId = typeof detail.documentId === 'string' ? detail.documentId.trim() : ''; + openDocumentInSecondaryPane(documentId, detail); + }); + + window.addEventListener('tree.page.open', (event) => { + const detail = event?.detail && typeof event.detail === 'object' ? event.detail : {}; + const openTarget = typeof detail.openTarget === 'string' ? detail.openTarget.trim().toLowerCase() : ''; + if (openTarget !== 'side') return; + const documentId = typeof detail.documentId === 'string' ? detail.documentId.trim() : ''; + openDocumentInSecondaryPane(documentId, detail); + }); +}; + +export const installSecondaryPaneCloseButtons = () => { + document.querySelectorAll('[data-mnote-pane-close="secondary"]').forEach((button) => { + button.addEventListener('click', (event) => { + event.preventDefault(); + const url = currentUrl(); + secondaryQueryParamNames.forEach((name) => url.searchParams.delete(name)); + if (typeof window.__mnoteDocumentPaneRuntime?.closeSecondaryDocument === 'function') { + window.__mnoteDocumentPaneRuntime.closeSecondaryDocument({ url }); + return; + } + window.location.assign(url.pathname + url.search + url.hash); + }); + }); +}; + +export const createSecondaryPaneWidthController = () => { + const workspace = document.querySelector('[data-testid="mnote-document-workspace"]'); + const resizer = document.querySelector('[data-document-pane-resizer="true"]'); + const secondaryWidthKey = 'mnote.document.secondary.width'; + const applyStoredSecondaryWidth = () => { + if (!(workspace instanceof HTMLElement)) return; + if (workspace.getAttribute('data-has-secondary-pane') !== 'true') { + workspace.style.removeProperty('grid-template-columns'); + return; + } + try { + const raw = window.localStorage ? window.localStorage.getItem(secondaryWidthKey) : ''; + const width = Number(raw || 0); + if (Number.isFinite(width) && width >= 320) { + workspace.style.gridTemplateColumns = `minmax(0, 1fr) 6px minmax(320px, ${Math.round(width)}px)`; + } + } catch (_) {} + }; + + if (workspace instanceof HTMLElement && resizer instanceof HTMLElement) { + resizer.addEventListener('pointerdown', (event) => { + if (workspace.getAttribute('data-has-secondary-pane') !== 'true') return; + event.preventDefault(); + const move = (nextEvent) => { + const rect = workspace.getBoundingClientRect(); + const width = Math.min(Math.max(320, rect.right - nextEvent.clientX), Math.max(420, rect.width - 360)); + workspace.style.gridTemplateColumns = `minmax(0, 1fr) 6px minmax(320px, ${Math.round(width)}px)`; + try { + if (window.localStorage) window.localStorage.setItem(secondaryWidthKey, String(Math.round(width))); + } catch (_) {} + }; + const up = () => { + window.removeEventListener('pointermove', move); + window.removeEventListener('pointerup', up); + }; + window.addEventListener('pointermove', move); + window.addEventListener('pointerup', up); + }); + } + + return { applyStoredSecondaryWidth }; +}; + +export const buildPaneRuntime = (paneDescriptor, rootSelector) => { + const paneRole = typeof paneDescriptor?.role === 'string' ? paneDescriptor.role : 'primary'; + const paneRoot = document.querySelector(`${rootSelector}[data-pane-role="${paneRole}"]`); + const observability = document.querySelector(`[data-editor-host-observability][data-pane-role="${paneRole}"]`); + if (!(paneRoot instanceof HTMLElement)) return null; + if (!paneDescriptor?.aggregate || !paneDescriptor?.bootstrap) return null; + return { + paneRole, + root: paneRoot, + observability, + aggregate: paneDescriptor.aggregate, + bootstrap: paneDescriptor.bootstrap, + }; +}; diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index 7ef22ccb..700664de 100644 --- a/rust/crates/mnote-web/src/routes/mod.rs +++ b/rust/crates/mnote-web/src/routes/mod.rs @@ -142,6 +142,10 @@ pub fn build_router(state: AppState) -> Router { "/api/mnote-browser-runtime/document-conflict-panel-runtime.js", get(web_shell::document_conflict_panel_runtime_asset), ) + .route( + "/api/mnote-browser-runtime/document-pane-runtime.js", + get(web_shell::document_pane_runtime_asset), + ) .route( "/api/mnote-browser-runtime/document-tiptap-conversion-runtime.js", get(web_shell::document_tiptap_conversion_runtime_asset), @@ -563,6 +567,7 @@ mod tests { "/api/mnote-browser-runtime/tree-live-controller.js", "/api/mnote-browser-runtime/tree-shell-runtime.js", "/api/mnote-browser-runtime/document-conflict-panel-runtime.js", + "/api/mnote-browser-runtime/document-pane-runtime.js", "/api/mnote-browser-runtime/document-tiptap-conversion-runtime.js", "/api/mnote-browser-runtime/document-editor-adapter-runtime.js", ] { diff --git a/rust/crates/mnote-web/src/routes/web_shell.rs b/rust/crates/mnote-web/src/routes/web_shell.rs index a0cc95d2..b1c495a8 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -855,6 +855,20 @@ pub async fn document_conflict_panel_runtime_asset() -> Response { .unwrap_or_else(|_| Response::new(Body::empty())) } +pub async fn document_pane_runtime_asset() -> Response { + const JS: &str = include_str!("../../browser/document-pane-runtime.js"); + Response::builder() + .status(StatusCode::OK) + .header( + header::CONTENT_TYPE, + "application/javascript; charset=utf-8", + ) + .header(header::CACHE_CONTROL, "no-store") + .header(HEADER_MNOTE_WEB_OWNER, "mnote-web") + .body(Body::from(JS)) + .unwrap_or_else(|_| Response::new(Body::empty())) +} + pub async fn document_tiptap_conversion_runtime_asset() -> Response { const JS: &str = include_str!("../../browser/document-tiptap-conversion-runtime.js"); Response::builder() @@ -1292,6 +1306,7 @@ mod tests { const DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS: &str = include_str!("../../browser/document-editor-adapter-runtime.js"); + const DOCUMENT_PANE_RUNTIME_JS: &str = include_str!("../../browser/document-pane-runtime.js"); const DOCUMENT_TIPTAP_CONVERSION_RUNTIME_JS: &str = include_str!("../../browser/document-tiptap-conversion-runtime.js"); @@ -2041,6 +2056,9 @@ mod tests { assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("PANES_BOOTSTRAP_ID")); assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("ROOT_SELECTOR")); assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("BRIDGE_PROTOCOL")); + assert!(DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("document-pane-runtime.js")); + assert!(DOCUMENT_PANE_RUNTIME_JS.contains("openDocumentInSecondaryPane")); + assert!(DOCUMENT_PANE_RUNTIME_JS.contains("buildPaneRuntime")); assert!( DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("document-tiptap-conversion-runtime.js") );