feat(ui): add ui.selection.getRects and getAnchorRect (SD-2936)#3134
feat(ui): add ui.selection.getRects and getAnchorRect (SD-2936)#3134caio-pizzol merged 3 commits intomainfrom
Conversation
Custom-UI consumers building floating selection toolbars currently fall back to window.getSelection().getRangeAt(0).getBoundingClientRect(), which reads from the offscreen ProseMirror DOM and returns coordinates that don't match what the painter shows on screen. The painted selection rect already exists internally on PresentationEditor; this just lifts it onto the public controller surface so consumers stop reaching past useSuperDocHost() through an untyped cast. ui.selection.getRects(capture?) returns viewport-relative ViewportRect[] for the live selection (or a captured one). ui.selection.getAnchorRect( options?, capture?) returns a single rect with placement: 'start' (the default — Word/GDocs bubble menu placement), 'end', or 'union' (bounding rect across all line rects). Both return [] / null when there's no addressable selection or when the editor stub has no presentation layer. The capture path resolves the captured TextTarget via the existing resolveTextTarget adapter helper, then calls presentationEditor.getRangeRects(from, to). Multi-segment captures collapse to a single (firstSegment.start, lastSegment.end) range to match how the doc-api represents the selection internally. Tests cover the live path (rects mapping, missing presentation layer, exception swallowing), each placement mode, and the captured-selection shapes.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5c03aa4460
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
…ET (SD-2936) Review feedback on the captured-selection rect path: 1. Captures whose target.story is non-body now early-return [] with the limitation documented on SelectionHandle.getRects. The full fix requires a getRangeRectsForStory(from, to, story) primitive on PresentationEditor that doesn't yet exist publicly — until that lands, captured rects on header / footer / footnote / endnote surfaces silently failed (resolveTextTarget against the host's body doc returns null for those blockIds). Body-only matches the same posture as scroll-into-view's text-anchored path. Live-selection rects still work on every surface because PresentationEditor .getSelectionRects() routes through getActiveEditor() internally. 2. resolveTextTarget re-throws AMBIGUOUS_TARGET (two blocks sharing an id) so callers can log the precise diagnostic. The bare catch was swallowing this. Now surfaces it via console.warn before returning []. 3. Trimmed the multi-segment comment to the rationale only — the first two sentences paraphrased the immediate next line, which the comment policy bans. Tests cover the non-body story rejection (returns [] without calling getRangeRects).
…SD-2936) The previous patch documented a body-only limitation; this is the actual fix. getRects(capture) and getAnchorRect(options, capture) now take both the host editor (for the presentation layer's getRangeRects) and the routed editor (for resolveTextTarget against the captured block ids). For captures taken in a non-body story (header, footer, footnote, endnote), the routed editor at call time owns the PM document those block ids belong to. Resolving against it produces the right positions, which then flow through presentationEditor.getRangeRects to land on the right surface. The previous resolveHostEditor-only path resolved non-body block ids against the body PM doc and silently returned []. The remaining limitation: when focus has moved to a sidebar / composer by call time, the routed editor falls back to the body and the captured non-body block ids no longer resolve there. The function returns [] gracefully in that case rather than misclassifying. Fully cross-surface captures need a story-keyed editor lookup on PresentationEditor that doesn't yet exist publicly — that's a follow-up.
|
🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.44 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.3.0-next.88 |
|
🎉 This PR is included in @superdoc-dev/react v1.2.0-next.86 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-cli v0.8.0-next.62 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.30.0-next.45 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.8.0-next.47 |
Two review issues from PR #3139: 1. entityAt previously called document.elementFromPoint globally and walked all ancestors with no check that the controller had a mounted editor or that the hit landed inside this instance's painted DOM. A page mounting two SuperDoc instances would have one's entityAt return ids from the other; post-destroy calls would return stale ids from cached painted nodes. Now resolves the host editor via resolveHostEditor, reads presentationEditor.visibleHost (newly added to the structural type), and returns [] when the host is missing or the hit element isn't inside it. 2. The published `superdoc/ui` declaration barrel at packages/superdoc/src/ui.d.ts didn't list the new public types, so `import type { ViewportEntityHit, ViewportEntityAtInput } from 'superdoc/ui'` failed for consumers. Same gap existed for SelectionAnchorRectOptions from PR #3134. Added all three.
Lifts the painted selection rect from the internal
PresentationEditorto the public controller, so consumers building floating selection toolbars stop reaching forwindow.getSelection()(which reads from the offscreen ProseMirror DOM and returns coordinates that don't match what the painter shows). Today the only working path is an untyped cast throughuseSuperDocHost()intosuperdoc.activeEditor.presentationEditor.getSelectionRects(). Now it'sui.selection.getAnchorRect().getRects(capture?)returns viewport-relativeViewportRect[]for the live selection or a captured one.getAnchorRect(options?, capture?)returns a single rect;placement: 'start'(default) matches Word/GDocs bubble-menu placement,'end'and'union'cover bottom-anchored popovers and selection overlays. Empty selection, no-presentation-layer, and stale captures all return[]/nullrather than throw.Captured rects route block-id resolution through the routed editor (so captures taken in a header / footer / footnote / endnote still resolve while the user remains in that story) and the rect engine through the host's presentation layer. When focus has moved to a sidebar / composer by call time and the routed editor has fallen back to the body, captured non-body ids fail to resolve and the call returns
[]gracefully rather than mixing surfaces — fully cross-surface captured rects need a story-keyed editor lookup onPresentationEditorthat doesn't yet exist publicly. Live rects work on every surface becausepresentationEditor.getSelectionRects()routes throughgetActiveEditor()internally.AMBIGUOUS_TARGETerrors fromresolveTextTargetare surfaced viaconsole.warnrather than swallowed, matching the diagnostic contract that helper sets out for callers.First in a stack of five PRs against SD-2936. Subsequent PRs (entityAt, context-menu contribution, selection.restore, command shortcuts) build on this.
Verified:
pnpm exec vitest run src/ui→ 205 passed (13 files);pnpm --filter @superdoc/super-editor build→ clean;pnpm --filter superdoc build→ clean.