feat(ui): story-aware selection state and capture/restore (SD-2954)#3166
feat(ui): story-aware selection state and capture/restore (SD-2954)#3166caio-pizzol merged 7 commits intomainfrom
Conversation
Two primitives consumers building custom UI keep reaching for and not
finding on the public surface:
ui.viewport.getHost() returns the editor's painted host element so
custom-UI components scope DOM listeners to the editor without a CSS
class filter. The information already lives on
presentationEditor.visibleHost; this lifts it onto the controller.
ui.viewport.positionAt({ x, y }) resolves a viewport coordinate to a
caret position on the routed editor's PM document, returning both the
SelectionPoint and the SelectionTarget shapes so consumers can pass
the result straight to editor.doc.insert / replace / etc. The natural
pair to entityAt: while entityAt answers "what entity is under this
point?", positionAt answers "what caret position is under this
point?" — the missing primitive that lets right-click menus offer
"Paste here" / "Insert at this point" honestly, instead of dispatching
against the user's previous selection.
Both methods scope to the controller's painted host: a multi-instance
page can't have one controller's positionAt return positions in
another's PM doc, and post-destroy calls return null.
Tests cover the happy path, the no-editor-mounted case, and the
missing-posAtCoords stub case.
…-2943) readBlockId now uses the sdBlockId ?? id ?? blockId fallback the selection resolver already applies, so positionAt resolves paragraph clicks instead of returning null. Adds PresentationEditor.getActiveStoryLocator (unifies story-session and header/footer-session locators) and threads the result onto SelectionPoint.story / SelectionTarget.story so doc-api operations route to the active story instead of falling back to body.
The controller stamps the active story locator onto the live TextTarget when the routed editor is a header/footer/footnote/endnote, so state.selection.target / selectionTarget and ui.selection.capture() all carry the same routing information ui.viewport.positionAt got in SD-2943. ui.selection.restore now compares the captured story against the active surface and returns a typed 'stale' on mismatch instead of falling through to a less-specific resolver failure. Captures with no story keep the prior body/default behavior. The fix is scoped to the controller surface. Direct editor.doc.selection.current() calls still return body-scoped targets; threading story through the lower-level resolver is a separate change.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 50a391e520
ℹ️ 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".
Move readActiveStoryLocator and attachStoryToTextTarget below textTargetToSelectionTarget so the existing JSDoc reattaches to its function (it was orphaned between two JSDoc blocks). Move the SD-2954 story-mismatch check after the isEditable / setTextSelection guards so a header capture restored against a viewing-mode editor still surfaces 'read-only', matching what body captures already see in the same condition. Adds a regression test covering the read-only + story-capture path.
readActiveStoryLocator was reading hostEditor.presentationEditor directly, missing the legacy _presentationEditor field and the superdocStore.documents[].getPresentationEditor() lookup that resolveToolbarSources covers. Mounts using either fallback would still report body-scoped selection state and return 'stale' for valid story captures. Route the locator lookup through resolveToolbarSources so all three documented presentation-resolution paths surface the active story. Selection-restore drops its duplicate helper and accepts the pre-resolved locator from the controller, removing the separate code path. Adds a regression test covering the _presentationEditor fallback.
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
|
🎉 This PR is included in @superdoc-dev/mcp v0.3.0-next.55 The release is available on GitHub release |
|
🎉 This PR is included in vscode-ext v2.3.0-next.99 |
|
🎉 This PR is included in @superdoc-dev/react v1.2.0-next.97 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-cli v0.8.0-next.73 The release is available on GitHub release |
|
🎉 This PR is included in superdoc v1.30.0-next.56 The release is available on GitHub release |
|
🎉 This PR is included in superdoc-sdk v1.8.0-next.56 |
Stacked on top of #3157. The controller seam stamps the active story locator onto the live
TextTargetwhen the routed editor is a header / footer / footnote / endnote, sostate.selection.target,state.selection.selectionTarget, andui.selection.capture()all carry the samestoryfield thatui.viewport.positionAtgot in SD-2943. Without it, custom-UI flows that read the live selection and feed it toeditor.doc.insert/replaceroute to body and silently fail to find the block.ui.selection.restorenow compares the captured story against the active surface up-front and returns a typed'stale'on mismatch (or when the captured story is no longer active). Match by structural value acrossstoryType,refId,noteId,headerFooterKind,variant, and serializedsection, so a fresh locator object with the same fields still matches. Captures with nostorykeep the prior body/default behavior.The selection-info resolver runs against the routed editor and has no path back to the host's
PresentationEditor, so the controller seam is the only place where both editors are reachable. Same shape SD-2943 used for the same reason.Scope: this fixes the controller surface (
ui.state.selection.target,ui.state.selection.selectionTarget,ui.selection.capture,ui.selection.restore). Directeditor.doc.selection.current()calls are intentionally unchanged. Threading story through the lower-level adapter is a deeper change that belongs in its own ticket; flagging here so reviewers don't expect it to land with this PR.Verified:
pnpm exec vitest run src/ui-> 258 passed (16 files, +4 new);pnpm exec tsc -b tsconfig.references.json-> clean.