fix: repair resource tab and slash menu regressions
This commit is contained in:
@@ -613,6 +613,7 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
const CHANGE_EVENT = `${EVENT_PREFIX}:change`;
|
||||
const SAVE_EVENT = `${EVENT_PREFIX}:save-request`;
|
||||
const READY_EVENT = `${EVENT_PREFIX}:ready`;
|
||||
const STATE_EVENT = `${EVENT_PREFIX}:state`;
|
||||
const ERROR_EVENT = `${EVENT_PREFIX}:error`;
|
||||
const COMMAND_EVENT = `${EVENT_PREFIX}:command`;
|
||||
const BRIDGE_PROTOCOL = 'mnote.leptos_tiptap.bridge.v1';
|
||||
@@ -2898,7 +2899,13 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
if (view.onError) root.removeEventListener(ERROR_EVENT, view.onError);
|
||||
if (view.onChange) root.removeEventListener(CHANGE_EVENT, view.onChange);
|
||||
if (view.onSave) root.removeEventListener(SAVE_EVENT, view.onSave);
|
||||
if (view.onState) root.removeEventListener(STATE_EVENT, view.onState);
|
||||
if (view.onKeydown) root.removeEventListener('keydown', view.onKeydown, true);
|
||||
if (view.onInput) root.removeEventListener('input', view.onInput);
|
||||
if (view.disconnectSlashObserver) {
|
||||
view.disconnectSlashObserver();
|
||||
view.disconnectSlashObserver = null;
|
||||
}
|
||||
if (view.mountId != null && typeof view.runtime?.unmount === 'function') {
|
||||
try {
|
||||
view.runtime.unmount(view.mountId);
|
||||
@@ -2929,6 +2936,135 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
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);
|
||||
if (anchorElement instanceof Element) {
|
||||
const activeRoot = roots.find((root) => root.contains(anchorElement));
|
||||
if (activeRoot) return activeRoot;
|
||||
}
|
||||
return roots.find((root) => root.offsetParent !== null) || roots[0] || null;
|
||||
};
|
||||
|
||||
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 positionSlashMenuForRoot = (root) => {
|
||||
if (!(root instanceof HTMLElement)) root = activeEditorRootForSlashMenu();
|
||||
if (!(root instanceof HTMLElement)) return;
|
||||
const menu = root.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]')
|
||||
|| document.querySelector('[data-testid="mnote-leptos-tiptap-slash-menu"]');
|
||||
if (!(menu instanceof HTMLElement)) return;
|
||||
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;
|
||||
}
|
||||
}
|
||||
menu.style.position = 'fixed';
|
||||
menu.style.zIndex = '130';
|
||||
menu.style.left = `${Math.round(left)}px`;
|
||||
menu.style.top = `${Math.round(top)}px`;
|
||||
menu.style.width = `min(316px, calc(100vw - 16px))`;
|
||||
menu.style.maxHeight = `min(430px, calc(100vh - 16px))`;
|
||||
menu.setAttribute('data-mnote-slash-positioned', 'host');
|
||||
};
|
||||
|
||||
const scheduleSlashMenuPosition = (root) => {
|
||||
window.requestAnimationFrame(() => positionSlashMenuForRoot(root));
|
||||
};
|
||||
|
||||
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(() => scheduleSlashMenuPosition(root));
|
||||
observer.observe(document.body, { childList: true, subtree: true });
|
||||
view.disconnectSlashObserver = () => observer.disconnect();
|
||||
};
|
||||
|
||||
const createEditorViewBinding = (session, runtime, runtimeDescriptor) => {
|
||||
cancelDocumentSessionRelease(session);
|
||||
const view = {
|
||||
@@ -2946,7 +3082,10 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
onError: null,
|
||||
onChange: null,
|
||||
onSave: null,
|
||||
onState: null,
|
||||
onKeydown: null,
|
||||
onInput: null,
|
||||
disconnectSlashObserver: null,
|
||||
};
|
||||
|
||||
view.onReady = () => {
|
||||
@@ -2965,23 +3104,34 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
setStatus(runtimeDescriptor, 'error', payload?.message || 'runtime_error');
|
||||
};
|
||||
view.onChange = (event) => {
|
||||
scheduleSlashMenuPosition(runtimeDescriptor.root);
|
||||
handleSessionChange(session, view, event);
|
||||
};
|
||||
view.onSave = (event) => {
|
||||
handleSessionChange(session, view, event);
|
||||
};
|
||||
view.onState = () => {
|
||||
scheduleSlashMenuPosition(runtimeDescriptor.root);
|
||||
};
|
||||
view.onKeydown = (event) => {
|
||||
if (event.key === '/') scheduleSlashMenuPosition(runtimeDescriptor.root);
|
||||
};
|
||||
view.onInput = (event) => {
|
||||
const target = event.target;
|
||||
if (!(target instanceof Element) || !target.closest('.ProseMirror')) return;
|
||||
session.lastUserInputAt = Date.now();
|
||||
scheduleSlashMenuPosition(runtimeDescriptor.root);
|
||||
};
|
||||
|
||||
runtimeDescriptor.root.addEventListener(READY_EVENT, view.onReady);
|
||||
runtimeDescriptor.root.addEventListener(ERROR_EVENT, view.onError);
|
||||
runtimeDescriptor.root.addEventListener(CHANGE_EVENT, view.onChange);
|
||||
runtimeDescriptor.root.addEventListener(SAVE_EVENT, view.onSave);
|
||||
runtimeDescriptor.root.addEventListener(STATE_EVENT, view.onState);
|
||||
runtimeDescriptor.root.addEventListener('keydown', view.onKeydown, true);
|
||||
runtimeDescriptor.root.addEventListener('input', view.onInput);
|
||||
session.views.set(view.id, view);
|
||||
observeSlashMenuPosition(view);
|
||||
observeEditorViewBinding(view);
|
||||
return view;
|
||||
};
|
||||
@@ -2994,13 +3144,17 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
panelRoot: document.querySelector('[data-mnote-resource-tab-panel-root]'),
|
||||
});
|
||||
|
||||
const resourceIconForKind = (kind) => {
|
||||
if (kind === 'office') return 'article';
|
||||
if (kind === 'pdf') return 'picture_as_pdf';
|
||||
const resourceTabBadgeKind = (input, kind) => {
|
||||
const title = String(input?.title || input?.fileName || input?.path || '').trim().toLowerCase();
|
||||
if (kind === 'office') {
|
||||
if (/\.(ppt|pptx|odp)$/i.test(title)) return 'ppt';
|
||||
if (/\.(xls|xlsx|ods|csv)$/i.test(title)) return 'sheet';
|
||||
return 'word';
|
||||
}
|
||||
if (kind === 'pdf') return 'ppt';
|
||||
if (kind === 'markdown' || kind === 'text' || kind === 'code') return 'code';
|
||||
if (kind === 'image') return 'image';
|
||||
if (kind === 'markdown') return 'notes';
|
||||
if (kind === 'code') return 'code';
|
||||
return 'draft';
|
||||
return 'file';
|
||||
};
|
||||
|
||||
const currentWebShellWorkspaceId = () => {
|
||||
@@ -3078,7 +3232,8 @@ pub(crate) fn render_editor_island_adapter_script() -> &'static str {
|
||||
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>';
|
||||
tab.setAttribute('data-mnote-tab-badge-kind', resourceTabBadgeKind(input, kind));
|
||||
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;
|
||||
tab.addEventListener('click', (event) => {
|
||||
@@ -3985,6 +4140,17 @@ mod tests {
|
||||
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("class=\"mnote-main-tab-badge\""));
|
||||
assert!(!html.contains(">description</span><span class=\"mnote-main-tab-title\""));
|
||||
assert!(html.contains("data-mnote-tab-badge-kind"));
|
||||
assert!(html.contains("resourceTabBadgeKind(input, kind)"));
|
||||
assert!(html.contains("positionSlashMenuForRoot"));
|
||||
assert!(html.contains("menu.style.position = 'fixed';"));
|
||||
assert!(html.contains("installGlobalSlashMenuPositioning();"));
|
||||
assert!(html.contains("data-mnote-slash-positioned', 'host'"));
|
||||
assert!(
|
||||
html.contains("runtimeDescriptor.root.addEventListener(STATE_EVENT, view.onState);")
|
||||
);
|
||||
assert!(html.contains("data-mnote-resource-tab-host"));
|
||||
assert!(html.contains("openPrimaryMindmap"));
|
||||
assert!(html.contains("openResourceInActiveTab"));
|
||||
|
||||
Reference in New Issue
Block a user