Lexical iOS is a rich-text editor on iOS parallel to the its javascript counterpart, Lexical Javascript. Lexical iOS is based on TextKit 1.
Now we focus on the design of selection.
1 Concepts
A selection specifies a part of a document. We have three variants:
- A range selection specifies a segment in a document.
- A node selection specifies a set of nodes in a document.
- A grid selection specifies a sub-rectangle in a grid.
1.1 Representation
Range Selection. A range selection is represented by a pair of selection points. A selection point specifies a location in the document, which is either a position in a text node or a position in an element node. The segment specified by the range selection is the part of document between its end points, called anchor and focus.
Node Selection. A node selection is explicitly represented by a set of nodes, actually, by a set of node keys which can be used to identify the set of nodes.
Grid Selection. A grid selection is represented by a grid node together with an anchor node and a focus node in it.
The range selection is the most essential of the three variants. We will focus on it in the following.
2 Implementation of Range Selection
2.1 Clarification on the selected part
Consider a range selection selection
with the anchor at the first child of an element element
, and the focus at the location past the last child of the same element. Then what should selection.getNodes()
return? The list of the children, or all the nodes of the subtrees rooted at these children. We choose the latter in Lexical iOS.
It is clear that we intend to select the subtrees. The ambiguity lies in that a node can be interpreted as a node in itself or as the root of a subtree. Either way is fine as long as we are clear which interpretation we are up to, and implement the operations consistently.
2.2 baseSelection
: the protocol
Method | Semantic |
---|---|
dirty |
✓ |
clone() |
- |
extract() |
✓ |
getNodes() |
✓ |
getTextContent() |
Returns a plain text representation of the content of the selection. |
insertRawText(_:) |
Deal with \n and insert. |
isSelection(_:) |
Checks for selection equality. |
insertNodes(nodes:selectStart:) |
✓ |
deleteCharacter(isBackwards:) |
- |
deleteWord(isBackwards:) |
- |
deleteLine(isBackwards:) |
- |
insertParagraph() |
- |
insertLineBreak(selectStart:) |
soft line break |
insertText(_:) |
✓ |
2.3 Point
: the selection point
Field | Semantic |
---|---|
key: NodeKey |
|
type: SelectionType |
|
offset: Int |
|
selection: baseSelection? |
The selection that contains this point. |
2.4 RangeSelection
: the range selection
2.4.1 Fields
Constituents
Field | Semantic |
---|---|
anchor |
- |
focus |
- |
Properties
Field | Semantic |
---|---|
dirty |
- |
format |
- |
style |
- |
2.4.2 Methods
Implementation of baseSelection
protocol:
Method | Implementation |
---|---|
dirty |
member field |
clone() |
- |
extract() |
Obtain getNodes() and cut at the ends. |
getNodes() |
Return all nodes that are selected as a whole or partially. |
getTextContent() |
Just concatenate. |
insertRawText(_:) |
Split by \n and insert the results. |
isSelection(_:) |
- |
insertNodes(nodes:selectStart:) |
~200 Lines. |
deleteCharacter(isBackwards:) |
~100 Lines. |
deleteWord(isBackwards:) |
Invoke modify(alter:isBackward:granularity:) . |
deleteLine(isBackwards:) |
Invoke modify(alter:isBackward:granularity:) . |
insertParagraph() |
~100 Lines. |
insertLineBreak(selectStart:) |
- |
insertText(_:) |
~300 Lines. |
Miscellaneous:
Method | Semantic | Note |
---|---|---|
hasFormat(type:) |
- | |
isBackward() |
- | |
isCollapsed() |
- | |
getCharacterOffsets(selection:) |
Returns the character offsets of the two ends | |
setTextNodeRange(...) |
- | |
applyNativeSelection(_:) |
- | |
applySelectionRange(_:affinity:) |
||
init?(nativeSelection:) |
||
formatText(formatType:) |
~150 Lines. |