2024-09-18 20:43:39 +08:00
/ * *
* Copyright ( c ) Meta Platforms , Inc . and affiliates .
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree .
*
* /
/* eslint-disable no-constant-condition */
import type { EditorConfig , LexicalEditor } from './LexicalEditor' ;
import type { BaseSelection , RangeSelection } from './LexicalSelection' ;
import type { Klass , KlassConstructor } from 'lexical' ;
import invariant from 'lexical/shared/invariant' ;
import {
$createParagraphNode ,
$isDecoratorNode ,
$isElementNode ,
$isRootNode ,
$isTextNode ,
type DecoratorNode ,
ElementNode ,
} from '.' ;
import {
$getSelection ,
$isNodeSelection ,
$isRangeSelection ,
$moveSelectionPointToEnd ,
$updateElementSelectionOnCreateDeleteNode ,
moveSelectionPointToSibling ,
} from './LexicalSelection' ;
import {
errorOnReadOnly ,
getActiveEditor ,
getActiveEditorState ,
} from './LexicalUpdates' ;
import {
$cloneWithProperties ,
$getCompositionKey ,
$getNodeByKey ,
$isRootOrShadowRoot ,
$maybeMoveChildrenSelectionToParent ,
$setCompositionKey ,
$setNodeKey ,
$setSelection ,
errorOnInsertTextNodeOnRoot ,
internalMarkNodeAsDirty ,
removeFromParent ,
} from './LexicalUtils' ;
export type NodeMap = Map < NodeKey , LexicalNode > ;
export type SerializedLexicalNode = {
type : string ;
version : number ;
} ;
export function $removeNode (
nodeToRemove : LexicalNode ,
restoreSelection : boolean ,
preserveEmptyParent? : boolean ,
) : void {
errorOnReadOnly ( ) ;
const key = nodeToRemove . __key ;
const parent = nodeToRemove . getParent ( ) ;
if ( parent === null ) {
return ;
}
const selection = $maybeMoveChildrenSelectionToParent ( nodeToRemove ) ;
let selectionMoved = false ;
if ( $isRangeSelection ( selection ) && restoreSelection ) {
const anchor = selection . anchor ;
const focus = selection . focus ;
if ( anchor . key === key ) {
moveSelectionPointToSibling (
anchor ,
nodeToRemove ,
parent ,
nodeToRemove . getPreviousSibling ( ) ,
nodeToRemove . getNextSibling ( ) ,
) ;
selectionMoved = true ;
}
if ( focus . key === key ) {
moveSelectionPointToSibling (
focus ,
nodeToRemove ,
parent ,
nodeToRemove . getPreviousSibling ( ) ,
nodeToRemove . getNextSibling ( ) ,
) ;
selectionMoved = true ;
}
} else if (
$isNodeSelection ( selection ) &&
restoreSelection &&
nodeToRemove . isSelected ( )
) {
nodeToRemove . selectPrevious ( ) ;
}
if ( $isRangeSelection ( selection ) && restoreSelection && ! selectionMoved ) {
// Doing this is O(n) so lets avoid it unless we need to do it
const index = nodeToRemove . getIndexWithinParent ( ) ;
removeFromParent ( nodeToRemove ) ;
$updateElementSelectionOnCreateDeleteNode ( selection , parent , index , - 1 ) ;
} else {
removeFromParent ( nodeToRemove ) ;
}
if (
! preserveEmptyParent &&
! $isRootOrShadowRoot ( parent ) &&
! parent . canBeEmpty ( ) &&
parent . isEmpty ( )
) {
$removeNode ( parent , restoreSelection ) ;
}
if ( restoreSelection && $isRootNode ( parent ) && parent . isEmpty ( ) ) {
parent . selectEnd ( ) ;
}
}
export type DOMConversion < T extends HTMLElement = HTMLElement > = {
conversion : DOMConversionFn < T > ;
priority? : 0 | 1 | 2 | 3 | 4 ;
} ;
export type DOMConversionFn < T extends HTMLElement = HTMLElement > = (
element : T ,
) = > DOMConversionOutput | null ;
export type DOMChildConversion = (
lexicalNode : LexicalNode ,
parentLexicalNode : LexicalNode | null | undefined ,
) = > LexicalNode | null | undefined ;
export type DOMConversionMap < T extends HTMLElement = HTMLElement > = Record <
NodeName ,
( node : T ) = > DOMConversion < T > | null
> ;
type NodeName = string ;
2024-12-16 01:11:02 +08:00
/ * *
* Output for a DOM conversion .
* Node can be set to 'ignore' to ignore the conversion and handling of the DOMNode
* including all its children .
* /
2024-09-18 20:43:39 +08:00
export type DOMConversionOutput = {
after ? : ( childLexicalNodes : Array < LexicalNode > ) = > Array < LexicalNode > ;
forChild? : DOMChildConversion ;
2024-12-16 01:11:02 +08:00
node : null | LexicalNode | Array < LexicalNode > | 'ignore' ;
2024-09-18 20:43:39 +08:00
} ;
export type DOMExportOutputMap = Map <
Klass < LexicalNode > ,
( editor : LexicalEditor , target : LexicalNode ) = > DOMExportOutput
> ;
export type DOMExportOutput = {
after ? : (
generatedElement : HTMLElement | Text | null | undefined ,
) = > HTMLElement | Text | null | undefined ;
element : HTMLElement | Text | null ;
} ;
export type NodeKey = string ;
export class LexicalNode {
// Allow us to look up the type including static props
[ 'constructor' ] ! : KlassConstructor < typeof LexicalNode > ;
/** @internal */
__type : string ;
/** @internal */
//@ts-ignore We set the key in the constructor.
__key : string ;
/** @internal */
__parent : null | NodeKey ;
/** @internal */
__prev : null | NodeKey ;
/** @internal */
__next : null | NodeKey ;
// Flow doesn't support abstract classes unfortunately, so we can't _force_
// subclasses of Node to implement statics. All subclasses of Node should have
// a static getType and clone method though. We define getType and clone here so we can call it
// on any Node, and we throw this error by default since the subclass should provide
// their own implementation.
/ * *
* Returns the string type of this node . Every node must
* implement this and it MUST BE UNIQUE amongst nodes registered
* on the editor .
*
* /
static getType ( ) : string {
invariant (
false ,
'LexicalNode: Node %s does not implement .getType().' ,
this . name ,
) ;
}
/ * *
* Clones this node , creating a new node with a different key
* and adding it to the EditorState ( but not attaching it anywhere ! ) . All nodes must
* implement this method .
*
* /
static clone ( _data : unknown ) : LexicalNode {
invariant (
false ,
'LexicalNode: Node %s does not implement .clone().' ,
this . name ,
) ;
}
/ * *
* Perform any state updates on the clone of prevNode that are not already
* handled by the constructor call in the static clone method . If you have
* state to update in your clone that is not handled directly by the
* constructor , it is advisable to override this method but it is required
* to include a call to ` super.afterCloneFrom(prevNode) ` in your
* implementation . This is only intended to be called by
* { @link $cloneWithProperties } function or via a super call .
*
* @example
* ` ` ` ts
* class ClassesTextNode extends TextNode {
* // Not shown: static getType, static importJSON, exportJSON, createDOM, updateDOM
* __classes = new Set < string > ( ) ;
* static clone ( node : ClassesTextNode ) : ClassesTextNode {
* // The inherited TextNode constructor is used here, so
* // classes is not set by this method.
* return new ClassesTextNode ( node . __text , node . __key ) ;
* }
* afterCloneFrom ( node : this ) : void {
* // This calls TextNode.afterCloneFrom and LexicalNode.afterCloneFrom
* // for necessary state updates
* super . afterCloneFrom ( node ) ;
* this . __addClasses ( node . __classes ) ;
* }
* // This method is a private implementation detail, it is not
* // suitable for the public API because it does not call getWritable
* __addClasses ( classNames : Iterable < string > ) : this {
* for ( const className of classNames ) {
* this . __classes . add ( className ) ;
* }
* return this ;
* }
* addClass ( . . . classNames : string [ ] ) : this {
* return this . getWritable ( ) . __addClasses ( classNames ) ;
* }
* removeClass ( . . . classNames : string [ ] ) : this {
* const node = this . getWritable ( ) ;
* for ( const className of classNames ) {
* this . __classes . delete ( className ) ;
* }
* return this ;
* }
* getClasses ( ) : Set < string > {
* return this . getLatest ( ) . __classes ;
* }
* }
* ` ` `
*
* /
afterCloneFrom ( prevNode : this ) {
this . __parent = prevNode . __parent ;
this . __next = prevNode . __next ;
this . __prev = prevNode . __prev ;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
static importDOM ? : ( ) = > DOMConversionMap < any > | null ;
constructor ( key? : NodeKey ) {
this . __type = this . constructor . getType ( ) ;
this . __parent = null ;
this . __prev = null ;
this . __next = null ;
$setNodeKey ( this , key ) ;
if ( __DEV__ ) {
if ( this . __type !== 'root' ) {
errorOnReadOnly ( ) ;
errorOnTypeKlassMismatch ( this . __type , this . constructor ) ;
}
}
}
// Getters and Traversers
/ * *
* Returns the string type of this node .
* /
getType ( ) : string {
return this . __type ;
}
isInline ( ) : boolean {
invariant (
false ,
'LexicalNode: Node %s does not implement .isInline().' ,
this . constructor . name ,
) ;
}
/ * *
* Returns true if there is a path between this node and the RootNode , false otherwise .
* This is a way of determining if the node is "attached" EditorState . Unattached nodes
* won ' t be reconciled and will ultimatelt be cleaned up by the Lexical GC .
* /
isAttached ( ) : boolean {
let nodeKey : string | null = this . __key ;
while ( nodeKey !== null ) {
if ( nodeKey === 'root' ) {
return true ;
}
const node : LexicalNode | null = $getNodeByKey ( nodeKey ) ;
if ( node === null ) {
break ;
}
nodeKey = node . __parent ;
}
return false ;
}
/ * *
* Returns true if this node is contained within the provided Selection . , false otherwise .
* Relies on the algorithms implemented in { @link BaseSelection . getNodes } to determine
* what ' s included .
*
* @param selection - The selection that we want to determine if the node is in .
* /
isSelected ( selection? : null | BaseSelection ) : boolean {
const targetSelection = selection || $getSelection ( ) ;
if ( targetSelection == null ) {
return false ;
}
const isSelected = targetSelection
. getNodes ( )
. some ( ( n ) = > n . __key === this . __key ) ;
if ( $isTextNode ( this ) ) {
return isSelected ;
}
// For inline images inside of element nodes.
// Without this change the image will be selected if the cursor is before or after it.
const isElementRangeSelection =
$isRangeSelection ( targetSelection ) &&
targetSelection . anchor . type === 'element' &&
targetSelection . focus . type === 'element' ;
if ( isElementRangeSelection ) {
if ( targetSelection . isCollapsed ( ) ) {
return false ;
}
const parentNode = this . getParent ( ) ;
if ( $isDecoratorNode ( this ) && this . isInline ( ) && parentNode ) {
const firstPoint = targetSelection . isBackward ( )
? targetSelection . focus
: targetSelection . anchor ;
const firstElement = firstPoint . getNode ( ) as ElementNode ;
if (
firstPoint . offset === firstElement . getChildrenSize ( ) &&
firstElement . is ( parentNode ) &&
firstElement . getLastChildOrThrow ( ) . is ( this )
) {
return false ;
}
}
}
return isSelected ;
}
/ * *
* Returns this nodes key .
* /
getKey ( ) : NodeKey {
// Key is stable between copies
return this . __key ;
}
/ * *
* Returns the zero - based index of this node within the parent .
* /
getIndexWithinParent ( ) : number {
const parent = this . getParent ( ) ;
if ( parent === null ) {
return - 1 ;
}
let node = parent . getFirstChild ( ) ;
let index = 0 ;
while ( node !== null ) {
if ( this . is ( node ) ) {
return index ;
}
index ++ ;
node = node . getNextSibling ( ) ;
}
return - 1 ;
}
/ * *
* Returns the parent of this node , or null if none is found .
* /
getParent < T extends ElementNode > ( ) : T | null {
const parent = this . getLatest ( ) . __parent ;
if ( parent === null ) {
return null ;
}
return $getNodeByKey < T > ( parent ) ;
}
/ * *
* Returns the parent of this node , or throws if none is found .
* /
getParentOrThrow < T extends ElementNode > ( ) : T {
const parent = this . getParent < T > ( ) ;
if ( parent === null ) {
invariant ( false , 'Expected node %s to have a parent.' , this . __key ) ;
}
return parent ;
}
/ * *
* Returns the highest ( in the EditorState tree )
* non - root ancestor of this node , or null if none is found . See { @link lexical ! $isRootOrShadowRoot }
* for more information on which Elements comprise "roots" .
* /
getTopLevelElement ( ) : ElementNode | DecoratorNode < unknown > | null {
let node : ElementNode | this | null = this ;
while ( node !== null ) {
const parent : ElementNode | null = node . getParent ( ) ;
if ( $isRootOrShadowRoot ( parent ) ) {
invariant (
$isElementNode ( node ) || ( node === this && $isDecoratorNode ( node ) ) ,
'Children of root nodes must be elements or decorators' ,
) ;
return node ;
}
node = parent ;
}
return null ;
}
/ * *
* Returns the highest ( in the EditorState tree )
* non - root ancestor of this node , or throws if none is found . See { @link lexical ! $isRootOrShadowRoot }
* for more information on which Elements comprise "roots" .
* /
getTopLevelElementOrThrow ( ) : ElementNode | DecoratorNode < unknown > {
const parent = this . getTopLevelElement ( ) ;
if ( parent === null ) {
invariant (
false ,
'Expected node %s to have a top parent element.' ,
this . __key ,
) ;
}
return parent ;
}
/ * *
* Returns a list of the every ancestor of this node ,
* all the way up to the RootNode .
*
* /
getParents ( ) : Array < ElementNode > {
const parents : Array < ElementNode > = [ ] ;
let node = this . getParent ( ) ;
while ( node !== null ) {
parents . push ( node ) ;
node = node . getParent ( ) ;
}
return parents ;
}
/ * *
* Returns a list of the keys of every ancestor of this node ,
* all the way up to the RootNode .
*
* /
getParentKeys ( ) : Array < NodeKey > {
const parents = [ ] ;
let node = this . getParent ( ) ;
while ( node !== null ) {
parents . push ( node . __key ) ;
node = node . getParent ( ) ;
}
return parents ;
}
/ * *
* Returns the "previous" siblings - that is , the node that comes
* before this one in the same parent .
*
* /
getPreviousSibling < T extends LexicalNode > ( ) : T | null {
const self = this . getLatest ( ) ;
const prevKey = self . __prev ;
return prevKey === null ? null : $getNodeByKey < T > ( prevKey ) ;
}
/ * *
* Returns the "previous" siblings - that is , the nodes that come between
* this one and the first child of it ' s parent , inclusive .
*
* /
getPreviousSiblings < T extends LexicalNode > ( ) : Array < T > {
const siblings : Array < T > = [ ] ;
const parent = this . getParent ( ) ;
if ( parent === null ) {
return siblings ;
}
let node : null | T = parent . getFirstChild ( ) ;
while ( node !== null ) {
if ( node . is ( this ) ) {
break ;
}
siblings . push ( node ) ;
node = node . getNextSibling ( ) ;
}
return siblings ;
}
/ * *
* Returns the "next" siblings - that is , the node that comes
* after this one in the same parent
*
* /
getNextSibling < T extends LexicalNode > ( ) : T | null {
const self = this . getLatest ( ) ;
const nextKey = self . __next ;
return nextKey === null ? null : $getNodeByKey < T > ( nextKey ) ;
}
/ * *
* Returns all "next" siblings - that is , the nodes that come between this
* one and the last child of it ' s parent , inclusive .
*
* /
getNextSiblings < T extends LexicalNode > ( ) : Array < T > {
const siblings : Array < T > = [ ] ;
let node : null | T = this . getNextSibling ( ) ;
while ( node !== null ) {
siblings . push ( node ) ;
node = node . getNextSibling ( ) ;
}
return siblings ;
}
/ * *
* Returns the closest common ancestor of this node and the provided one or null
* if one cannot be found .
*
* @param node - the other node to find the common ancestor of .
* /
getCommonAncestor < T extends ElementNode = ElementNode > (
node : LexicalNode ,
) : T | null {
const a = this . getParents ( ) ;
const b = node . getParents ( ) ;
if ( $isElementNode ( this ) ) {
a . unshift ( this ) ;
}
if ( $isElementNode ( node ) ) {
b . unshift ( node ) ;
}
const aLength = a . length ;
const bLength = b . length ;
if ( aLength === 0 || bLength === 0 || a [ aLength - 1 ] !== b [ bLength - 1 ] ) {
return null ;
}
const bSet = new Set ( b ) ;
for ( let i = 0 ; i < aLength ; i ++ ) {
const ancestor = a [ i ] as T ;
if ( bSet . has ( ancestor ) ) {
return ancestor ;
}
}
return null ;
}
/ * *
* Returns true if the provided node is the exact same one as this node , from Lexical ' s perspective .
* Always use this instead of referential equality .
*
* @param object - the node to perform the equality comparison on .
* /
is ( object : LexicalNode | null | undefined ) : boolean {
if ( object == null ) {
return false ;
}
return this . __key === object . __key ;
}
/ * *
* Returns true if this node logical precedes the target node in the editor state .
*
* @param targetNode - the node we 're testing to see if it' s after this one .
* /
isBefore ( targetNode : LexicalNode ) : boolean {
if ( this === targetNode ) {
return false ;
}
if ( targetNode . isParentOf ( this ) ) {
return true ;
}
if ( this . isParentOf ( targetNode ) ) {
return false ;
}
const commonAncestor = this . getCommonAncestor ( targetNode ) ;
let indexA = 0 ;
let indexB = 0 ;
let node : this | ElementNode | LexicalNode = this ;
while ( true ) {
const parent : ElementNode = node . getParentOrThrow ( ) ;
if ( parent === commonAncestor ) {
indexA = node . getIndexWithinParent ( ) ;
break ;
}
node = parent ;
}
node = targetNode ;
while ( true ) {
const parent : ElementNode = node . getParentOrThrow ( ) ;
if ( parent === commonAncestor ) {
indexB = node . getIndexWithinParent ( ) ;
break ;
}
node = parent ;
}
return indexA < indexB ;
}
/ * *
* Returns true if this node is the parent of the target node , false otherwise .
*
* @param targetNode - the would - be child node .
* /
isParentOf ( targetNode : LexicalNode ) : boolean {
const key = this . __key ;
if ( key === targetNode . __key ) {
return false ;
}
let node : ElementNode | LexicalNode | null = targetNode ;
while ( node !== null ) {
if ( node . __key === key ) {
return true ;
}
node = node . getParent ( ) ;
}
return false ;
}
// TO-DO: this function can be simplified a lot
/ * *
* Returns a list of nodes that are between this node and
* the target node in the EditorState .
*
* @param targetNode - the node that marks the other end of the range of nodes to be returned .
* /
getNodesBetween ( targetNode : LexicalNode ) : Array < LexicalNode > {
const isBefore = this . isBefore ( targetNode ) ;
const nodes = [ ] ;
const visited = new Set ( ) ;
let node : LexicalNode | this | null = this ;
while ( true ) {
if ( node === null ) {
break ;
}
const key = node . __key ;
if ( ! visited . has ( key ) ) {
visited . add ( key ) ;
nodes . push ( node ) ;
}
if ( node === targetNode ) {
break ;
}
const child : LexicalNode | null = $isElementNode ( node )
? isBefore
? node . getFirstChild ( )
: node . getLastChild ( )
: null ;
if ( child !== null ) {
node = child ;
continue ;
}
const nextSibling : LexicalNode | null = isBefore
? node . getNextSibling ( )
: node . getPreviousSibling ( ) ;
if ( nextSibling !== null ) {
node = nextSibling ;
continue ;
}
const parent : LexicalNode | null = node . getParentOrThrow ( ) ;
if ( ! visited . has ( parent . __key ) ) {
nodes . push ( parent ) ;
}
if ( parent === targetNode ) {
break ;
}
let parentSibling = null ;
let ancestor : LexicalNode | null = parent ;
do {
if ( ancestor === null ) {
invariant ( false , 'getNodesBetween: ancestor is null' ) ;
}
parentSibling = isBefore
? ancestor . getNextSibling ( )
: ancestor . getPreviousSibling ( ) ;
ancestor = ancestor . getParent ( ) ;
if ( ancestor !== null ) {
if ( parentSibling === null && ! visited . has ( ancestor . __key ) ) {
nodes . push ( ancestor ) ;
}
} else {
break ;
}
} while ( parentSibling === null ) ;
node = parentSibling ;
}
if ( ! isBefore ) {
nodes . reverse ( ) ;
}
return nodes ;
}
/ * *
* Returns true if this node has been marked dirty during this update cycle .
*
* /
isDirty ( ) : boolean {
const editor = getActiveEditor ( ) ;
const dirtyLeaves = editor . _dirtyLeaves ;
return dirtyLeaves !== null && dirtyLeaves . has ( this . __key ) ;
}
/ * *
* Returns the latest version of the node from the active EditorState .
* This is used to avoid getting values from stale node references .
*
* /
getLatest ( ) : this {
const latest = $getNodeByKey < this > ( this . __key ) ;
if ( latest === null ) {
invariant (
false ,
'Lexical node does not exist in active editor state. Avoid using the same node references between nested closures from editorState.read/editor.update.' ,
) ;
}
return latest ;
}
/ * *
* Returns a mutable version of the node using { @link $cloneWithProperties }
* if necessary . Will throw an error if called outside of a Lexical Editor
* { @link LexicalEditor . update } callback .
*
* /
getWritable ( ) : this {
errorOnReadOnly ( ) ;
const editorState = getActiveEditorState ( ) ;
const editor = getActiveEditor ( ) ;
const nodeMap = editorState . _nodeMap ;
const key = this . __key ;
// Ensure we get the latest node from pending state
const latestNode = this . getLatest ( ) ;
const cloneNotNeeded = editor . _cloneNotNeeded ;
const selection = $getSelection ( ) ;
if ( selection !== null ) {
selection . setCachedNodes ( null ) ;
}
if ( cloneNotNeeded . has ( key ) ) {
// Transforms clear the dirty node set on each iteration to keep track on newly dirty nodes
internalMarkNodeAsDirty ( latestNode ) ;
return latestNode ;
}
const mutableNode = $cloneWithProperties ( latestNode ) ;
cloneNotNeeded . add ( key ) ;
internalMarkNodeAsDirty ( mutableNode ) ;
// Update reference in node map
nodeMap . set ( key , mutableNode ) ;
return mutableNode ;
}
/ * *
* Returns the text content of the node . Override this for
* custom nodes that should have a representation in plain text
* format ( for copy + paste , for example )
*
* /
getTextContent ( ) : string {
return '' ;
}
/ * *
* Returns the length of the string produced by calling getTextContent on this node .
*
* /
getTextContentSize ( ) : number {
return this . getTextContent ( ) . length ;
}
// View
/ * *
* Called during the reconciliation process to determine which nodes
* to insert into the DOM for this Lexical Node .
*
* This method must return exactly one HTMLElement . Nested elements are not supported .
*
* Do not attempt to update the Lexical EditorState during this phase of the update lifecyle .
*
* @param _config - allows access to things like the EditorTheme ( to apply classes ) during reconciliation .
* @param _editor - allows access to the editor for context during reconciliation .
*
* * /
createDOM ( _config : EditorConfig , _editor : LexicalEditor ) : HTMLElement {
invariant ( false , 'createDOM: base method not extended' ) ;
}
/ * *
* Called when a node changes and should update the DOM
* in whatever way is necessary to make it align with any changes that might
* have happened during the update .
*
* Returning "true" here will cause lexical to unmount and recreate the DOM node
* ( by calling createDOM ) . You would need to do this if the element tag changes ,
* for instance .
*
* * /
updateDOM (
_prevNode : unknown ,
_dom : HTMLElement ,
_config : EditorConfig ,
) : boolean {
invariant ( false , 'updateDOM: base method not extended' ) ;
}
/ * *
* Controls how the this node is serialized to HTML . This is important for
* copy and paste between Lexical and non - Lexical editors , or Lexical editors with different namespaces ,
* in which case the primary transfer format is HTML . It 's also important if you' re serializing
* to HTML for any other reason via { @link @lexical / html ! $generateHtmlFromNodes } . You could
* also use this method to build your own HTML renderer .
*
* * /
exportDOM ( editor : LexicalEditor ) : DOMExportOutput {
const element = this . createDOM ( editor . _config , editor ) ;
return { element } ;
}
/ * *
* Controls how the this node is serialized to JSON . This is important for
* copy and paste between Lexical editors sharing the same namespace . It ' s also important
* if you ' re serializing to JSON for persistent storage somewhere .
* See [ Serialization & Deserialization ] ( https : //lexical.dev/docs/concepts/serialization#lexical---html).
*
* * /
exportJSON ( ) : SerializedLexicalNode {
invariant ( false , 'exportJSON: base method not extended' ) ;
}
/ * *
* Controls how the this node is deserialized from JSON . This is usually boilerplate ,
* but provides an abstraction between the node implementation and serialized interface that can
* be important if you ever make breaking changes to a node schema ( by adding or removing properties ) .
* See [ Serialization & Deserialization ] ( https : //lexical.dev/docs/concepts/serialization#lexical---html).
*
* * /
static importJSON ( _serializedNode : SerializedLexicalNode ) : LexicalNode {
invariant (
false ,
'LexicalNode: Node %s does not implement .importJSON().' ,
this . name ,
) ;
}
/ * *
* @experimental
*
* Registers the returned function as a transform on the node during
* Editor initialization . Most such use cases should be addressed via
* the { @link LexicalEditor . registerNodeTransform } API .
*
* Experimental - use at your own risk .
* /
static transform ( ) : ( ( node : LexicalNode ) = > void ) | null {
return null ;
}
// Setters and mutators
/ * *
* Removes this LexicalNode from the EditorState . If the node isn ' t re - inserted
* somewhere , the Lexical garbage collector will eventually clean it up .
*
* @param preserveEmptyParent - If falsy , the node ' s parent will be removed if
* it ' s empty after the removal operation . This is the default behavior , subject to
* other node heuristics such as { @link ElementNode # canBeEmpty }
* * /
remove ( preserveEmptyParent? : boolean ) : void {
$removeNode ( this , true , preserveEmptyParent ) ;
}
/ * *
* Replaces this LexicalNode with the provided node , optionally transferring the children
* of the replaced node to the replacing node .
*
* @param replaceWith - The node to replace this one with .
* @param includeChildren - Whether or not to transfer the children of this node to the replacing node .
* * /
replace < N extends LexicalNode > ( replaceWith : N , includeChildren? : boolean ) : N {
errorOnReadOnly ( ) ;
let selection = $getSelection ( ) ;
if ( selection !== null ) {
selection = selection . clone ( ) ;
}
errorOnInsertTextNodeOnRoot ( this , replaceWith ) ;
const self = this . getLatest ( ) ;
const toReplaceKey = this . __key ;
const key = replaceWith . __key ;
const writableReplaceWith = replaceWith . getWritable ( ) ;
const writableParent = this . getParentOrThrow ( ) . getWritable ( ) ;
const size = writableParent . __size ;
removeFromParent ( writableReplaceWith ) ;
const prevSibling = self . getPreviousSibling ( ) ;
const nextSibling = self . getNextSibling ( ) ;
const prevKey = self . __prev ;
const nextKey = self . __next ;
const parentKey = self . __parent ;
$removeNode ( self , false , true ) ;
if ( prevSibling === null ) {
writableParent . __first = key ;
} else {
const writablePrevSibling = prevSibling . getWritable ( ) ;
writablePrevSibling . __next = key ;
}
writableReplaceWith . __prev = prevKey ;
if ( nextSibling === null ) {
writableParent . __last = key ;
} else {
const writableNextSibling = nextSibling . getWritable ( ) ;
writableNextSibling . __prev = key ;
}
writableReplaceWith . __next = nextKey ;
writableReplaceWith . __parent = parentKey ;
writableParent . __size = size ;
if ( includeChildren ) {
invariant (
$isElementNode ( this ) && $isElementNode ( writableReplaceWith ) ,
'includeChildren should only be true for ElementNodes' ,
) ;
this . getChildren ( ) . forEach ( ( child : LexicalNode ) = > {
writableReplaceWith . append ( child ) ;
} ) ;
}
if ( $isRangeSelection ( selection ) ) {
$setSelection ( selection ) ;
const anchor = selection . anchor ;
const focus = selection . focus ;
if ( anchor . key === toReplaceKey ) {
$moveSelectionPointToEnd ( anchor , writableReplaceWith ) ;
}
if ( focus . key === toReplaceKey ) {
$moveSelectionPointToEnd ( focus , writableReplaceWith ) ;
}
}
if ( $getCompositionKey ( ) === toReplaceKey ) {
$setCompositionKey ( key ) ;
}
return writableReplaceWith ;
}
/ * *
* Inserts a node after this LexicalNode ( as the next sibling ) .
*
* @param nodeToInsert - The node to insert after this one .
* @param restoreSelection - Whether or not to attempt to resolve the
* selection to the appropriate place after the operation is complete .
* * /
insertAfter ( nodeToInsert : LexicalNode , restoreSelection = true ) : LexicalNode {
errorOnReadOnly ( ) ;
errorOnInsertTextNodeOnRoot ( this , nodeToInsert ) ;
const writableSelf = this . getWritable ( ) ;
const writableNodeToInsert = nodeToInsert . getWritable ( ) ;
const oldParent = writableNodeToInsert . getParent ( ) ;
const selection = $getSelection ( ) ;
let elementAnchorSelectionOnNode = false ;
let elementFocusSelectionOnNode = false ;
if ( oldParent !== null ) {
// TODO: this is O(n), can we improve?
const oldIndex = nodeToInsert . getIndexWithinParent ( ) ;
removeFromParent ( writableNodeToInsert ) ;
if ( $isRangeSelection ( selection ) ) {
const oldParentKey = oldParent . __key ;
const anchor = selection . anchor ;
const focus = selection . focus ;
elementAnchorSelectionOnNode =
anchor . type === 'element' &&
anchor . key === oldParentKey &&
anchor . offset === oldIndex + 1 ;
elementFocusSelectionOnNode =
focus . type === 'element' &&
focus . key === oldParentKey &&
focus . offset === oldIndex + 1 ;
}
}
const nextSibling = this . getNextSibling ( ) ;
const writableParent = this . getParentOrThrow ( ) . getWritable ( ) ;
const insertKey = writableNodeToInsert . __key ;
const nextKey = writableSelf . __next ;
if ( nextSibling === null ) {
writableParent . __last = insertKey ;
} else {
const writableNextSibling = nextSibling . getWritable ( ) ;
writableNextSibling . __prev = insertKey ;
}
writableParent . __size ++ ;
writableSelf . __next = insertKey ;
writableNodeToInsert . __next = nextKey ;
writableNodeToInsert . __prev = writableSelf . __key ;
writableNodeToInsert . __parent = writableSelf . __parent ;
if ( restoreSelection && $isRangeSelection ( selection ) ) {
const index = this . getIndexWithinParent ( ) ;
$updateElementSelectionOnCreateDeleteNode (
selection ,
writableParent ,
index + 1 ,
) ;
const writableParentKey = writableParent . __key ;
if ( elementAnchorSelectionOnNode ) {
selection . anchor . set ( writableParentKey , index + 2 , 'element' ) ;
}
if ( elementFocusSelectionOnNode ) {
selection . focus . set ( writableParentKey , index + 2 , 'element' ) ;
}
}
return nodeToInsert ;
}
/ * *
* Inserts a node before this LexicalNode ( as the previous sibling ) .
*
* @param nodeToInsert - The node to insert before this one .
* @param restoreSelection - Whether or not to attempt to resolve the
* selection to the appropriate place after the operation is complete .
* * /
insertBefore (
nodeToInsert : LexicalNode ,
restoreSelection = true ,
) : LexicalNode {
errorOnReadOnly ( ) ;
errorOnInsertTextNodeOnRoot ( this , nodeToInsert ) ;
const writableSelf = this . getWritable ( ) ;
const writableNodeToInsert = nodeToInsert . getWritable ( ) ;
const insertKey = writableNodeToInsert . __key ;
removeFromParent ( writableNodeToInsert ) ;
const prevSibling = this . getPreviousSibling ( ) ;
const writableParent = this . getParentOrThrow ( ) . getWritable ( ) ;
const prevKey = writableSelf . __prev ;
// TODO: this is O(n), can we improve?
const index = this . getIndexWithinParent ( ) ;
if ( prevSibling === null ) {
writableParent . __first = insertKey ;
} else {
const writablePrevSibling = prevSibling . getWritable ( ) ;
writablePrevSibling . __next = insertKey ;
}
writableParent . __size ++ ;
writableSelf . __prev = insertKey ;
writableNodeToInsert . __prev = prevKey ;
writableNodeToInsert . __next = writableSelf . __key ;
writableNodeToInsert . __parent = writableSelf . __parent ;
const selection = $getSelection ( ) ;
if ( restoreSelection && $isRangeSelection ( selection ) ) {
const parent = this . getParentOrThrow ( ) ;
$updateElementSelectionOnCreateDeleteNode ( selection , parent , index ) ;
}
return nodeToInsert ;
}
/ * *
* Whether or not this node has a required parent . Used during copy + paste operations
* to normalize nodes that would otherwise be orphaned . For example , ListItemNodes without
* a ListNode parent or TextNodes with a ParagraphNode parent .
*
* * /
isParentRequired ( ) : boolean {
return false ;
}
/ * *
* The creation logic for any required parent . Should be implemented if { @link isParentRequired } returns true .
*
* * /
createParentElementNode ( ) : ElementNode {
return $createParagraphNode ( ) ;
}
selectStart ( ) : RangeSelection {
return this . selectPrevious ( ) ;
}
selectEnd ( ) : RangeSelection {
return this . selectNext ( 0 , 0 ) ;
}
/ * *
* Moves selection to the previous sibling of this node , at the specified offsets .
*
* @param anchorOffset - The anchor offset for selection .
* @param focusOffset - The focus offset for selection
* * /
selectPrevious ( anchorOffset? : number , focusOffset? : number ) : RangeSelection {
errorOnReadOnly ( ) ;
const prevSibling = this . getPreviousSibling ( ) ;
const parent = this . getParentOrThrow ( ) ;
if ( prevSibling === null ) {
return parent . select ( 0 , 0 ) ;
}
if ( $isElementNode ( prevSibling ) ) {
return prevSibling . select ( ) ;
} else if ( ! $isTextNode ( prevSibling ) ) {
const index = prevSibling . getIndexWithinParent ( ) + 1 ;
return parent . select ( index , index ) ;
}
return prevSibling . select ( anchorOffset , focusOffset ) ;
}
/ * *
* Moves selection to the next sibling of this node , at the specified offsets .
*
* @param anchorOffset - The anchor offset for selection .
* @param focusOffset - The focus offset for selection
* * /
selectNext ( anchorOffset? : number , focusOffset? : number ) : RangeSelection {
errorOnReadOnly ( ) ;
const nextSibling = this . getNextSibling ( ) ;
const parent = this . getParentOrThrow ( ) ;
if ( nextSibling === null ) {
return parent . select ( ) ;
}
if ( $isElementNode ( nextSibling ) ) {
return nextSibling . select ( 0 , 0 ) ;
} else if ( ! $isTextNode ( nextSibling ) ) {
const index = nextSibling . getIndexWithinParent ( ) ;
return parent . select ( index , index ) ;
}
return nextSibling . select ( anchorOffset , focusOffset ) ;
}
/ * *
* Marks a node dirty , triggering transforms and
* forcing it to be reconciled during the update cycle .
*
* * /
markDirty ( ) : void {
this . getWritable ( ) ;
}
2025-01-22 20:54:13 +08:00
/ * *
* Insert the DOM of this node into that of the parent .
* Allows this node to implement custom DOM attachment logic .
* Boolean result indicates if the insertion was handled by the function .
* A true return value prevents default insertion logic from taking place .
* /
insertDOMIntoParent ( nodeDOM : HTMLElement , parentDOM : HTMLElement ) : boolean {
return false ;
}
2024-09-18 20:43:39 +08:00
}
function errorOnTypeKlassMismatch (
type : string ,
klass : Klass < LexicalNode > ,
) : void {
const registeredNode = getActiveEditor ( ) . _nodes . get ( type ) ;
// Common error - split in its own invariant
if ( registeredNode === undefined ) {
invariant (
false ,
'Create node: Attempted to create node %s that was not configured to be used on the editor.' ,
klass . name ,
) ;
}
const editorKlass = registeredNode . klass ;
if ( editorKlass !== klass ) {
invariant (
false ,
'Create node: Type %s in node %s does not match registered node %s with the same type' ,
type ,
klass . name ,
editorKlass . name ,
) ;
}
}
/ * *
* Insert a series of nodes after this LexicalNode ( as next siblings )
*
* @param firstToInsert - The first node to insert after this one .
* @param lastToInsert - The last node to insert after this one . Must be a
* later sibling of FirstNode . If not provided , it will be its last sibling .
* /
export function insertRangeAfter (
node : LexicalNode ,
firstToInsert : LexicalNode ,
lastToInsert? : LexicalNode ,
) {
const lastToInsert2 =
lastToInsert || firstToInsert . getParentOrThrow ( ) . getLastChild ( ) ! ;
let current = firstToInsert ;
const nodesToInsert = [ firstToInsert ] ;
while ( current !== lastToInsert2 ) {
if ( ! current . getNextSibling ( ) ) {
invariant (
false ,
'insertRangeAfter: lastToInsert must be a later sibling of firstToInsert' ,
) ;
}
current = current . getNextSibling ( ) ! ;
nodesToInsert . push ( current ) ;
}
let currentNode : LexicalNode = node ;
for ( const nodeToInsert of nodesToInsert ) {
currentNode = currentNode . insertAfter ( nodeToInsert ) ;
}
}