Skip to content

Selection API

Jiuqing Song edited this page Feb 15, 2019 · 2 revisions

Selection APIs help manage focus/selections. Selection APIs include a Position class and some related utilities.

Position

Position class represents a position inside a DOM tree. It is a combination of a DOM node, a offset value, and some other properties:

Position properties

class Position implements NodePosition {
    readonly node: Node;
    readonly element: HTMLElement;
    readonly offset: number;
    readonly isAtEnd: boolean;
    ...
}

NodePosition is the interface of Position class. We declare it in roosterjs-editor-types package so that other interfaces from this package can also use it.

For example, given this HTML segment:

<html>
  <body>
    <div id="div1">
       <span id="span1">Text 1</span>
       <span id="span2">Text 2</span>
    </div>
  </body>
</html>

node is the DOM node of this position. It can be an HTML Element, or a Text node. With the HTML code above, if we want to have a position between the two SPAN nodes, then the node here will be DIV#div1.

offset is a number, it represents the character count (for text node) or the child node count (for HTML element) from the beginning of node to the position. With the HTML code above, if we want to have a position between the two SPAN nodes, then the offset here will be 1 since there is only one child node (SPAN#span1) before the position under node.

isAtEnd is a calculated value. When it is true, it means current position is at the end of node. In most case this value can be calculated from offset since we know the child node count/character count under current node. One exception is for void node (such as IMG, BR, INPUT, ...), they can never have child node, so we use isAtEnd to specify whether the position is at the beginning or ending of the void node.

element is a calculated value. Its value is set to the node when node is an HTML element, or its parent element.

Create a Position

An instance of Position can be created from the several ways:

  • Clone from an existing Position
class Position {
    ...
    constructor(position: NodePosition);
}

This will create a cloned position from the existing position object.

  • Normalize from an existing Position
class Position {
    ...
    normalize(): NodePosition;
}

Position.normalize() will always go to the leaf node of the DOM tree.

With the HTML code above, if we want to normalize a position between the two SPAN nodes, the result will be the beginning of text node "Text 2".

  • Create from a node and an offset value
class Position {
    ...
    constructor(node: Node, offset: number);
}

This will create Position object with the given node and offset. Node must be a valid HTML node, and offset should be a valid number within [0, <Count of child nodes/characters>]. And the constructor will do a verification and make sure the final offset value must be valid.

For example with the HTML code above, if we want to create a position with node = DIV#div1 and offset = 1, the returned value will be the position between two SPANs, but if use offset = 3 instead, the final result will be node = DIV#div1 and offset = 2 because there are only 2 child nodes under DIV#div1.

class Position {
    ...
    constructor(node: Node, positionType: PositionType);
}

const enum PositionType {
    Begin = 0,
    End = -1,
    Before = -2,
    After = -3,
}

Using PositionType can easily specify a position at beginning/ending of a node without calculating its child node/character count, or before/after a node with knowning its parent node, the constructor function will do these calculation for us.

For example with the HTML code above, if we create a position with node = DIV#div1 and positionType = PositionType.End, the returned value will be the position node = DIV#div1 and offset = 2 because there are only 2 child nodes under DIV#div1. The same result can also be created from node = SPAN#span2 and positionType = PositionType.After.

  • Create from a Range with its start/end position
class Position {
    ...
    static getStart(range: Range): NodePosition;
    static getEnd(range: Range): NodePosition;
}

When we have a range object, using Position.getStart() / Position.getEnd() will be an easy way to get position with range.start/endContainer and range.start/endOffset.

Use a Position

Position class has 4 methods:

Normalize a position, use the leaf level node and offset to represent the same position.

Check if current position is equal to the given position.

Check if current position is after the given position.

Move the position with the given offset, and return the moved result. This will only move under the same container node. If the moving result is beyond current container node, the result will be a trimmed position still under the same node.

Note that a Position object is immutable. So that any methods changing current position will return a new one, and current position keeps no change.

Selection Utilities

getPositionRect

function getPositionRect(position: NodePosition): Rect;

getPositionRect() function gets the top/bottom/left/right value of a position. Top-left corner of the document is (0,0). Note that a position has no width, so left and right value are always the same.

isPositionAtBeginningOf

 function isPositionAtBeginningOf(position: NodePosition, targetNode: Node): boolean;

isPositionAtBeginningOf() function check if a given position is at the beginning of a node. The function returns true if there no visible content between the start of node and the given position. So that if there are some empty tag like SPAN, FONT, ..., return value is not impacted. But visible element such as IMG, LI will be treated as visible element and the funtion will return false if there are such elements before the position. See isNodeEmpty() for more details.

getSelectionPath

function getSelectionPath(rootNode: HTMLElement, range: Range): SelectionPath;

getSelectionPath() function converts a Range object to a SelectionPath object to help serialize a range. When we want to save editor content with current selection, this function will be used.

The function requires a rootNode as parameter, which will be the parent of all content. The result selection path will be relative to this root node.

createRange and getRangeFromSelectionPath

function createRange(node: Node, endNode?: Node): Range;

function createRange(start: NodePosition, end?: NodePosition): Range;

function getRangeFromSelectionPath(rootNode: HTMLElement, path: SelectionPath): Range;

These functions are used for create a Range object from Nodes, Positions or SelectionPaths.

createRange() function can create a Range object from 1-2 Nodes or 1-2 NodePositions. If only 1 node is specified, the range will cover the node; if 2 nodes are specified, result will start before the first node and end after the second node. If only 1 NodePosition is specified, the result will be a collapsed range at the given position; if 2 NodePositions are specified, the result will start at the first position and end at the second position.

getRangeFromSelectionPath() function converts a SelectionPath back to Range. It also requires a rootNode as paremeter which will be the root node of the content, selection path will be calculated from this root node.