Skip to content

feat(ui): add ui.selection.getRects and getAnchorRect (SD-2936)#3134

Merged
caio-pizzol merged 3 commits intomainfrom
caio/sd-2936-selection-rects
May 5, 2026
Merged

feat(ui): add ui.selection.getRects and getAnchorRect (SD-2936)#3134
caio-pizzol merged 3 commits intomainfrom
caio/sd-2936-selection-rects

Conversation

@caio-pizzol
Copy link
Copy Markdown
Contributor

@caio-pizzol caio-pizzol commented May 4, 2026

Lifts the painted selection rect from the internal PresentationEditor to the public controller, so consumers building floating selection toolbars stop reaching for window.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 through useSuperDocHost() into superdoc.activeEditor.presentationEditor.getSelectionRects(). Now it's ui.selection.getAnchorRect().

getRects(capture?) returns viewport-relative ViewportRect[] 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 [] / null rather 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 on PresentationEditor that doesn't yet exist publicly. Live rects work on every surface because presentationEditor.getSelectionRects() routes through getActiveEditor() internally.

AMBIGUOUS_TARGET errors from resolveTextTarget are surfaced via console.warn rather 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.

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.
@caio-pizzol caio-pizzol requested a review from a team as a code owner May 4, 2026 23:52
@linear
Copy link
Copy Markdown

linear Bot commented May 4, 2026

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread packages/super-editor/src/ui/selection-rects.ts Outdated
@codecov-commenter
Copy link
Copy Markdown

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.
@caio-pizzol caio-pizzol merged commit 37221ff into main May 5, 2026
67 checks passed
@caio-pizzol caio-pizzol deleted the caio/sd-2936-selection-rects branch May 5, 2026 00:28
@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.44

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in vscode-ext v2.3.0-next.88

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in @superdoc-dev/react v1.2.0-next.86

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in superdoc-cli v0.8.0-next.62

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in superdoc v1.30.0-next.45

The release is available on GitHub release

@superdoc-bot
Copy link
Copy Markdown
Contributor

superdoc-bot Bot commented May 5, 2026

🎉 This PR is included in superdoc-sdk v1.8.0-next.47

caio-pizzol added a commit that referenced this pull request May 5, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants