2 * Copyright (c) Meta Platforms, Inc. and affiliates.
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
9 import type {NodeKey, SerializedLexicalNode} from '../LexicalNode';
14 } from '../LexicalSelection';
15 import type {KlassConstructor, Spread} from 'lexical';
17 import invariant from 'lexical/shared/invariant';
19 import {$isTextNode, TextNode} from '../index';
24 } from '../LexicalConstants';
25 import {LexicalNode} from '../LexicalNode';
28 $internalMakeRangeSelection,
30 moveSelectionPointToSibling,
31 } from '../LexicalSelection';
32 import {errorOnReadOnly, getActiveEditor} from '../LexicalUpdates';
37 } from '../LexicalUtils';
39 export type SerializedElementNode<
40 T extends SerializedLexicalNode = SerializedLexicalNode,
44 direction: 'ltr' | 'rtl' | null;
49 export type ElementFormatType =
58 // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
59 export interface ElementNode {
60 getTopLevelElement(): ElementNode | null;
61 getTopLevelElementOrThrow(): ElementNode;
65 // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
66 export class ElementNode extends LexicalNode {
67 ['constructor']!: KlassConstructor<typeof ElementNode>;
69 __first: null | NodeKey;
71 __last: null | NodeKey;
77 __dir: 'ltr' | 'rtl' | null;
79 constructor(key?: NodeKey) {
88 afterCloneFrom(prevNode: this) {
89 super.afterCloneFrom(prevNode);
90 this.__first = prevNode.__first;
91 this.__last = prevNode.__last;
92 this.__size = prevNode.__size;
93 this.__style = prevNode.__style;
94 this.__dir = prevNode.__dir;
98 const self = this.getLatest();
101 getChildren<T extends LexicalNode>(): Array<T> {
102 const children: Array<T> = [];
103 let child: T | null = this.getFirstChild();
104 while (child !== null) {
105 children.push(child);
106 child = child.getNextSibling();
110 getChildrenKeys(): Array<NodeKey> {
111 const children: Array<NodeKey> = [];
112 let child: LexicalNode | null = this.getFirstChild();
113 while (child !== null) {
114 children.push(child.__key);
115 child = child.getNextSibling();
119 getChildrenSize(): number {
120 const self = this.getLatest();
124 return this.getChildrenSize() === 0;
127 const editor = getActiveEditor();
128 const dirtyElements = editor._dirtyElements;
129 return dirtyElements !== null && dirtyElements.has(this.__key);
131 isLastChild(): boolean {
132 const self = this.getLatest();
133 const parentLastChild = this.getParentOrThrow().getLastChild();
134 return parentLastChild !== null && parentLastChild.is(self);
136 getAllTextNodes(): Array<TextNode> {
137 const textNodes = [];
138 let child: LexicalNode | null = this.getFirstChild();
139 while (child !== null) {
140 if ($isTextNode(child)) {
141 textNodes.push(child);
143 if ($isElementNode(child)) {
144 const subChildrenNodes = child.getAllTextNodes();
145 textNodes.push(...subChildrenNodes);
147 child = child.getNextSibling();
151 getFirstDescendant<T extends LexicalNode>(): null | T {
152 let node = this.getFirstChild<T>();
153 while ($isElementNode(node)) {
154 const child = node.getFirstChild<T>();
155 if (child === null) {
162 getLastDescendant<T extends LexicalNode>(): null | T {
163 let node = this.getLastChild<T>();
164 while ($isElementNode(node)) {
165 const child = node.getLastChild<T>();
166 if (child === null) {
173 getDescendantByIndex<T extends LexicalNode>(index: number): null | T {
174 const children = this.getChildren<T>();
175 const childrenLength = children.length;
176 // For non-empty element nodes, we resolve its descendant
177 // (either a leaf node or the bottom-most element)
178 if (index >= childrenLength) {
179 const resolvedNode = children[childrenLength - 1];
181 ($isElementNode(resolvedNode) && resolvedNode.getLastDescendant()) ||
186 const resolvedNode = children[index];
188 ($isElementNode(resolvedNode) && resolvedNode.getFirstDescendant()) ||
193 getFirstChild<T extends LexicalNode>(): null | T {
194 const self = this.getLatest();
195 const firstKey = self.__first;
196 return firstKey === null ? null : $getNodeByKey<T>(firstKey);
198 getFirstChildOrThrow<T extends LexicalNode>(): T {
199 const firstChild = this.getFirstChild<T>();
200 if (firstChild === null) {
201 invariant(false, 'Expected node %s to have a first child.', this.__key);
205 getLastChild<T extends LexicalNode>(): null | T {
206 const self = this.getLatest();
207 const lastKey = self.__last;
208 return lastKey === null ? null : $getNodeByKey<T>(lastKey);
210 getLastChildOrThrow<T extends LexicalNode>(): T {
211 const lastChild = this.getLastChild<T>();
212 if (lastChild === null) {
213 invariant(false, 'Expected node %s to have a last child.', this.__key);
217 getChildAtIndex<T extends LexicalNode>(index: number): null | T {
218 const size = this.getChildrenSize();
221 if (index < size / 2) {
222 node = this.getFirstChild<T>();
224 while (node !== null && i <= index) {
228 node = node.getNextSibling();
233 node = this.getLastChild<T>();
235 while (node !== null && i >= index) {
239 node = node.getPreviousSibling();
244 getTextContent(): string {
245 let textContent = '';
246 const children = this.getChildren();
247 const childrenLength = children.length;
248 for (let i = 0; i < childrenLength; i++) {
249 const child = children[i];
250 textContent += child.getTextContent();
252 $isElementNode(child) &&
253 i !== childrenLength - 1 &&
256 textContent += DOUBLE_LINE_BREAK;
261 getTextContentSize(): number {
262 let textContentSize = 0;
263 const children = this.getChildren();
264 const childrenLength = children.length;
265 for (let i = 0; i < childrenLength; i++) {
266 const child = children[i];
267 textContentSize += child.getTextContentSize();
269 $isElementNode(child) &&
270 i !== childrenLength - 1 &&
273 textContentSize += DOUBLE_LINE_BREAK.length;
276 return textContentSize;
278 getDirection(): 'ltr' | 'rtl' | null {
279 const self = this.getLatest();
285 select(_anchorOffset?: number, _focusOffset?: number): RangeSelection {
287 const selection = $getSelection();
288 let anchorOffset = _anchorOffset;
289 let focusOffset = _focusOffset;
290 const childrenCount = this.getChildrenSize();
291 if (!this.canBeEmpty()) {
292 if (_anchorOffset === 0 && _focusOffset === 0) {
293 const firstChild = this.getFirstChild();
294 if ($isTextNode(firstChild) || $isElementNode(firstChild)) {
295 return firstChild.select(0, 0);
298 (_anchorOffset === undefined || _anchorOffset === childrenCount) &&
299 (_focusOffset === undefined || _focusOffset === childrenCount)
301 const lastChild = this.getLastChild();
302 if ($isTextNode(lastChild) || $isElementNode(lastChild)) {
303 return lastChild.select();
307 if (anchorOffset === undefined) {
308 anchorOffset = childrenCount;
310 if (focusOffset === undefined) {
311 focusOffset = childrenCount;
313 const key = this.__key;
314 if (!$isRangeSelection(selection)) {
315 return $internalMakeRangeSelection(
324 selection.anchor.set(key, anchorOffset, 'element');
325 selection.focus.set(key, focusOffset, 'element');
326 selection.dirty = true;
330 selectStart(): RangeSelection {
331 const firstNode = this.getFirstDescendant();
332 return firstNode ? firstNode.selectStart() : this.select();
334 selectEnd(): RangeSelection {
335 const lastNode = this.getLastDescendant();
336 return lastNode ? lastNode.selectEnd() : this.select();
339 const writableSelf = this.getWritable();
340 const children = this.getChildren();
341 children.forEach((child) => child.remove());
344 append(...nodesToAppend: LexicalNode[]): this {
345 return this.splice(this.getChildrenSize(), 0, nodesToAppend);
347 setDirection(direction: 'ltr' | 'rtl' | null): this {
348 const self = this.getWritable();
349 self.__dir = direction;
352 setStyle(style: string): this {
353 const self = this.getWritable();
354 self.__style = style || '';
360 nodesToInsert: Array<LexicalNode>,
362 const nodesToInsertLength = nodesToInsert.length;
363 const oldSize = this.getChildrenSize();
364 const writableSelf = this.getWritable();
365 const writableSelfKey = writableSelf.__key;
366 const nodesToInsertKeys = [];
367 const nodesToRemoveKeys = [];
368 const nodeAfterRange = this.getChildAtIndex(start + deleteCount);
369 let nodeBeforeRange = null;
370 let newSize = oldSize - deleteCount + nodesToInsertLength;
373 if (start === oldSize) {
374 nodeBeforeRange = this.getLastChild();
376 const node = this.getChildAtIndex(start);
378 nodeBeforeRange = node.getPreviousSibling();
383 if (deleteCount > 0) {
385 nodeBeforeRange === null
386 ? this.getFirstChild()
387 : nodeBeforeRange.getNextSibling();
388 for (let i = 0; i < deleteCount; i++) {
389 if (nodeToDelete === null) {
390 invariant(false, 'splice: sibling not found');
392 const nextSibling = nodeToDelete.getNextSibling();
393 const nodeKeyToDelete = nodeToDelete.__key;
394 const writableNodeToDelete = nodeToDelete.getWritable();
395 removeFromParent(writableNodeToDelete);
396 nodesToRemoveKeys.push(nodeKeyToDelete);
397 nodeToDelete = nextSibling;
401 let prevNode = nodeBeforeRange;
402 for (let i = 0; i < nodesToInsertLength; i++) {
403 const nodeToInsert = nodesToInsert[i];
404 if (prevNode !== null && nodeToInsert.is(prevNode)) {
405 nodeBeforeRange = prevNode = prevNode.getPreviousSibling();
407 const writableNodeToInsert = nodeToInsert.getWritable();
408 if (writableNodeToInsert.__parent === writableSelfKey) {
411 removeFromParent(writableNodeToInsert);
412 const nodeKeyToInsert = nodeToInsert.__key;
413 if (prevNode === null) {
414 writableSelf.__first = nodeKeyToInsert;
415 writableNodeToInsert.__prev = null;
417 const writablePrevNode = prevNode.getWritable();
418 writablePrevNode.__next = nodeKeyToInsert;
419 writableNodeToInsert.__prev = writablePrevNode.__key;
421 if (nodeToInsert.__key === writableSelfKey) {
422 invariant(false, 'append: attempting to append self');
424 // Set child parent to self
425 writableNodeToInsert.__parent = writableSelfKey;
426 nodesToInsertKeys.push(nodeKeyToInsert);
427 prevNode = nodeToInsert;
430 if (start + deleteCount === oldSize) {
431 if (prevNode !== null) {
432 const writablePrevNode = prevNode.getWritable();
433 writablePrevNode.__next = null;
434 writableSelf.__last = prevNode.__key;
436 } else if (nodeAfterRange !== null) {
437 const writableNodeAfterRange = nodeAfterRange.getWritable();
438 if (prevNode !== null) {
439 const writablePrevNode = prevNode.getWritable();
440 writableNodeAfterRange.__prev = prevNode.__key;
441 writablePrevNode.__next = nodeAfterRange.__key;
443 writableNodeAfterRange.__prev = null;
447 writableSelf.__size = newSize;
449 // In case of deletion we need to adjust selection, unlink removed nodes
450 // and clean up node itself if it becomes empty. None of these needed
451 // for insertion-only cases
452 if (nodesToRemoveKeys.length) {
453 // Adjusting selection, in case node that was anchor/focus will be deleted
454 const selection = $getSelection();
455 if ($isRangeSelection(selection)) {
456 const nodesToRemoveKeySet = new Set(nodesToRemoveKeys);
457 const nodesToInsertKeySet = new Set(nodesToInsertKeys);
459 const {anchor, focus} = selection;
460 if (isPointRemoved(anchor, nodesToRemoveKeySet, nodesToInsertKeySet)) {
461 moveSelectionPointToSibling(
469 if (isPointRemoved(focus, nodesToRemoveKeySet, nodesToInsertKeySet)) {
470 moveSelectionPointToSibling(
478 // Cleanup if node can't be empty
479 if (newSize === 0 && !this.canBeEmpty() && !$isRootOrShadowRoot(this)) {
487 // JSON serialization
488 exportJSON(): SerializedElementNode {
491 direction: this.getDirection(),
496 // These are intended to be extends for specific element heuristics.
498 selection: RangeSelection,
499 restoreSelection?: boolean,
500 ): null | LexicalNode {
503 canIndent(): boolean {
507 * This method controls the behavior of a the node during backwards
508 * deletion (i.e., backspace) when selection is at the beginning of
509 * the node (offset 0)
511 collapseAtStart(selection: RangeSelection): boolean {
514 excludeFromCopy(destination?: 'clone' | 'html'): boolean {
517 /** @deprecated @internal */
518 canReplaceWith(replacement: LexicalNode): boolean {
521 /** @deprecated @internal */
522 canInsertAfter(node: LexicalNode): boolean {
525 canBeEmpty(): boolean {
528 canInsertTextBefore(): boolean {
531 canInsertTextAfter(): boolean {
534 isInline(): boolean {
537 // A shadow root is a Node that behaves like RootNode. The shadow root (and RootNode) mark the
538 // end of the hiercharchy, most implementations should treat it as there's nothing (upwards)
539 // beyond this point. For example, node.getTopLevelElement(), when performed inside a TableCellNode
540 // will return the immediate first child underneath TableCellNode instead of RootNode.
541 isShadowRoot(): boolean {
544 /** @deprecated @internal */
545 canMergeWith(node: ElementNode): boolean {
550 selection: BaseSelection | null,
551 destination: 'clone' | 'html',
557 * Determines whether this node, when empty, can merge with a first block
558 * of nodes being inserted.
560 * This method is specifically called in {@link RangeSelection.insertNodes}
561 * to determine merging behavior during nodes insertion.
564 * // In a ListItemNode or QuoteNode implementation:
565 * canMergeWhenEmpty(): true {
569 canMergeWhenEmpty(): boolean {
574 export function $isElementNode(
575 node: LexicalNode | null | undefined,
576 ): node is ElementNode {
577 return node instanceof ElementNode;
580 function isPointRemoved(
582 nodesToRemoveKeySet: Set<NodeKey>,
583 nodesToInsertKeySet: Set<NodeKey>,
585 let node: ElementNode | TextNode | null = point.getNode();
587 const nodeKey = node.__key;
588 if (nodesToRemoveKeySet.has(nodeKey) && !nodesToInsertKeySet.has(nodeKey)) {
591 node = node.getParent();