From 0f081ffa2b66479a8a2207ac7dfc234cf134f8cd Mon Sep 17 00:00:00 2001 From: Caio Pizzol Date: Tue, 13 Jan 2026 11:55:19 -0300 Subject: [PATCH] fix: popover menu renders at correct position in presentation mode --- packages/super-editor/src/extensions/index.js | 1 + .../popover-plugin/popover-plugin.js | 84 +++++++++++++++++-- 2 files changed, 78 insertions(+), 7 deletions(-) diff --git a/packages/super-editor/src/extensions/index.js b/packages/super-editor/src/extensions/index.js index 83d76220f..9e860ad6e 100644 --- a/packages/super-editor/src/extensions/index.js +++ b/packages/super-editor/src/extensions/index.js @@ -153,6 +153,7 @@ const getStarterExtensions = () => { Image, BookmarkStart, BookmarkEnd, + PopoverPlugin, Mention, Collaboration, CollaborationCursor, diff --git a/packages/super-editor/src/extensions/popover-plugin/popover-plugin.js b/packages/super-editor/src/extensions/popover-plugin/popover-plugin.js index 02a249420..6dec9140d 100644 --- a/packages/super-editor/src/extensions/popover-plugin/popover-plugin.js +++ b/packages/super-editor/src/extensions/popover-plugin/popover-plugin.js @@ -127,11 +127,13 @@ class Popover { users: this.editor.users, mention: atMention, inserMention: (user) => { - const { $from } = this.state.selection; + // Use fresh state from the view, not the stale captured state + const currentState = this.editor.view.state; + const { $from } = currentState.selection; const length = atMention.length; const attributes = { ...user }; const mentionNode = this.editor.schema.nodes.mention.create(attributes); - const tr = this.state.tr.replaceWith($from.pos - length, $from.pos, mentionNode); + const tr = currentState.tr.replaceWith($from.pos - length, $from.pos, mentionNode); this.editor.view.dispatch(tr); this.editor.view.focus(); }, @@ -148,19 +150,87 @@ class Popover { } showPopoverAtPosition(pos) { - const end = this.view.coordsAtPos(pos); + let left = 0; + let top = 0; + let source = 'fallback'; + + // In presentation mode, find position using DOM elements in painterHost + const presentationEditor = this.editor.presentationEditor; + if (presentationEditor) { + const result = this.getViewportCoordsFromPainterHost(presentationEditor, pos); + if (result) { + left = result.left; + top = result.bottom; + source = 'painterHost DOM'; + } + } + + // Fallback to view.coordsAtPos for non-presentation mode + if (source === 'fallback') { + const coords = this.view.coordsAtPos(pos); + left = coords.left; + top = coords.bottom; + } + this.popoverRect = { width: 0, height: 0, - top: end.bottom, - left: end.left, - bottom: end.bottom, - right: end.left, + top: top, + left: left, + bottom: top, + right: left, }; this.tippyInstance.show(); } + /** + * Get viewport coordinates by finding the DOM element in the painted content. + * This works in presentation mode where the actual DOM is off-screen but + * painted elements exist in the painterHost. + */ + getViewportCoordsFromPainterHost(presentationEditor, pos) { + // Access painterHost through the DOM - it's a private field but we can find it by class + const visibleHost = presentationEditor.element; + if (!visibleHost) return null; + + // painterHost has class 'presentation-editor__pages' + const painterHost = visibleHost.querySelector('.presentation-editor__pages'); + if (!painterHost) return null; + + // Find all page elements + const pageEls = painterHost.querySelectorAll('.superdoc-page[data-page-index]'); + if (!pageEls.length) return null; + + // Search through pages for a span containing this position + for (const pageEl of pageEls) { + const spanEls = pageEl.querySelectorAll('span[data-pm-start][data-pm-end]'); + for (const spanEl of spanEls) { + const pmStart = Number(spanEl.dataset.pmStart); + const pmEnd = Number(spanEl.dataset.pmEnd); + + if (pos >= pmStart && pos <= pmEnd && spanEl.firstChild?.nodeType === Node.TEXT_NODE) { + const textNode = spanEl.firstChild; + const charIndex = Math.min(pos - pmStart, textNode.length); + + const range = document.createRange(); + range.setStart(textNode, charIndex); + range.setEnd(textNode, charIndex); + + const rect = range.getBoundingClientRect(); + + return { + left: rect.left, + top: rect.top, + bottom: rect.bottom, + }; + } + } + } + + return null; + } + getMentionText(from) { const maxLookBehind = 20; const startPos = Math.max(0, from - maxLookBehind);