You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
* feat(document-api): selection primitives + multi-block comment targets (SD-2668)
Adds `editor.doc.selection.current()` and `editor.doc.selection.onChange()` so
consumers can build custom toolbars, comments sidebars, and selection-driven
UIs without reaching into ProseMirror internals. Widens `comments.create`
target to accept `TextTarget` so multi-block selections anchor across blocks
instead of silently collapsing to the first segment.
- Consumers previously had to resolve PM positions through
`editor.state.doc`, walk the PM tree for the containing block's `sdBlockId`,
and convert to flattened-text offsets — ~30 lines of editor internals.
`selection.current()` returns a portable `SelectionInfo { empty, target,
activeMarks, text? }` with a multi-segment `TextTarget` ready for
`comments.create`.
- `comments.list().target` already used multi-segment `TextTarget`; the
write side (`comments.create`) only accepted single-block `TextAddress`,
so drag-selecting across paragraphs lost data with no warning.
- Contract, schemas, dispatch, and adapter wired. Super-editor adapter
projects PM selections into the flattened-text model via the existing
`computeTextContentLength` helper.
Part of the SD-2667 drop-in assessment umbrella.
* fix(document-api): address PR review on selection + comments scope
1. Drop the aspirational `in: StoryLocator` parameter from
`selection.current`. The adapter always read the live editor selection
and merely copied `input.in` into the returned `TextTarget.story`, so a
body selection could be mislabeled as a header/footer selection. The
operation now documents that it always reflects whichever story holds
focus; story-scoped selection reads are out of scope.
2. Narrow `comments.create` multi-segment handling to contiguous ranges.
Validation previously accepted any non-empty segment array; the handler
spanned `first.start → last.end` as a single PM range, so disjoint or
out-of-order segments would silently anchor the comment over text the
caller never selected. `addCommentHandler` now resolves every segment,
rejects out-of-order pairs, and rejects any pair with non-empty text
between them (`INVALID_TARGET`).
3. Fix schema inconsistency: `SelectionInfo.target` is `TextTarget | null`
at the type level, but the published schema required it as an `object`.
Schema now uses `oneOf: [textTargetSchema, { type: 'null' }]` so empty
selections validate against the exported contract.
4. Make `SelectionAdapter` optional on `DocumentApiAdapters`. This is a
public exported interface; adding a required member is a source
breaking change for external adapter constructors. The factory now
throws `SELECTION_ADAPTER_UNAVAILABLE` when selection operations are
called without a registered adapter.
Tests: add 3 new cases (multi-segment forward, missing-adapter for
`current` + `onChange`). 1374 pass, 0 fail.
* fix(document-api): correct selection offset mapping + cancel pending flush
Two bot findings on PR 2924:
- P1: `collectTextSegments` was deriving block-relative offsets with
`selStart - blockStart`, treating raw PM positions as flattened text
offsets. That's only equivalent when the block contains pure text; it
diverges whenever the block has inline wrappers (e.g. `run` marks) or
leaf atoms whose PM boundary tokens do not count in the flattened
model. A selection inside a run would therefore return a `TextTarget`
off by the number of wrapper boundary tokens, and `comments.create`
using that target would anchor to the wrong text.
Added `pmPositionToTextOffset(blockNode, blockPos, pmPos)` alongside
the existing `resolveTextRangeInBlock` in `text-offset-resolver.ts` —
it walks the block with the same flattened model (text = length,
leaf = 1, block separator = 1, inline wrapper tokens = 0) and
returns the correct offset. `collectTextSegments` now uses it for
both endpoints.
- P2: `subscribeToSelection` scheduled `flush` via `queueMicrotask` but
the returned unsubscribe only detached listeners — a microtask
already queued before cleanup would still fire, invoking the listener
after unsubscribe returned (stale state updates on component unmount).
Added a `cancelled` closure flag set by unsubscribe and checked in
`flush` and `schedule`.
Tests: 5 new cases for `pmPositionToTextOffset` covering plain text,
inline wrapper transparency, leaf atoms with `nodeSize > 1`, before-
block-start, and past-block-end. 11515 super-editor pass, 0 fail.
* test(document-api): positive-path + write-side selection coverage
Addresses PR review findings on test coverage:
- Adds `selection-info-resolver.test.ts` (11 cases) covering
`resolveCurrentSelectionInfo` projection (empty state, single-block
selection, multi-block segment-per-touched-block, missing blockId,
includeText on/off, active-marks empty path) and
`subscribeToSelection` (listener fires once per tick, unsubscribe
stops firing, queued-microtask-cancellation on unmount).
- Adds 3 write-side cases to `comments-wrappers.test.ts` for the
multi-segment path: out-of-order rejection with INVALID_TARGET /
"document order" message, non-contiguous-gap rejection with
"contiguous" message, and the contiguous-success path verifying
the spanned PM range [first.from, last.to] is applied.
- Updates `assemble-adapters.test.ts` to assert both
`selection.current` and `selection.onChange` are wired on the
adapter bag.
- Adds a new section in `tests/consumer-typecheck/src/customer-scenario.ts`
exercising the exported `editor.doc.selection.current()` surface,
`SelectionInfo` destructuring, multi-segment `TextTarget` pass-through
to `comments.create`, and the `onChange` subscription shape. Requires
threading the new types through `packages/super-editor/src/index.ts`
and `packages/superdoc/src/index.js` JSDoc typedefs so they are
reachable from the `superdoc` package entrypoint.
- Exempts `selection.onChange` from the contract-parity member-path
check via `META_MEMBER_PATHS` — it is a subscription primitive, not
a request/response operation, so it does not belong in
`OPERATION_DEFINITIONS` / schemas / dispatch. Fixes the `validate`
CI job that otherwise rejects the new runtime member.
Deferred to SD-2671 (follow-up):
- doc-api-stories/comments/multi-segment-target.ts (CLI-harness story)
- doc-api-stories/selection story
- Playwright behavior test that drives selection.current → comments.create
Verified: document-api 1374 pass, super-editor 11529 pass,
tests/consumer-typecheck compiles clean against the packed tarball.
---------
Co-authored-by: Caio Pizzol <caio@superdoc.dev>
Copy file name to clipboardExpand all lines: apps/docs/document-api/reference/comments/create.mdx
+10-8Lines changed: 10 additions & 8 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -27,12 +27,7 @@ Returns a Receipt confirming the comment was created; reports NO_OP if the ancho
27
27
| Field | Type | Required | Description |
28
28
| --- | --- | --- | --- |
29
29
|`parentCommentId`| string | no ||
30
-
|`target`| TextAddress | no | TextAddress |
31
-
|`target.blockId`| string | no ||
32
-
|`target.kind`|`"text"`| no | Constant: `"text"`|
33
-
|`target.range`| Range | no | Range |
34
-
|`target.range.end`| integer | no ||
35
-
|`target.range.start`| integer | no ||
30
+
|`target`| TextAddress \\| TextTarget | no | One of: TextAddress, TextTarget |
36
31
|`text`| string | yes ||
37
32
38
33
### Example request
@@ -118,8 +113,15 @@ Returns a Receipt confirming the comment was created; reports NO_OP if the ancho
118
113
"type": "string"
119
114
},
120
115
"target": {
121
-
"$ref": "#/$defs/TextAddress",
122
-
"description": "Text range to anchor the comment: {kind:'text', blockId:'...', range:{start:N, end:N}}."
116
+
"description": "Text range to anchor the comment. Accepts either a single-block TextAddress {kind:'text', blockId, range} or a multi-segment TextTarget {kind:'text', segments:[{blockId, range}, ...]} for selections that span blocks.",
@@ -583,6 +584,12 @@ The tables below are grouped by namespace.
583
584
| --- | --- | --- |
584
585
| <spanstyle={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><ahref="/document-api/reference/ranges/resolve"><code>ranges.resolve</code></a></span> | <spanstyle={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.ranges.resolve(...)</code></span> | Resolve two explicit anchors into a contiguous document range. Returns a transparent SelectionTarget, a mutation-ready ref, and preview metadata. Stateless and deterministic. |
585
586
587
+
#### Selection
588
+
589
+
| Operation | API member path | Description |
590
+
| --- | --- | --- |
591
+
| <spanstyle={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><ahref="/document-api/reference/selection/current"><code>selection.current</code></a></span> | <spanstyle={{ whiteSpace: 'nowrap', wordBreak: 'normal', overflowWrap: 'normal' }}><code>editor.doc.selection.current(...)</code></span> | Read the editor's current selection as a portable SelectionInfo with a text-anchored TextTarget. Primitive for building custom comments UIs, floating toolbars, and other selection-driven components without reaching into ProseMirror internals. |
description: "Read the editor's current selection as a portable SelectionInfo with a text-anchored TextTarget. Primitive for building custom comments UIs, floating toolbars, and other selection-driven components without reaching into ProseMirror internals."
5
+
---
6
+
7
+
{/* GENERATED FILE: DO NOT EDIT. Regenerate via `pnpm run docapi:sync`. */}
8
+
9
+
## Summary
10
+
11
+
Read the editor's current selection as a portable SelectionInfo with a text-anchored TextTarget. Primitive for building custom comments UIs, floating toolbars, and other selection-driven components without reaching into ProseMirror internals.
12
+
13
+
- Operation ID: `selection.current`
14
+
- API member path: `editor.doc.selection.current(...)`
15
+
- Mutates document: `no`
16
+
- Idempotency: `idempotent`
17
+
- Supports tracked mode: `no`
18
+
- Supports dry run: `no`
19
+
- Deterministic target resolution: `yes`
20
+
21
+
## Expected result
22
+
23
+
Returns a SelectionInfo with `empty`, `target` (TextTarget or null), `activeMarks`, and optionally `text` when `includeText: true`.
Copy file name to clipboardExpand all lines: apps/docs/document-engine/sdks.mdx
+4Lines changed: 4 additions & 0 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -571,6 +571,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
571
571
|`doc.getHtml`|`get-html`| Extract the document content as an HTML string. |
572
572
|`doc.markdownToFragment`|`markdown-to-fragment`| Convert a Markdown string into an SDM/1 structural fragment. |
573
573
|`doc.info`|`info`| Return document summary info including word, character, paragraph, heading, table, image, comment, tracked-change, SDT-field, list, and page counts, plus outline and capabilities. |
574
+
|`doc.extract`|`extract`| Extract all document content with stable IDs for RAG pipelines. Returns blocks with full text, comments, and tracked changes — each with an ID compatible with scrollToElement(). |
574
575
|`doc.clearContent`|`clear-content`| Clear all document body content, leaving a single empty paragraph. |
575
576
|`doc.insert`|`insert`| Insert content into the document. Two input shapes: text-based (value + type) inserts inline content at a SelectionTarget or ref position within an existing block; structural SDFragment (content) inserts one or more blocks as siblings relative to a BlockNodeAddress target. When target/ref is omitted, content appends at the end of the document. Text mode supports text (default), markdown, and html content types via the `type` field. Structural mode uses `placement` (before/after/insideStart/insideEnd) to position relative to the target block. |
576
577
|`doc.replace`|`replace`| Replace content at a contiguous document selection. Text path accepts a SelectionTarget or ref plus replacement text. Structural path accepts a BlockNodeAddress (replaces whole block), SelectionTarget (expands to full covered block boundaries), or ref plus SDFragment content. |
@@ -580,6 +581,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
580
581
|`doc.blocks.deleteRange`|`blocks delete-range`| Delete a contiguous range of top-level blocks between two endpoints (inclusive). Both endpoints must be direct children of the document node. Supports dry-run preview. |
581
582
|`doc.query.match`|`query match`| Deterministic selector-based search returning mutation-grade addresses and text ranges. Use this to discover targets before any mutation. |
582
583
|`doc.ranges.resolve`|`ranges resolve`| Resolve two explicit anchors into a contiguous document range. Returns a transparent SelectionTarget, a mutation-ready ref, and preview metadata. Stateless and deterministic. |
584
+
|`doc.selection.current`|`selection current`| Read the editor's current selection as a portable SelectionInfo with a text-anchored TextTarget. Primitive for building custom comments UIs, floating toolbars, and other selection-driven components without reaching into ProseMirror internals. |
583
585
|`doc.mutations.preview`|`mutations preview`| Dry-run a mutation plan, returning resolved targets without applying changes. |
584
586
|`doc.mutations.apply`|`mutations apply`| Execute a mutation plan atomically against the document. |
585
587
|`doc.capabilities.get`|`capabilities`| Query runtime capabilities supported by the current document engine. |
@@ -1031,6 +1033,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
1031
1033
|`doc.get_html`|`get-html`| Extract the document content as an HTML string. |
1032
1034
|`doc.markdown_to_fragment`|`markdown-to-fragment`| Convert a Markdown string into an SDM/1 structural fragment. |
1033
1035
|`doc.info`|`info`| Return document summary info including word, character, paragraph, heading, table, image, comment, tracked-change, SDT-field, list, and page counts, plus outline and capabilities. |
1036
+
|`doc.extract`|`extract`| Extract all document content with stable IDs for RAG pipelines. Returns blocks with full text, comments, and tracked changes — each with an ID compatible with scrollToElement(). |
1034
1037
|`doc.clear_content`|`clear-content`| Clear all document body content, leaving a single empty paragraph. |
1035
1038
|`doc.insert`|`insert`| Insert content into the document. Two input shapes: text-based (value + type) inserts inline content at a SelectionTarget or ref position within an existing block; structural SDFragment (content) inserts one or more blocks as siblings relative to a BlockNodeAddress target. When target/ref is omitted, content appends at the end of the document. Text mode supports text (default), markdown, and html content types via the `type` field. Structural mode uses `placement` (before/after/insideStart/insideEnd) to position relative to the target block. |
1036
1039
|`doc.replace`|`replace`| Replace content at a contiguous document selection. Text path accepts a SelectionTarget or ref plus replacement text. Structural path accepts a BlockNodeAddress (replaces whole block), SelectionTarget (expands to full covered block boundaries), or ref plus SDFragment content. |
@@ -1040,6 +1043,7 @@ The SDKs expose all operations from the [Document API](/document-api/overview) p
1040
1043
|`doc.blocks.delete_range`|`blocks delete-range`| Delete a contiguous range of top-level blocks between two endpoints (inclusive). Both endpoints must be direct children of the document node. Supports dry-run preview. |
1041
1044
|`doc.query.match`|`query match`| Deterministic selector-based search returning mutation-grade addresses and text ranges. Use this to discover targets before any mutation. |
1042
1045
|`doc.ranges.resolve`|`ranges resolve`| Resolve two explicit anchors into a contiguous document range. Returns a transparent SelectionTarget, a mutation-ready ref, and preview metadata. Stateless and deterministic. |
1046
+
|`doc.selection.current`|`selection current`| Read the editor's current selection as a portable SelectionInfo with a text-anchored TextTarget. Primitive for building custom comments UIs, floating toolbars, and other selection-driven components without reaching into ProseMirror internals. |
1043
1047
|`doc.mutations.preview`|`mutations preview`| Dry-run a mutation plan, returning resolved targets without applying changes. |
1044
1048
|`doc.mutations.apply`|`mutations apply`| Execute a mutation plan atomically against the document. |
1045
1049
|`doc.capabilities.get`|`capabilities`| Query runtime capabilities supported by the current document engine. |
0 commit comments