Selection

The cursor and range state held by the model.

The browser tracks selection as a DOM Range — an anchor node, an offset, a focus node, an offset. That representation is about rendered layout, not document structure. Two selections that look identical on screen can have different DOM Ranges depending on how the runtime split text nodes for the last render. Persisting or replaying the user's selection across edits, renders, or re-mounts is brittle.

SpineEditor keeps selection in the model. A Selection is a plain, JSON-safe value addressed by document positions and paths. The DOM is a projection of it, not the source.

Worked example

A caret after the fourth UTF-16 unit of the first paragraph:

const selection: TextSelection = textSelection(pos(4), pos(4));

That value is collapsed text selection state. Path hints may be attached by the runtime when they are needed to disambiguate same-offset boundaries.

Selecting a media atom:

const selection: NodeSelection = nodeSelection([3]);

Selecting three consecutive paragraphs under the root:

const selection: BlockSelection = blockSelection([0], [2]);

BlockSelection normalizes the ordered fromPath / toPath, while anchorPath / headPath preserve selection direction.

Three kinds

type Selection = TextSelection | NodeSelection | BlockSelection;
  • TextSelection{ kind: "text", anchor, head, from, to, isCollapsed, anchorPathHint?, headPathHint?, fromPathHint?, toPathHint? }. Anchor/head preserve direction; from/to are normalized. Collapsed means caret. Path hints speed up resolution but are ignored when they no longer match the document.
  • NodeSelection{ kind: "node", path, isCollapsed: false }. One structural node (a media atom, for example). Never collapsed.
  • BlockSelection{ kind: "block", parentPath, anchorPath, headPath, fromPath, toPath, isCollapsed }. Contiguous sibling blocks under one parent.

Predicates: isCollapsed, isTextSelection, isNodeSelection, isBlockSelection.

Mapping through a transaction

When a transaction changes the document, text positions may shift. mapSelection(selection, mapPos) maps a text selection through a transform using the Mapping returned from apply. Structural selections are path-based, so the engine runs its domain-bounded structural carry/repair layer before commit. Unsupported structural selections collapse to a deterministic text boundary instead of inventing a cross-parent range.

Pos semantics

Pos is Brand<number, "Pos"> — a zero-based UTF-16 code-unit offset into the document. Construct it with pos(value), which rejects non-integer or negative inputs. A grapheme cluster like a combining emoji spans multiple Pos units; the runtime is responsible for not splitting one on arrow navigation.

API

textSelection(anchor: Pos, head: Pos, options?): TextSelection;
nodeSelection(path: NodePath): NodeSelection;
blockSelection(anchorPath: NodePath, headPath: NodePath): BlockSelection;

mapSelection(selection: Selection, mapPos): Selection;

See packages/core/src/selection.ts for the full surface.