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 48ddd87e..6dc3852d 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 @@ -123,7 +123,7 @@ cargo test --manifest-path rust/Cargo.toml -p mnote-web --lib routes::tests::mno - [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。 -- [ ] B5. `document-slash-position-runtime.js`:slash menu active root、positioning、mutation observer。 +- [x] B5. `document-slash-position-runtime.js`:slash menu active root、positioning、mutation observer。 - [ ] B6. entrypoint 只负责读取 bootstrap、加载 runtime、装配各子模块。 验收标准: 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 c33fb446..0ce3d0af 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,9 @@ +import { + installGlobalSlashMenuPositioning, + markIntendedSlashRoot, + observeSlashMenuPosition, + scheduleSlashMenuPosition, +} from './document-slash-position-runtime.js'; import { buildPaneRuntime, clearSecondaryParams, @@ -1950,225 +1956,8 @@ import { view.disconnectObserver = () => observer.disconnect(); }; - const activeEditorRootForSlashMenu = () => { - const selection = window.getSelection(); - const anchorNode = selection?.anchorNode || null; - const anchorElement = anchorNode && anchorNode.nodeType === Node.ELEMENT_NODE - ? anchorNode - : anchorNode?.parentElement || null; - const roots = Array.from(document.querySelectorAll(ROOT_SELECTOR)).filter((node) => ( - node instanceof HTMLElement - && node.offsetParent !== null - && node.getAttribute('data-mnote-side-target-unsupported') !== 'true' - )); - const intendedRoot = window.__mnoteIntendedSlashRoot instanceof HTMLElement - && roots.includes(window.__mnoteIntendedSlashRoot) - ? window.__mnoteIntendedSlashRoot - : null; - if (intendedRoot) return intendedRoot; - const focused = document.activeElement instanceof Element - ? roots.find((root) => root.contains(document.activeElement)) - : null; - if (focused) return focused; - if (anchorElement instanceof Element) { - const activeRoot = roots.find((root) => root.contains(anchorElement)); - if (activeRoot) return activeRoot; - } - return roots.find((root) => root.querySelector('.ProseMirror:focus-within')) || roots[0] || null; - }; - - const markIntendedSlashRoot = (entry) => { - const root = entry?.view?.runtimeDescriptor?.root; - if (!(root instanceof HTMLElement) || !root.isConnected) return; - window.__mnoteIntendedSlashRoot = root; - hideSlashMenusOutsideRoot(root); - scheduleSlashMenuPosition(root); - }; - - const slashMenuAnchorFromSelection = (root) => { - const currentBlockFromSelection = () => { - try { - const selection = window.getSelection(); - const anchorNode = selection?.anchorNode || null; - const editor = root.querySelector('.ProseMirror'); - const anchorElement = anchorNode && anchorNode.nodeType === Node.ELEMENT_NODE - ? anchorNode - : anchorNode?.parentElement || null; - if (!(editor instanceof HTMLElement) || !(anchorElement instanceof Element) || !editor.contains(anchorElement)) return null; - let current = anchorElement; - while (current && current.parentElement && current.parentElement !== editor) { - current = current.parentElement; - } - return current instanceof HTMLElement && current.parentElement === editor ? current : null; - } catch (_) { - return null; - } - }; - const fallback = () => { - const editor = root.querySelector('.ProseMirror'); - if (editor instanceof HTMLElement) { - const block = currentBlockFromSelection() - || Array.from(editor.children).find((node) => node instanceof HTMLElement && node.matches(':focus-within, .ProseMirror-selectednode')) - || Array.from(editor.children).find((node) => node instanceof HTMLElement && node.getBoundingClientRect().height > 0); - if (block instanceof HTMLElement) { - const rect = block.getBoundingClientRect(); - return { top: rect.bottom + 8, left: rect.left, anchorTop: rect.top }; - } - } - const rect = root.getBoundingClientRect(); - return { top: rect.top + 24, left: rect.left + 24, anchorTop: rect.top + 24 }; - }; - try { - const selection = window.getSelection(); - if (!selection || selection.rangeCount <= 0) return fallback(); - const anchorNode = selection.anchorNode; - if (anchorNode && !root.contains(anchorNode.nodeType === Node.ELEMENT_NODE ? anchorNode : anchorNode.parentElement)) { - return fallback(); - } - const rect = selection.getRangeAt(0).getBoundingClientRect(); - if (rect && (rect.top > 0 || rect.left > 0 || rect.height > 0)) { - return { top: rect.bottom + 8, left: rect.left, anchorTop: rect.top }; - } - } catch (_) {} - return fallback(); - }; - - const setSlashMenuInactive = (menu, inactive) => { - if (!(menu instanceof HTMLElement)) return; - if (inactive) { - if (menu.getAttribute('data-mnote-slash-inactive') !== 'true') { - menu.setAttribute('data-mnote-slash-inactive', 'true'); - } - if (menu.style.display !== 'none') menu.style.display = 'none'; - return; - } - if (menu.getAttribute('data-mnote-slash-inactive') === 'true') { - menu.removeAttribute('data-mnote-slash-inactive'); - } - if (menu.style.display === 'none') menu.style.display = ''; - }; - - const setSlashMenuStyle = (menu, property, value) => { - if (!(menu instanceof HTMLElement)) return; - if (menu.style[property] !== value) menu.style[property] = value; - }; - - const setSlashMenuAttribute = (menu, name, value) => { - if (!(menu instanceof HTMLElement)) return; - if (menu.getAttribute(name) !== value) menu.setAttribute(name, value); - }; - - const hideSlashMenusOutsideRoot = (activeRoot) => { - document.querySelectorAll(`${ROOT_SELECTOR} [data-testid="mnote-leptos-tiptap-slash-menu"]`).forEach((menu) => { - const root = menu.closest(ROOT_SELECTOR); - if (root !== activeRoot) setSlashMenuInactive(menu, true); - }); - }; - - const positionSlashMenuForRoot = (root) => { - const activeRoot = activeEditorRootForSlashMenu(); - if (!(root instanceof HTMLElement)) root = activeRoot; - if (!(root instanceof HTMLElement)) return; - if (activeRoot instanceof HTMLElement && root !== activeRoot) { - const inactiveMenu = root.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]'); - setSlashMenuInactive(inactiveMenu, true); - hideSlashMenusOutsideRoot(activeRoot); - return; - } - hideSlashMenusOutsideRoot(root); - const menu = root.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]'); - if (!(menu instanceof HTMLElement)) return; - setSlashMenuInactive(menu, false); - const anchor = slashMenuAnchorFromSelection(root); - const gap = 8; - const menuWidth = Math.min(316, Math.max(160, window.innerWidth - 16)); - const menuHeight = Math.min(430, Math.max(120, window.innerHeight - 16)); - const left = Math.min(Math.max(gap, anchor.left), Math.max(gap, window.innerWidth - menuWidth - gap)); - const belowTop = Math.min(Math.max(gap, anchor.top), Math.max(gap, window.innerHeight - 120)); - let top = belowTop + menuHeight > window.innerHeight - gap - ? Math.min(Math.max(gap, anchor.anchorTop - menuHeight - gap), Math.max(gap, window.innerHeight - 120)) - : belowTop; - const mindmap = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); - if (mindmap instanceof HTMLElement) { - const mindmapRect = mindmap.getBoundingClientRect(); - const overlapsMindmap = top < mindmapRect.bottom && top + menuHeight > mindmapRect.top; - if (overlapsMindmap && mindmapRect.bottom > window.innerHeight - 48) { - top = Math.max(top, Math.max(gap, window.innerHeight - menuHeight - gap)); - } else if (overlapsMindmap && mindmapRect.bottom + menuHeight + gap <= window.innerHeight) { - top = mindmapRect.bottom + gap; - } - } - setSlashMenuStyle(menu, 'position', 'fixed'); - setSlashMenuStyle(menu, 'zIndex', '130'); - setSlashMenuStyle(menu, 'left', `${Math.round(left)}px`); - setSlashMenuStyle(menu, 'top', `${Math.round(top)}px`); - setSlashMenuStyle(menu, 'width', `min(316px, calc(100vw - 16px))`); - setSlashMenuStyle(menu, 'maxHeight', `min(430px, calc(100vh - 16px))`); - setSlashMenuAttribute(menu, 'data-mnote-slash-positioned', 'host'); - }; - - let pendingSlashMenuPositionFrame = 0; - let pendingSlashMenuPositionRoot = null; - const scheduleSlashMenuPosition = (root) => { - if (root instanceof HTMLElement) pendingSlashMenuPositionRoot = root; - if (pendingSlashMenuPositionFrame) return; - pendingSlashMenuPositionFrame = window.requestAnimationFrame(() => { - const targetRoot = pendingSlashMenuPositionRoot; - pendingSlashMenuPositionFrame = 0; - pendingSlashMenuPositionRoot = null; - positionSlashMenuForRoot(targetRoot); - }); - }; - - const shouldReactToSlashMenuMutation = (records, root) => { - if (!Array.isArray(records) || !(root instanceof HTMLElement)) return false; - return records.some((record) => { - const target = record && record.target instanceof HTMLElement ? record.target : null; - if (!target || !root.contains(target) && target !== root) return false; - if (record.type === 'attributes') { - return record.attributeName !== 'style' && record.attributeName !== 'data-mnote-slash-positioned' && record.attributeName !== 'data-mnote-slash-inactive'; - } - if (record.type === 'childList') { - return Array.from(record.addedNodes || []).some((node) => node instanceof HTMLElement && node.closest('[data-testid="mnote-leptos-tiptap-slash-menu"]')) - || Array.from(record.removedNodes || []).some((node) => node instanceof HTMLElement && node.closest('[data-testid="mnote-leptos-tiptap-slash-menu"]')); - } - return true; - }); - }; - - const scheduleGlobalSlashMenuPosition = () => { - scheduleSlashMenuPosition(activeEditorRootForSlashMenu()); - }; - - const installGlobalSlashMenuPositioning = () => { - if (window.__MNOTE_SLASH_MENU_POSITIONING_INSTALLED__) return; - window.__MNOTE_SLASH_MENU_POSITIONING_INSTALLED__ = true; - if (typeof MutationObserver === 'function' && document.body instanceof HTMLElement) { - const observer = new MutationObserver(() => scheduleGlobalSlashMenuPosition()); - observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden'] }); - } - document.addEventListener('keydown', (event) => { - if (event.key === '/') scheduleGlobalSlashMenuPosition(); - }, true); - document.addEventListener('selectionchange', () => scheduleGlobalSlashMenuPosition(), true); - window.addEventListener('resize', scheduleGlobalSlashMenuPosition); - window.addEventListener('scroll', scheduleGlobalSlashMenuPosition, true); - }; - installGlobalSlashMenuPositioning(); - const observeSlashMenuPosition = (view) => { - const root = view.runtimeDescriptor.root; - if (!(root instanceof HTMLElement) || typeof MutationObserver !== 'function') return; - const observer = new MutationObserver((records) => { - if (!view.disposed && shouldReactToSlashMenuMutation(records, root)) { - scheduleSlashMenuPosition(root); - } - }); - observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'hidden', 'class', 'data-mnote-slash-positioned', 'data-mnote-slash-inactive'] }); - view.disconnectSlashObserver = () => observer.disconnect(); - }; - const createEditorViewBinding = (session, runtime, runtimeDescriptor) => { cancelDocumentSessionRelease(session); const view = { diff --git a/rust/crates/mnote-web/browser/document-slash-position-runtime.js b/rust/crates/mnote-web/browser/document-slash-position-runtime.js new file mode 100644 index 00000000..7bea46f0 --- /dev/null +++ b/rust/crates/mnote-web/browser/document-slash-position-runtime.js @@ -0,0 +1,218 @@ +const ROOT_SELECTOR = '[data-testid="mnote-leptos-tiptap-island-editor-root"]'; + +export const activeEditorRootForSlashMenu = () => { + const selection = window.getSelection(); + const anchorNode = selection?.anchorNode || null; + const anchorElement = anchorNode && anchorNode.nodeType === Node.ELEMENT_NODE + ? anchorNode + : anchorNode?.parentElement || null; + const roots = Array.from(document.querySelectorAll(ROOT_SELECTOR)).filter((node) => ( + node instanceof HTMLElement + && node.offsetParent !== null + && node.getAttribute('data-mnote-side-target-unsupported') !== 'true' + )); + const intendedRoot = window.__mnoteIntendedSlashRoot instanceof HTMLElement + && roots.includes(window.__mnoteIntendedSlashRoot) + ? window.__mnoteIntendedSlashRoot + : null; + if (intendedRoot) return intendedRoot; + const focused = document.activeElement instanceof Element + ? roots.find((root) => root.contains(document.activeElement)) + : null; + if (focused) return focused; + if (anchorElement instanceof Element) { + const activeRoot = roots.find((root) => root.contains(anchorElement)); + if (activeRoot) return activeRoot; + } + return roots.find((root) => root.querySelector('.ProseMirror:focus-within')) || roots[0] || null; +}; + +export const markIntendedSlashRoot = (entry) => { + const root = entry?.view?.runtimeDescriptor?.root; + if (!(root instanceof HTMLElement) || !root.isConnected) return; + window.__mnoteIntendedSlashRoot = root; + hideSlashMenusOutsideRoot(root); + scheduleSlashMenuPosition(root); +}; + +export const slashMenuAnchorFromSelection = (root) => { + const currentBlockFromSelection = () => { + try { + const selection = window.getSelection(); + const anchorNode = selection?.anchorNode || null; + const editor = root.querySelector('.ProseMirror'); + const anchorElement = anchorNode && anchorNode.nodeType === Node.ELEMENT_NODE + ? anchorNode + : anchorNode?.parentElement || null; + if (!(editor instanceof HTMLElement) || !(anchorElement instanceof Element) || !editor.contains(anchorElement)) return null; + let current = anchorElement; + while (current && current.parentElement && current.parentElement !== editor) { + current = current.parentElement; + } + return current instanceof HTMLElement && current.parentElement === editor ? current : null; + } catch (_) { + return null; + } + }; + const fallback = () => { + const editor = root.querySelector('.ProseMirror'); + if (editor instanceof HTMLElement) { + const block = currentBlockFromSelection() + || Array.from(editor.children).find((node) => node instanceof HTMLElement && node.matches(':focus-within, .ProseMirror-selectednode')) + || Array.from(editor.children).find((node) => node instanceof HTMLElement && node.getBoundingClientRect().height > 0); + if (block instanceof HTMLElement) { + const rect = block.getBoundingClientRect(); + return { top: rect.bottom + 8, left: rect.left, anchorTop: rect.top }; + } + } + const rect = root.getBoundingClientRect(); + return { top: rect.top + 24, left: rect.left + 24, anchorTop: rect.top + 24 }; + }; + try { + const selection = window.getSelection(); + if (!selection || selection.rangeCount <= 0) return fallback(); + const anchorNode = selection.anchorNode; + if (anchorNode && !root.contains(anchorNode.nodeType === Node.ELEMENT_NODE ? anchorNode : anchorNode.parentElement)) { + return fallback(); + } + const rect = selection.getRangeAt(0).getBoundingClientRect(); + if (rect && (rect.top > 0 || rect.left > 0 || rect.height > 0)) { + return { top: rect.bottom + 8, left: rect.left, anchorTop: rect.top }; + } + } catch (_) {} + return fallback(); +}; + +export const setSlashMenuInactive = (menu, inactive) => { + if (!(menu instanceof HTMLElement)) return; + if (inactive) { + if (menu.getAttribute('data-mnote-slash-inactive') !== 'true') { + menu.setAttribute('data-mnote-slash-inactive', 'true'); + } + if (menu.style.display !== 'none') menu.style.display = 'none'; + return; + } + if (menu.getAttribute('data-mnote-slash-inactive') === 'true') { + menu.removeAttribute('data-mnote-slash-inactive'); + } + if (menu.style.display === 'none') menu.style.display = ''; +}; + +export const setSlashMenuStyle = (menu, property, value) => { + if (!(menu instanceof HTMLElement)) return; + if (menu.style[property] !== value) menu.style[property] = value; +}; + +export const setSlashMenuAttribute = (menu, name, value) => { + if (!(menu instanceof HTMLElement)) return; + if (menu.getAttribute(name) !== value) menu.setAttribute(name, value); +}; + +export const hideSlashMenusOutsideRoot = (activeRoot) => { + document.querySelectorAll(`${ROOT_SELECTOR} [data-testid="mnote-leptos-tiptap-slash-menu"]`).forEach((menu) => { + const root = menu.closest(ROOT_SELECTOR); + if (root !== activeRoot) setSlashMenuInactive(menu, true); + }); +}; + +export const positionSlashMenuForRoot = (root) => { + const activeRoot = activeEditorRootForSlashMenu(); + if (!(root instanceof HTMLElement)) root = activeRoot; + if (!(root instanceof HTMLElement)) return; + if (activeRoot instanceof HTMLElement && root !== activeRoot) { + const inactiveMenu = root.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]'); + setSlashMenuInactive(inactiveMenu, true); + hideSlashMenusOutsideRoot(activeRoot); + return; + } + hideSlashMenusOutsideRoot(root); + const menu = root.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]'); + if (!(menu instanceof HTMLElement)) return; + setSlashMenuInactive(menu, false); + const anchor = slashMenuAnchorFromSelection(root); + const gap = 8; + const menuWidth = Math.min(316, Math.max(160, window.innerWidth - 16)); + const menuHeight = Math.min(430, Math.max(120, window.innerHeight - 16)); + const left = Math.min(Math.max(gap, anchor.left), Math.max(gap, window.innerWidth - menuWidth - gap)); + const belowTop = Math.min(Math.max(gap, anchor.top), Math.max(gap, window.innerHeight - 120)); + let top = belowTop + menuHeight > window.innerHeight - gap + ? Math.min(Math.max(gap, anchor.anchorTop - menuHeight - gap), Math.max(gap, window.innerHeight - 120)) + : belowTop; + const mindmap = document.querySelector('[data-testid="mnote-mindmap-editor-root"]'); + if (mindmap instanceof HTMLElement) { + const mindmapRect = mindmap.getBoundingClientRect(); + const overlapsMindmap = top < mindmapRect.bottom && top + menuHeight > mindmapRect.top; + if (overlapsMindmap && mindmapRect.bottom > window.innerHeight - 48) { + top = Math.max(top, Math.max(gap, window.innerHeight - menuHeight - gap)); + } else if (overlapsMindmap && mindmapRect.bottom + menuHeight + gap <= window.innerHeight) { + top = mindmapRect.bottom + gap; + } + } + setSlashMenuStyle(menu, 'position', 'fixed'); + setSlashMenuStyle(menu, 'zIndex', '130'); + setSlashMenuStyle(menu, 'left', `${Math.round(left)}px`); + setSlashMenuStyle(menu, 'top', `${Math.round(top)}px`); + setSlashMenuStyle(menu, 'width', `min(316px, calc(100vw - 16px))`); + setSlashMenuStyle(menu, 'maxHeight', `min(430px, calc(100vh - 16px))`); + setSlashMenuAttribute(menu, 'data-mnote-slash-positioned', 'host'); +}; + +export let pendingSlashMenuPositionFrame = 0; +export let pendingSlashMenuPositionRoot = null; +export const scheduleSlashMenuPosition = (root) => { + if (root instanceof HTMLElement) pendingSlashMenuPositionRoot = root; + if (pendingSlashMenuPositionFrame) return; + pendingSlashMenuPositionFrame = window.requestAnimationFrame(() => { + const targetRoot = pendingSlashMenuPositionRoot; + pendingSlashMenuPositionFrame = 0; + pendingSlashMenuPositionRoot = null; + positionSlashMenuForRoot(targetRoot); + }); +}; + +export const shouldReactToSlashMenuMutation = (records, root) => { + if (!Array.isArray(records) || !(root instanceof HTMLElement)) return false; + return records.some((record) => { + const target = record && record.target instanceof HTMLElement ? record.target : null; + if (!target || !root.contains(target) && target !== root) return false; + if (record.type === 'attributes') { + return record.attributeName !== 'style' && record.attributeName !== 'data-mnote-slash-positioned' && record.attributeName !== 'data-mnote-slash-inactive'; + } + if (record.type === 'childList') { + return Array.from(record.addedNodes || []).some((node) => node instanceof HTMLElement && node.closest('[data-testid="mnote-leptos-tiptap-slash-menu"]')) + || Array.from(record.removedNodes || []).some((node) => node instanceof HTMLElement && node.closest('[data-testid="mnote-leptos-tiptap-slash-menu"]')); + } + return true; + }); +}; + +export const scheduleGlobalSlashMenuPosition = () => { + scheduleSlashMenuPosition(activeEditorRootForSlashMenu()); +}; + +export const installGlobalSlashMenuPositioning = () => { + if (window.__MNOTE_SLASH_MENU_POSITIONING_INSTALLED__) return; + window.__MNOTE_SLASH_MENU_POSITIONING_INSTALLED__ = true; + if (typeof MutationObserver === 'function' && document.body instanceof HTMLElement) { + const observer = new MutationObserver(() => scheduleGlobalSlashMenuPosition()); + observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class', 'hidden'] }); + } + document.addEventListener('keydown', (event) => { + if (event.key === '/') scheduleGlobalSlashMenuPosition(); + }, true); + document.addEventListener('selectionchange', () => scheduleGlobalSlashMenuPosition(), true); + window.addEventListener('resize', scheduleGlobalSlashMenuPosition); + window.addEventListener('scroll', scheduleGlobalSlashMenuPosition, true); +}; + +export const observeSlashMenuPosition = (view) => { + const root = view.runtimeDescriptor.root; + if (!(root instanceof HTMLElement) || typeof MutationObserver !== 'function') return; + const observer = new MutationObserver((records) => { + if (!view.disposed && shouldReactToSlashMenuMutation(records, root)) { + scheduleSlashMenuPosition(root); + } + }); + observer.observe(root, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'hidden', 'class', 'data-mnote-slash-positioned', 'data-mnote-slash-inactive'] }); + view.disconnectSlashObserver = () => observer.disconnect(); +}; diff --git a/rust/crates/mnote-web/src/routes/mod.rs b/rust/crates/mnote-web/src/routes/mod.rs index 700664de..130f4d39 100644 --- a/rust/crates/mnote-web/src/routes/mod.rs +++ b/rust/crates/mnote-web/src/routes/mod.rs @@ -146,6 +146,10 @@ pub fn build_router(state: AppState) -> Router { "/api/mnote-browser-runtime/document-pane-runtime.js", get(web_shell::document_pane_runtime_asset), ) + .route( + "/api/mnote-browser-runtime/document-slash-position-runtime.js", + get(web_shell::document_slash_position_runtime_asset), + ) .route( "/api/mnote-browser-runtime/document-tiptap-conversion-runtime.js", get(web_shell::document_tiptap_conversion_runtime_asset), @@ -568,6 +572,7 @@ mod tests { "/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-slash-position-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 b1c495a8..d2abeb5c 100644 --- a/rust/crates/mnote-web/src/routes/web_shell.rs +++ b/rust/crates/mnote-web/src/routes/web_shell.rs @@ -869,6 +869,20 @@ pub async fn document_pane_runtime_asset() -> Response { .unwrap_or_else(|_| Response::new(Body::empty())) } +pub async fn document_slash_position_runtime_asset() -> Response { + const JS: &str = include_str!("../../browser/document-slash-position-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() @@ -1307,6 +1321,8 @@ 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_SLASH_POSITION_RUNTIME_JS: &str = + include_str!("../../browser/document-slash-position-runtime.js"); const DOCUMENT_TIPTAP_CONVERSION_RUNTIME_JS: &str = include_str!("../../browser/document-tiptap-conversion-runtime.js"); @@ -1565,21 +1581,24 @@ mod tests { assert!(!html .contains("nodes.pageTab?.getAttribute?.('data-document-id') || currentDocumentId()")); assert!(runtime.contains("runtime.default({ module_or_path: wasmUrl })")); - assert!(runtime.contains("positionSlashMenuForRoot")); - assert!(runtime.contains("setSlashMenuStyle(menu, 'position', 'fixed');")); + let slash_runtime = DOCUMENT_SLASH_POSITION_RUNTIME_JS; + assert!(slash_runtime.contains("positionSlashMenuForRoot")); + assert!(slash_runtime.contains("setSlashMenuStyle(menu, 'position', 'fixed');")); assert!(runtime.contains("installGlobalSlashMenuPositioning();")); - assert!(runtime.contains("data-mnote-slash-positioned', 'host'")); - assert!(runtime.contains( + assert!(slash_runtime.contains("data-mnote-slash-positioned', 'host'")); + assert!(slash_runtime.contains( "const menu = root.querySelector('[data-testid=\"mnote-leptos-tiptap-slash-menu\"]');" )); - assert!(runtime.contains("const setSlashMenuInactive = (menu, inactive) =>")); - assert!(runtime.contains("const hideSlashMenusOutsideRoot = (activeRoot) =>")); - assert!(runtime.contains("data-mnote-slash-inactive")); - assert!(runtime.contains("if (activeRoot instanceof HTMLElement && root !== activeRoot)")); + assert!(slash_runtime.contains("const setSlashMenuInactive = (menu, inactive) =>")); + assert!(slash_runtime.contains("const hideSlashMenusOutsideRoot = (activeRoot) =>")); + assert!(slash_runtime.contains("data-mnote-slash-inactive")); + assert!( + slash_runtime.contains("if (activeRoot instanceof HTMLElement && root !== activeRoot)") + ); assert!(!runtime.contains( "|| document.querySelector('[data-testid=\"mnote-leptos-tiptap-slash-menu\"]')" )); - assert!(runtime.contains("data-mnote-side-target-unsupported') !== 'true'")); + assert!(slash_runtime.contains("data-mnote-side-target-unsupported') !== 'true'")); assert!( runtime.contains("runtimeDescriptor.root.addEventListener(STATE_EVENT, view.onState);") ); @@ -2059,6 +2078,9 @@ mod tests { 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-slash-position-runtime.js")); + assert!(DOCUMENT_SLASH_POSITION_RUNTIME_JS.contains("observeSlashMenuPosition")); + assert!(DOCUMENT_SLASH_POSITION_RUNTIME_JS.contains("scheduleSlashMenuPosition")); assert!( DOCUMENT_EDITOR_ADAPTER_RUNTIME_JS.contains("document-tiptap-conversion-runtime.js") );