The isSelected method is defined as an instance method on the LexicalNode class (in packages/lexical/src/LexicalNode.ts). Its purpose is to determine whether "this" node is included in a given selection (or—if no selection is provided—the current selection returned by $getSelection()). In other words, it tells you whether that node is "selected" by comparing its own unique key with the list of node keys that make up the selection.
When isSelected is called, it does the following:
- It uses an optional parameter (a BaseSelection instance). If none is provided, it gets the current selection via $getSelection().
- It calls the selection's getNodes() method to obtain an array of nodes that are included in the selection.
- Then it checks whether any of these nodes has the same __key as "this" node.
- For text nodes these simple criteria are enough. For non‐text nodes (for example, inline decorators or inline elements) the function performs an extra check. In that case, if the selection is an "element" selection and collapsed (or if the selection's anchor/focus fall right at the boundary), it may decide that even though the node's key is found in the selection list, the node should not be considered "selected" in the context of formatting or deletion. This extra branch is intended to avoid unwanted side effects when, for example, a decorator node (like an image or an emoji inserted as a decorator) is next to the caret.
Below is a simplified excerpt illustrating its logic:
// 1. If no selection is passed, try to get the current selection.
const targetSelection = selection || $getSelection();
if (targetSelection == null) return false;
// 2. Check if this node's key exists in the selection's nodes.
const isSelected = targetSelection.getNodes().some(n => n.__key === this.__key);
// 3. If the node is a text node, return the result.
if ($isTextNode(this)) return isSelected;
// 4. For non-text nodes, add extra logic for inline images/decorators:
if (/* selection is an element range */) { … }
return isSelected;
- It is defined on the LexicalNode base class.
- No evidence was found in the code that any subclass (for instance, TextNode, ElementNode, or DecoratorNode) overrides isSelected. They all simply inherit it from LexicalNode.
It is used in many parts of Lexical's selection and editing logic. Here are a few examples:
- When retrieving the "selected" nodes (for instance, inside BaseSelection.getNodes() or functions like $appendNodesToJSON), the code iterates over nodes to include only those that are "selected" by calling each node's isSelected method.
- During formatting, deletion, or clipboard operations the editor may need to check for each node whether it falls in the current selection. For example, when a user applies a style change or deletes text, the selection must be computed based on which nodes are selected.
- The method helps ensure that even if the DOM's native selection (or some computed selection) is "fuzzy" at the boundaries, the Lexical selection logic only acts on the nodes that truly belong to that selection. This is particularly important for inline decorators and when dealing with "element"–type points vs. "text"–type points.
Method Name | isSelected |
---|---|
Defined on | LexicalNode (packages/lexical/src/LexicalNode.ts) |
Input Parameter(s) | An optional selection (BaseSelection or null) |
What it Does | - Gets the nodes included in the selection - Looks for this node's unique key among them - For non-text nodes, applies extra logic |
Return Value | Boolean – true if "this" node is selected; false otherwise |
Overridden in | It is not overridden by subclasses; all use the base implementation |
Used for | - Determining what nodes are part of a selection and should be affected by formatting, deletion, copy/paste, etc. |
Below is a simple diagram that shows how isSelected interacts with the selection system:
┌─────────────────────────────┐
│ LexicalEditor │
│ (Global Editor State) │
└─────────────┬───────────────┘
│
▼
┌─────────────────────────────┐
│ BaseSelection │
│ (e.g. RangeSelection) │
│ │
│ getNodes() returns List │◄─────────────┐
└─────────────┬───────────────┘ │
│ │
Contains │
│ │
▼ │
┌─────────────────────────────┐ │
│ LexicalNode (A Node) │ │
│ (has __key property) │ │
└─────────────────────────────┘ │
│ │
│ method isSelected() │
│ (checks: is __key in getNodes()) │
▼ │
Returns TRUE if selected │
│ │
└──────────────────────────────┘
In summary, isSelected is the core method used to check if a node is part of the selection. It is defined on the LexicalNode base class and is inherited by all concrete node types (TextNode, ElementNode, DecoratorNode). It compares the node's unique key against the keys of nodes returned by the selection's getNodes() method and—for non-text nodes—adds extra conditions for inline cases. It is used throughout the editing and update routines for determining what content is "selected" (for operations like formatting, deletion, or clipboard copying).
Let me know if you'd like further details or diagrams on any part of this process!
Below is an explanation of how a Lexical Selection's "nodes" are calculated. Similar to how isSelected works by checking whether the current node's key appears in the list returned from the selection, much of the heavy lifting for determining what is "selected" is performed by methods on the Selection object itself—most notably the getNodes() method on a RangeSelection (and similarly in NodeSelection).
Lexical (the rich text editor) represents the current selection as a Selection object that can either be a RangeSelection (for text selections) or a NodeSelection (for selecting discrete nodes). When you want the list of nodes that the Selection "covers", you call getNodes(). For a RangeSelection this involves:
- Examining the anchor and focus points (which denote the start and the end of the selection).
- Resolving those points into specific Lexical nodes that are "hit" by the selection.
- Traversing the tree from the first node (derived from the "earlier" of the two points) to the last node (from the "later" point) by using helper methods such as getNodesBetween() on LexicalNode.
- Applying some heuristics to "trim" any extra nodes that might have been included due to the fact that when the selection is made on an element boundary the raw offsets may overselect some non-intended nodes.
In contrast, a NodeSelection stores an explicit set of node keys. In that case, getNodes() simply retrieves each node matching one of those keys.
Below is a simplified summary of what happens in RangeSelection.getNodes():
-
Cache Check:
- If _cachedNodes exists (and the selection isn't "dirty"), then return it immediately.
-
Resolve Boundary Points:
- The selection has two points: anchor and focus.
- The method first determines which is earlier (using anchor.isBefore) and designates that as the "start" (firstPoint) and the later one as "end" (lastPoint).
-
Converting Points to Nodes:
- If the point's node is an ElementNode, the selection usually does not pick the element itself but rather a descendant based on the offset.
- For example, if the selection is of type "element" and the offset is nonzero, firstPoint.getDescendantByIndex(offset) is used to "drill down" into the selected content.
-
Gathering Nodes:
- If the selection is collapsed (or if the resolved start and end nodes are the same) the method will return only that single node (or sometimes an empty array if the node is an element with children).
- Otherwise, the method calls firstNode.getNodesBetween(lastNode). This helper function (implemented on LexicalNode) walks from the first node in document order to the last, collecting all nodes encountered.
- There is then additional "clean up" logic that removes (or "trims") nodes that are considered "overselected" because when selecting at an element boundary the start point might have been too far down (or similarly for the end), which is why flags like overselectedFirstNode or overselectedLastNodes are used.
-
Caching and Return:
- Once calculated, the resulting node array is cached (if not in read‐only mode) and then returned.
Before getNodes() can be computed, Lexical must map the browser's native DOM selection into its own internal representation. This happens in functions such as:
- $internalResolveSelectionPoints – given a DOM anchor and focus (nodes and offsets), it resolves these into Lexical Points.
- $internalResolveSelectionPoint – for each DOM point, it finds the corresponding LexicalNode using $getNodeFromDOM and then "converts" the raw offset by checking if the underlying DOM node is a text node, an element (with child nodes), or even a decorator element.
- Once both points are resolved, they become the anchor and focus of a new RangeSelection.
A diagram of the process might look like this:
────────────────────────────────────────────
│ Native DOM Selection │
│ (anchorNode, anchorOffset, …) │
────────────────────────────────────────────
│
▼
────────────────────────────────────────────
│ $internalResolveSelectionPoints │
│ • Calls $internalResolveSelectionPoint │
│ for anchor and focus respectively. │
────────────────────────────────────────────
│
▼
────────────────────────────────────────────
│ Lexical Points (anchor, focus) │
│ (e.g., { key: 'abc123', offset: 5, │
│ type:'text'}) │
────────────────────────────────────────────
│
▼
────────────────────────────────────────────
│ New RangeSelection Object │
│ (stores anchor & focus, formatting, │
│ style) │
────────────────────────────────────────────
│
▼
────────────────────────────────────────────
│ getNodes() Method on Selection │
│ • Resolves first/last point nodes │
│ • Calls getNodesBetween() on │
│ LexicalNode │
│ • Applies heuristics to trim │
│ boundaries │
────────────────────────────────────────────
Method / Function | Purpose |
---|---|
$internalResolveSelectionPoint | Maps a DOM node+offset to a Lexical point (key,offset) |
$internalResolveSelectionPoints | Applies the above function to both anchor and focus; returns a tuple of Lexical Points |
RangeSelection.getNodes() | Using the anchor/focus of a RangeSelection, computes the list of Lexical nodes that are considered selected |
LexicalNode.getNodesBetween() | Traverses from one node to another (in document order), returning an array of nodes in between |
NodeSelection.getNodes() | For node selections, returns nodes corresponding to the stored set of selected keys |
In summary, when determining which nodes fall within the current selection, Lexical takes a multi‐step approach:
- The native DOM selection (with its raw nodes and offsets) is first converted into Lexical "points."
- These points become the anchor and focus of a new RangeSelection object.
- The RangeSelection then computes its "nodes" by using the resolved points. It identifies the starting (first) node and ending (last) node and gathers every node in between by traversing the internal document tree.
- Heuristics are applied to ensure that selection boundaries are "trimmed" correctly so that nodes inserted as decorations, inline elements, or splitting boundary cases are addressed appropriately.
- Finally, the computed array of nodes is cached (if applicable) and used by other parts of Lyndical (for example, when exporting content, clipboard operations, or formatting changes).
This deep dive into the Selection's node-calculation logic shows how the rich text editor reconciles the differences between the browser's native selection and its own internal document model, ensuring that operations like formatting, deletion, and copy/paste act only on the "true" selected nodes.
Let me know if you'd like further diagrams or deeper explanations on any specific part of this process!