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';
22 ELEMENT_FORMAT_TO_TYPE,
23 ELEMENT_TYPE_TO_FORMAT,
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;
45 format: ElementFormatType;
51 export type ElementFormatType =
60 // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
61 export interface ElementNode {
62 getTopLevelElement(): ElementNode | null;
63 getTopLevelElementOrThrow(): ElementNode;
67 // eslint-disable-next-line @typescript-eslint/no-unsafe-declaration-merging
68 export class ElementNode extends LexicalNode {
69 ['constructor']!: KlassConstructor<typeof ElementNode>;
71 __first: null | NodeKey;
73 __last: null | NodeKey;
83 __dir: 'ltr' | 'rtl' | null;
85 constructor(key?: NodeKey) {
96 afterCloneFrom(prevNode: this) {
97 super.afterCloneFrom(prevNode);
98 this.__first = prevNode.__first;
99 this.__last = prevNode.__last;
100 this.__size = prevNode.__size;
101 this.__indent = prevNode.__indent;
102 this.__format = prevNode.__format;
103 this.__style = prevNode.__style;
104 this.__dir = prevNode.__dir;
107 getFormat(): number {
108 const self = this.getLatest();
109 return self.__format;
111 getFormatType(): ElementFormatType {
112 const format = this.getFormat();
113 return ELEMENT_FORMAT_TO_TYPE[format] || '';
116 const self = this.getLatest();
119 getIndent(): number {
120 const self = this.getLatest();
121 return self.__indent;
123 getChildren<T extends LexicalNode>(): Array<T> {
124 const children: Array<T> = [];
125 let child: T | null = this.getFirstChild();
126 while (child !== null) {
127 children.push(child);
128 child = child.getNextSibling();
132 getChildrenKeys(): Array<NodeKey> {
133 const children: Array<NodeKey> = [];
134 let child: LexicalNode | null = this.getFirstChild();
135 while (child !== null) {
136 children.push(child.__key);
137 child = child.getNextSibling();
141 getChildrenSize(): number {
142 const self = this.getLatest();
146 return this.getChildrenSize() === 0;
149 const editor = getActiveEditor();
150 const dirtyElements = editor._dirtyElements;
151 return dirtyElements !== null && dirtyElements.has(this.__key);
153 isLastChild(): boolean {
154 const self = this.getLatest();
155 const parentLastChild = this.getParentOrThrow().getLastChild();
156 return parentLastChild !== null && parentLastChild.is(self);
158 getAllTextNodes(): Array<TextNode> {
159 const textNodes = [];
160 let child: LexicalNode | null = this.getFirstChild();
161 while (child !== null) {
162 if ($isTextNode(child)) {
163 textNodes.push(child);
165 if ($isElementNode(child)) {
166 const subChildrenNodes = child.getAllTextNodes();
167 textNodes.push(...subChildrenNodes);
169 child = child.getNextSibling();
173 getFirstDescendant<T extends LexicalNode>(): null | T {
174 let node = this.getFirstChild<T>();
175 while ($isElementNode(node)) {
176 const child = node.getFirstChild<T>();
177 if (child === null) {
184 getLastDescendant<T extends LexicalNode>(): null | T {
185 let node = this.getLastChild<T>();
186 while ($isElementNode(node)) {
187 const child = node.getLastChild<T>();
188 if (child === null) {
195 getDescendantByIndex<T extends LexicalNode>(index: number): null | T {
196 const children = this.getChildren<T>();
197 const childrenLength = children.length;
198 // For non-empty element nodes, we resolve its descendant
199 // (either a leaf node or the bottom-most element)
200 if (index >= childrenLength) {
201 const resolvedNode = children[childrenLength - 1];
203 ($isElementNode(resolvedNode) && resolvedNode.getLastDescendant()) ||
208 const resolvedNode = children[index];
210 ($isElementNode(resolvedNode) && resolvedNode.getFirstDescendant()) ||
215 getFirstChild<T extends LexicalNode>(): null | T {
216 const self = this.getLatest();
217 const firstKey = self.__first;
218 return firstKey === null ? null : $getNodeByKey<T>(firstKey);
220 getFirstChildOrThrow<T extends LexicalNode>(): T {
221 const firstChild = this.getFirstChild<T>();
222 if (firstChild === null) {
223 invariant(false, 'Expected node %s to have a first child.', this.__key);
227 getLastChild<T extends LexicalNode>(): null | T {
228 const self = this.getLatest();
229 const lastKey = self.__last;
230 return lastKey === null ? null : $getNodeByKey<T>(lastKey);
232 getLastChildOrThrow<T extends LexicalNode>(): T {
233 const lastChild = this.getLastChild<T>();
234 if (lastChild === null) {
235 invariant(false, 'Expected node %s to have a last child.', this.__key);
239 getChildAtIndex<T extends LexicalNode>(index: number): null | T {
240 const size = this.getChildrenSize();
243 if (index < size / 2) {
244 node = this.getFirstChild<T>();
246 while (node !== null && i <= index) {
250 node = node.getNextSibling();
255 node = this.getLastChild<T>();
257 while (node !== null && i >= index) {
261 node = node.getPreviousSibling();
266 getTextContent(): string {
267 let textContent = '';
268 const children = this.getChildren();
269 const childrenLength = children.length;
270 for (let i = 0; i < childrenLength; i++) {
271 const child = children[i];
272 textContent += child.getTextContent();
274 $isElementNode(child) &&
275 i !== childrenLength - 1 &&
278 textContent += DOUBLE_LINE_BREAK;
283 getTextContentSize(): number {
284 let textContentSize = 0;
285 const children = this.getChildren();
286 const childrenLength = children.length;
287 for (let i = 0; i < childrenLength; i++) {
288 const child = children[i];
289 textContentSize += child.getTextContentSize();
291 $isElementNode(child) &&
292 i !== childrenLength - 1 &&
295 textContentSize += DOUBLE_LINE_BREAK.length;
298 return textContentSize;
300 getDirection(): 'ltr' | 'rtl' | null {
301 const self = this.getLatest();
304 hasFormat(type: ElementFormatType): boolean {
306 const formatFlag = ELEMENT_TYPE_TO_FORMAT[type];
307 return (this.getFormat() & formatFlag) !== 0;
314 select(_anchorOffset?: number, _focusOffset?: number): RangeSelection {
316 const selection = $getSelection();
317 let anchorOffset = _anchorOffset;
318 let focusOffset = _focusOffset;
319 const childrenCount = this.getChildrenSize();
320 if (!this.canBeEmpty()) {
321 if (_anchorOffset === 0 && _focusOffset === 0) {
322 const firstChild = this.getFirstChild();
323 if ($isTextNode(firstChild) || $isElementNode(firstChild)) {
324 return firstChild.select(0, 0);
327 (_anchorOffset === undefined || _anchorOffset === childrenCount) &&
328 (_focusOffset === undefined || _focusOffset === childrenCount)
330 const lastChild = this.getLastChild();
331 if ($isTextNode(lastChild) || $isElementNode(lastChild)) {
332 return lastChild.select();
336 if (anchorOffset === undefined) {
337 anchorOffset = childrenCount;
339 if (focusOffset === undefined) {
340 focusOffset = childrenCount;
342 const key = this.__key;
343 if (!$isRangeSelection(selection)) {
344 return $internalMakeRangeSelection(
353 selection.anchor.set(key, anchorOffset, 'element');
354 selection.focus.set(key, focusOffset, 'element');
355 selection.dirty = true;
359 selectStart(): RangeSelection {
360 const firstNode = this.getFirstDescendant();
361 return firstNode ? firstNode.selectStart() : this.select();
363 selectEnd(): RangeSelection {
364 const lastNode = this.getLastDescendant();
365 return lastNode ? lastNode.selectEnd() : this.select();
368 const writableSelf = this.getWritable();
369 const children = this.getChildren();
370 children.forEach((child) => child.remove());
373 append(...nodesToAppend: LexicalNode[]): this {
374 return this.splice(this.getChildrenSize(), 0, nodesToAppend);
376 setDirection(direction: 'ltr' | 'rtl' | null): this {
377 const self = this.getWritable();
378 self.__dir = direction;
381 setFormat(type: ElementFormatType): this {
382 const self = this.getWritable();
383 self.__format = type !== '' ? ELEMENT_TYPE_TO_FORMAT[type] : 0;
386 setStyle(style: string): this {
387 const self = this.getWritable();
388 self.__style = style || '';
391 setIndent(indentLevel: number): this {
392 const self = this.getWritable();
393 self.__indent = indentLevel;
399 nodesToInsert: Array<LexicalNode>,
401 const nodesToInsertLength = nodesToInsert.length;
402 const oldSize = this.getChildrenSize();
403 const writableSelf = this.getWritable();
404 const writableSelfKey = writableSelf.__key;
405 const nodesToInsertKeys = [];
406 const nodesToRemoveKeys = [];
407 const nodeAfterRange = this.getChildAtIndex(start + deleteCount);
408 let nodeBeforeRange = null;
409 let newSize = oldSize - deleteCount + nodesToInsertLength;
412 if (start === oldSize) {
413 nodeBeforeRange = this.getLastChild();
415 const node = this.getChildAtIndex(start);
417 nodeBeforeRange = node.getPreviousSibling();
422 if (deleteCount > 0) {
424 nodeBeforeRange === null
425 ? this.getFirstChild()
426 : nodeBeforeRange.getNextSibling();
427 for (let i = 0; i < deleteCount; i++) {
428 if (nodeToDelete === null) {
429 invariant(false, 'splice: sibling not found');
431 const nextSibling = nodeToDelete.getNextSibling();
432 const nodeKeyToDelete = nodeToDelete.__key;
433 const writableNodeToDelete = nodeToDelete.getWritable();
434 removeFromParent(writableNodeToDelete);
435 nodesToRemoveKeys.push(nodeKeyToDelete);
436 nodeToDelete = nextSibling;
440 let prevNode = nodeBeforeRange;
441 for (let i = 0; i < nodesToInsertLength; i++) {
442 const nodeToInsert = nodesToInsert[i];
443 if (prevNode !== null && nodeToInsert.is(prevNode)) {
444 nodeBeforeRange = prevNode = prevNode.getPreviousSibling();
446 const writableNodeToInsert = nodeToInsert.getWritable();
447 if (writableNodeToInsert.__parent === writableSelfKey) {
450 removeFromParent(writableNodeToInsert);
451 const nodeKeyToInsert = nodeToInsert.__key;
452 if (prevNode === null) {
453 writableSelf.__first = nodeKeyToInsert;
454 writableNodeToInsert.__prev = null;
456 const writablePrevNode = prevNode.getWritable();
457 writablePrevNode.__next = nodeKeyToInsert;
458 writableNodeToInsert.__prev = writablePrevNode.__key;
460 if (nodeToInsert.__key === writableSelfKey) {
461 invariant(false, 'append: attempting to append self');
463 // Set child parent to self
464 writableNodeToInsert.__parent = writableSelfKey;
465 nodesToInsertKeys.push(nodeKeyToInsert);
466 prevNode = nodeToInsert;
469 if (start + deleteCount === oldSize) {
470 if (prevNode !== null) {
471 const writablePrevNode = prevNode.getWritable();
472 writablePrevNode.__next = null;
473 writableSelf.__last = prevNode.__key;
475 } else if (nodeAfterRange !== null) {
476 const writableNodeAfterRange = nodeAfterRange.getWritable();
477 if (prevNode !== null) {
478 const writablePrevNode = prevNode.getWritable();
479 writableNodeAfterRange.__prev = prevNode.__key;
480 writablePrevNode.__next = nodeAfterRange.__key;
482 writableNodeAfterRange.__prev = null;
486 writableSelf.__size = newSize;
488 // In case of deletion we need to adjust selection, unlink removed nodes
489 // and clean up node itself if it becomes empty. None of these needed
490 // for insertion-only cases
491 if (nodesToRemoveKeys.length) {
492 // Adjusting selection, in case node that was anchor/focus will be deleted
493 const selection = $getSelection();
494 if ($isRangeSelection(selection)) {
495 const nodesToRemoveKeySet = new Set(nodesToRemoveKeys);
496 const nodesToInsertKeySet = new Set(nodesToInsertKeys);
498 const {anchor, focus} = selection;
499 if (isPointRemoved(anchor, nodesToRemoveKeySet, nodesToInsertKeySet)) {
500 moveSelectionPointToSibling(
508 if (isPointRemoved(focus, nodesToRemoveKeySet, nodesToInsertKeySet)) {
509 moveSelectionPointToSibling(
517 // Cleanup if node can't be empty
518 if (newSize === 0 && !this.canBeEmpty() && !$isRootOrShadowRoot(this)) {
526 // JSON serialization
527 exportJSON(): SerializedElementNode {
530 direction: this.getDirection(),
531 format: this.getFormatType(),
532 indent: this.getIndent(),
537 // These are intended to be extends for specific element heuristics.
539 selection: RangeSelection,
540 restoreSelection?: boolean,
541 ): null | LexicalNode {
544 canIndent(): boolean {
548 * This method controls the behavior of a the node during backwards
549 * deletion (i.e., backspace) when selection is at the beginning of
550 * the node (offset 0)
552 collapseAtStart(selection: RangeSelection): boolean {
555 excludeFromCopy(destination?: 'clone' | 'html'): boolean {
558 /** @deprecated @internal */
559 canReplaceWith(replacement: LexicalNode): boolean {
562 /** @deprecated @internal */
563 canInsertAfter(node: LexicalNode): boolean {
566 canBeEmpty(): boolean {
569 canInsertTextBefore(): boolean {
572 canInsertTextAfter(): boolean {
575 isInline(): boolean {
578 // A shadow root is a Node that behaves like RootNode. The shadow root (and RootNode) mark the
579 // end of the hiercharchy, most implementations should treat it as there's nothing (upwards)
580 // beyond this point. For example, node.getTopLevelElement(), when performed inside a TableCellNode
581 // will return the immediate first child underneath TableCellNode instead of RootNode.
582 isShadowRoot(): boolean {
585 /** @deprecated @internal */
586 canMergeWith(node: ElementNode): boolean {
591 selection: BaseSelection | null,
592 destination: 'clone' | 'html',
598 * Determines whether this node, when empty, can merge with a first block
599 * of nodes being inserted.
601 * This method is specifically called in {@link RangeSelection.insertNodes}
602 * to determine merging behavior during nodes insertion.
605 * // In a ListItemNode or QuoteNode implementation:
606 * canMergeWhenEmpty(): true {
610 canMergeWhenEmpty(): boolean {
615 export function $isElementNode(
616 node: LexicalNode | null | undefined,
617 ): node is ElementNode {
618 return node instanceof ElementNode;
621 function isPointRemoved(
623 nodesToRemoveKeySet: Set<NodeKey>,
624 nodesToInsertKeySet: Set<NodeKey>,
626 let node: ElementNode | TextNode | null = point.getNode();
628 const nodeKey = node.__key;
629 if (nodesToRemoveKeySet.has(nodeKey) && !nodesToInsertKeySet.has(nodeKey)) {
632 node = node.getParent();