lexical
Version:
Lexical is an extensible text editor framework that provides excellent reliability, accessible and performance.
1,515 lines (1,426 loc) • 70.7 kB
text/typescript
/**
* 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.
*
*/
import type {PROTOTYPE_CONFIG_METHOD} from './LexicalConstants';
import type {
EditorConfig,
Klass,
KlassConstructor,
LexicalEditor,
} from './LexicalEditor';
import invariant from '@lexical/internal/invariant';
import {
$createParagraphNode,
$getCommonAncestor,
$getCommonAncestorResultBranchOrder,
$isDecoratorNode,
$isElementNode,
$isRootNode,
$isTextNode,
type DecoratorNode,
type ElementNode,
type NODE_STATE_KEY,
} from '.';
import {DOMSlot} from './LexicalDOMSlot';
import {
$updateStateFromJSON,
type NodeState,
type NodeStateJSON,
type Prettify,
type RequiredNodeStateConfig,
} from './LexicalNodeState';
import {CACHED_TEXT_SIZE_KEY} from './LexicalReconciler';
import {
$getSelection,
$isNodeSelection,
$isRangeSelection,
$moveSelectionPointToEnd,
$updateElementSelectionOnCreateDeleteNode,
type BaseSelection,
moveSelectionPointToSibling,
type RangeSelection,
} from './LexicalSelection';
import {
$errorOnSlotCycleChild,
$getSlot,
$getSlotHost,
$getSlotHostKey,
$getSlotNames,
$getSlotsTextContent,
$isSlotHost,
$removeSlot,
$setSlot,
} from './LexicalSlot';
import {
errorOnReadOnly,
getActiveEditor,
getActiveEditorState,
} from './LexicalUpdates';
import {
$cloneWithProperties,
$getCompositionKey,
$getNodeByKey,
$hasAncestor,
$isRootOrShadowRoot,
$maybeMoveChildrenSelectionToParent,
$removeFromParent,
$setCompositionKey,
$setNodeKey,
$setSelection,
errorOnInsertTextNodeOnRoot,
getRegisteredNode,
getStaticNodeConfig,
getSuperclassOf,
internalMarkNodeAsDirty,
} from './LexicalUtils';
const __DEV__ = process.env.NODE_ENV !== 'production';
export type NodeMap = Map<NodeKey, LexicalNode>;
/**
* The base type for all serialized nodes
*/
export type SerializedLexicalNode = {
/** The type string used by the Node class */
type: string;
/** A numeric version for this schema, defaulting to 1, but not generally recommended for use */
version: number;
/**
* Any state persisted with the NodeState API that is not
* configured for flat storage
*/
[NODE_STATE_KEY]?: Record<string, unknown>;
/**
* Named slot subtrees keyed by slot name. Present on host nodes (an
* ElementNode or DecoratorNode that registered slots via `$setSlot`).
* The `$` prefix keeps the framework-owned key out of the namespace a
* third-party subclass may already use for its own serialized `slots`
* property (mirroring the reserved NodeState `'$'` key).
* @experimental named-slots
*/
$slots?: Record<string, SerializedLexicalNode>;
};
/**
* EXPERIMENTAL
* The configuration of a node returned by LexicalNode.$config()
*
* @example
* ```ts
* class CustomText extends TextNode {
* $config() {
* return this.config('custom-text', {extends: TextNode}};
* }
* }
* ```
*/
export interface StaticNodeConfigValue<
T extends LexicalNode,
Type extends string | symbol,
> {
/**
* The exact type of T.getType(), e.g. 'text' - the method itself must
* have a more generic 'string' type to be compatible wtih subclassing.
*
* For a concrete node this is its string `type`. An abstract base class is
* keyed in {@link BaseStaticNodeConfig} by a symbol (it has no concrete node
* `type`), so `Type` is widened to `string | symbol`; the `type` field is
* never populated for a symbol-keyed config.
*/
readonly type?: Type;
/**
* An alternative to the internal static transform() method
* that provides better type inference. If implemented this
* transform will be registered for this class and any subclass.
*/
readonly $transform?: (node: T) => void;
/**
* An alternative to the static importJSON() method
* that provides better type inference.
*/
readonly $importJSON?: (serializedNode: SerializedLexicalNode) => T;
/**
* An alternative to the static importDOM() method
*/
readonly importDOM?: DOMConversionMap;
/**
* EXPERIMENTAL
*
* An array of RequiredNodeStateConfig to initialize your node with
* its state requirements. This may be used to configure serialization of
* that state.
*
* This function will be called (at most) once per editor initialization,
* directly on your node's prototype. It must not depend on any state
* initialized in the constructor.
*
* @example
* ```ts
* const flatState = createState("flat", {parse: parseNumber});
* const nestedState = createState("nested", {parse: parseNumber});
* class MyNode extends TextNode {
* $config() {
* return this.config(
* 'my-node',
* {
* extends: TextNode,
* stateConfigs: [
* { stateConfig: flatState, flat: true},
* nestedState,
* ]
* },
* );
* }
* }
* ```
*/
readonly stateConfigs?: readonly RequiredNodeStateConfig[];
/**
* @experimental named-slots
*
* Canonical order for this host's named slots. Declared names render,
* fold, serialize, and traverse in this order; occupied names that are
* not declared follow in code-unit order. Order is derived from this
* declaration at every {@link $setSlot} (never stored), so documents
* re-canonicalize on load and concurrent collaborative slot additions
* converge to the same order on every client. The declaration is not a
* schema: undeclared names are still accepted and retained, so adding,
* reordering, or dropping entries over time is non-destructive.
*
* Declaring slots also opts the host into eager slots-map creation in
* \@lexical/yjs, which makes each name's first set merge per-entry under
* concurrency instead of racing on attribute creation.
*/
readonly slots?: readonly string[];
/**
* If specified, this must be the exact superclass of the node. It is not
* checked at compile time and it is provided automatically at runtime.
*
* You would want to specify this when you are extending a node that
* has non-trivial configuration in its $config such
* as required state. If you do not specify this, the inferred
* types for your node class might be missing some of that.
*/
readonly extends?: Klass<LexicalNode>;
}
/**
* This is the type of LexicalNode.$config() that can be
* overridden by subclasses.
*
* Concrete nodes are keyed by their string `type`. An abstract base class
* (such as ElementNode or DecoratorNode) has no concrete node `type`, so when
* it needs to declare configuration that is shared with its concrete
* subclasses (for example required {@link RequiredNodeStateConfig} state or a
* `$transform`) it is keyed instead by a well-known symbol, by convention
* `Symbol.for(<NodeClassName>)` (e.g. `Symbol.for('ElementNode')`). The
* descriptive, globally-registered symbol keeps the config easy to find in a
* debugger and can never collide with a real node `type`.
*/
export type BaseStaticNodeConfig = {
readonly [K in string | symbol]?:
| undefined
| StaticNodeConfigValue<LexicalNode, K>;
};
/**
* Used to extract the node and type from a StaticNodeConfigRecord
*/
export type StaticNodeConfig<
T extends LexicalNode,
Type extends string,
> = BaseStaticNodeConfig & {
readonly [K in Type]?: StaticNodeConfigValue<T, Type>;
};
/**
* Any StaticNodeConfigValue (for generics and collections)
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type AnyStaticNodeConfigValue = StaticNodeConfigValue<any, any>;
/**
* @internal
*
* Type-only key under which a {@link StaticNodeConfigRecord} carries the node's
* own (most-derived) `type` as a nullary accessor `() => Type`.
*
* TypeScript's type system is structural, so a node subclass that adds no
* type-distinguishable members over its base (e.g. {@link TabNode} over
* {@link TextNode}, which only overrides methods with identical signatures) is
* structurally identical to that base despite being a distinct class — the
* negative branch of an `instanceof`-style guard like `$isTabNode()` would
* otherwise collapse to `never` because TypeScript concludes the base is also
* assignable to the subclass.
*
* Encoding the type as a *function* (rather than a bare `Type`) lets the
* accessor accumulate down the `extends` chain by intersection: a subclass'
* record is `ParentRecord & {[STATIC_NODE_TYPE]: () => OwnType}`, so the key
* holds `(() => ParentType) & (() => OwnType)`. TypeScript resolves an
* intersection of call signatures as an overload set, which has two effects:
*
* - {@link GetStaticNodeType} reads `ReturnType<...>`, which selects the *last*
* overload — the most-derived own `type` — rather than a union of the chain.
* - A node stays assignable to each of its ancestors (the intersection
* satisfies every `() => AncestorType`) while an ancestor is not assignable
* to it (it lacks the `() => OwnType` signature), so guards narrow correctly
* at every level of the hierarchy — not just one.
*
* This keeps the node classes themselves nominally distinguishable along their
* real hierarchy; it is not a cross-cutting trait marker. The accessor is never
* read at runtime — `config()` does not set this key — so it is `declare`-only
* and carries no runtime cost. It is `@internal` (surfaced via the
* {@link StaticNodeTypeAccessor} interface so that inferred `$config()` return
* types remain nameable in generated declaration files) and a node never
* references it. Distinction is automatic for any node that declares its
* `extends`; a subclass adds nothing by hand.
*/
export declare const STATIC_NODE_TYPE: unique symbol;
/**
* @internal
*
* Carries a node's own `type` under the {@link STATIC_NODE_TYPE} accessor. This
* is an `interface` (rather than an inline object type) on purpose: it is never
* inlined in generated declaration files, so a subclass' `$config()` return type
* references it by name (`StaticNodeTypeAccessor<'tab'>`) and the symbol key
* stays encapsulated here rather than leaking — unnamed — into every node's
* `.d.ts`.
*/
export interface StaticNodeTypeAccessor<Type extends string> {
readonly [STATIC_NODE_TYPE]: () => Type;
}
/**
* @internal
*
* Type-only key under which a {@link StaticNodeConfigRecord} carries the node's
* own configuration (the value passed to {@link LexicalNode.config}) as a
* nullary accessor `() => Config`.
*
* This mirrors {@link STATIC_NODE_TYPE}: encoding the config as a *function*
* lets it accumulate down the `extends` chain by call-signature intersection so
* that `ReturnType<...>` resolves the most-derived own config. It exists so that
* an abstract base class keyed by a symbol — which has no string `type` to index
* the record by — can still expose its own config to the state-config
* collectors, and so that such a base's symbol-keyed `$config()` return remains
* a valid override of an accessor-bearing superclass `$config()` (it inherits
* the superclass record, hence that record's {@link STATIC_NODE_TYPE} accessor).
* Never read at runtime — `config()` does not set this key — so it is
* `declare`-only and carries no runtime cost.
*/
export declare const STATIC_NODE_CONFIG: unique symbol;
/**
* @internal
*
* Carries a node's own config under the {@link STATIC_NODE_CONFIG} accessor.
* Like {@link StaticNodeTypeAccessor} this is a named `interface` so the symbol
* key stays encapsulated and inferred `$config()` return types remain nameable
* in generated declaration files.
*/
export interface StaticNodeConfigAccessor<
Config extends AnyStaticNodeConfigValue,
> {
readonly [STATIC_NODE_CONFIG]: () => Config;
}
/**
* @internal
*
* This is the more specific type than BaseStaticNodeConfig that a subclass
* should return from $config().
*
* A node that declares its `extends` accumulates the configuration of its
* superclass and records its own `type` under {@link STATIC_NODE_TYPE} and its
* own config under {@link STATIC_NODE_CONFIG}, so that it is nominally distinct
* from — yet still assignable to — that superclass, and so the state-config
* collectors can read its own config without indexing by `type`.
*/
export type StaticNodeConfigRecord<
Type extends string,
Config extends AnyStaticNodeConfigValue,
> = Config extends {
extends: abstract new (...args: never) => infer Inst extends LexicalNode;
}
? ReturnType<Inst[typeof PROTOTYPE_CONFIG_METHOD]> & {
readonly [K in Type]?: Config;
} & StaticNodeTypeAccessor<Type> &
StaticNodeConfigAccessor<Config>
: BaseStaticNodeConfig & {readonly [K in Type]?: Config};
/**
* @internal
*
* The record returned by {@link LexicalNode.config} for an abstract base class
* keyed by a symbol. Unlike {@link StaticNodeConfigRecord} it records no string
* `type` and adds no {@link STATIC_NODE_TYPE} accessor (an abstract base has no
* concrete node type), but it does inherit its superclass record and expose its
* own config under {@link STATIC_NODE_CONFIG}. Inheriting the superclass record
* is what keeps the override valid when the superclass is a concrete node whose
* `$config()` return already carries a {@link STATIC_NODE_TYPE} accessor.
*/
export type AbstractStaticNodeConfigRecord<
Config extends AnyStaticNodeConfigValue,
> = Config extends {
extends: abstract new (...args: never) => infer Inst extends LexicalNode;
}
? ReturnType<Inst[typeof PROTOTYPE_CONFIG_METHOD]> &
StaticNodeConfigAccessor<Config>
: BaseStaticNodeConfig & StaticNodeConfigAccessor<Config>;
/**
* Extract the type from a node based on its $config
*
* @example
* ```ts
* type TextNodeType = GetStaticNodeType<TextNode>;
* // ? 'text'
* ```
*/
export type GetStaticNodeType<T extends LexicalNode> =
ReturnType<T[typeof PROTOTYPE_CONFIG_METHOD]> extends {
readonly [STATIC_NODE_TYPE]: infer Accessor extends () => string;
}
? ReturnType<Accessor>
: ReturnType<T[typeof PROTOTYPE_CONFIG_METHOD]> extends StaticNodeConfig<
T,
infer Type
>
? Type
: string;
/**
* @internal
*
* A node's own config (the value it passed to {@link LexicalNode.config}), read
* from the {@link STATIC_NODE_CONFIG} accessor, or `never` for a node whose
* `$config()` does not set that accessor (a legacy node, or one whose record
* uses the {@link BaseStaticNodeConfig} fallback). Unlike indexing the record by
* a resolved string `type`, this also resolves the own config of an abstract
* base class keyed by a symbol.
*/
export type GetStaticNodeOwnConfig<T extends LexicalNode> =
ReturnType<T[typeof PROTOTYPE_CONFIG_METHOD]> extends {
readonly [STATIC_NODE_CONFIG]: infer Accessor extends
() => AnyStaticNodeConfigValue;
}
? ReturnType<Accessor>
: never;
/**
* The most precise type we can infer for the JSON that will
* be produced by T.exportJSON().
*
* Do not use this for the return type of T.exportJSON()! It must be
* a more generic type to be compatible with subclassing.
*/
export type LexicalExportJSON<T extends LexicalNode> = Prettify<
Omit<ReturnType<T['exportJSON']>, 'type'> & {
type: GetStaticNodeType<T>;
} & NodeStateJSON<T>
>;
/**
* Omit the children, type, and version properties from the given SerializedLexicalNode definition.
*/
export type LexicalUpdateJSON<T extends SerializedLexicalNode> = Omit<
T,
'children' | 'type' | 'version'
>;
/** @internal */
export interface LexicalPrivateDOM {
__lexicalTextContent?: string | undefined | null;
/**
* @experimental named-slots. Byte length of the slot text folded slots-first
* into the front of `__lexicalTextContent` for a host element. The suffix
* fast path splices the linked-list child suffix, which lives after this
* prefix, so it strips this many leading chars to recover the child-only
* text before splicing and lets the slot fold re-prepend the slot text.
* Absent / `0` for non-host elements, so non-slot trees splice unchanged.
*/
__lexicalSlotTextLength?: number | undefined;
/**
* NodeKey of the deep first text descendant (DFS order) of this
* element, or `null` if the subtree carries no text descendants.
* Maintained alongside `__lexicalTextContent` and used by the
* suffix-incremental fast path in `$reconcileChildren` to decide in
* O(1) (via the cycle's dirty-children set) whether the prefix still
* carries the canonical first text descendant.
*/
__lexicalFirstTextKey?: NodeKey | null | undefined;
__lexicalLineBreak?: HTMLBRElement | HTMLImageElement | undefined | null;
/**
* Kind of last child recorded for this element during the previous
* reconcile, used by `$reconcileElementTerminatingLineBreak` to decide
* whether the trailing-`<br>` shape needs to change without calling
* `isInline()` on the prev-state node reference (which would resolve
* to a detached node once the last child has been removed in this
* commit). Set alongside `setManagedLineBreak` / cleared alongside
* `removeManagedLineBreak`.
*/
__lexicalLastChildKind?:
| 'line-break'
| 'decorator'
| 'empty'
| null
| undefined;
__lexicalDir?: 'ltr' | 'rtl' | null | undefined;
__lexicalUnmanaged?: boolean | undefined;
/**
* When true, the DOM subtree owns its own window selection — analogous to
* a DecoratorNode subtree. Resolution logic that would otherwise force the
* Lexical selection back onto a managed position treats the caret as
* intentional and leaves it alone. Set via the `captureSelection` option
* on {@link setDOMUnmanaged}.
*/
__lexicalCapturedSelection?: boolean | undefined;
}
export function $removeNode(
nodeToRemove: LexicalNode,
restoreSelection: boolean,
preserveEmptyParent?: boolean,
): void {
errorOnReadOnly();
const key = nodeToRemove.__key;
const parent = nodeToRemove.getParent();
if (parent === null) {
invariant(
$getSlotHostKey(nodeToRemove) === null,
'$removeNode: node %s is slotted into host %s; use removeSlot on the host instead of remove().',
key,
String($getSlotHostKey(nodeToRemove)),
);
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 &&
selection &&
$isRootNode(parent) &&
parent.isEmpty()
) {
parent.selectEnd();
}
}
export type DOMConversionProp<T extends HTMLElement> = (
node: T,
) => DOMConversion<T> | null;
export type DOMConversionPropByTagName<K extends string> = DOMConversionProp<
K extends keyof HTMLElementTagNameMap ? HTMLElementTagNameMap[K] : HTMLElement
>;
export type DOMConversionTagNameMap<K extends string> = {
[NodeName in K]?: DOMConversionPropByTagName<NodeName>;
};
/**
* An identity function that will infer the type of DOM nodes
* based on tag names to make it easier to construct a
* DOMConversionMap.
*/
export function buildImportMap<K extends string>(importMap: {
[NodeName in K]: DOMConversionPropByTagName<NodeName>;
}): DOMConversionMap {
return importMap as unknown as DOMConversionMap;
}
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,
DOMConversionProp<T>
>;
type NodeName = string;
export type DOMConversionOutput = {
after?: (childLexicalNodes: LexicalNode[]) => LexicalNode[];
forChild?: DOMChildConversion;
node: null | LexicalNode | LexicalNode[];
};
export type DOMExportOutputMap = Map<
Klass<LexicalNode>,
(editor: LexicalEditor, target: LexicalNode) => DOMExportOutput
>;
export interface DOMExportOutput {
/**
* Called after the node and all of its children are constructed, can be used
* to perform any in-place updates to the node or return something else
* entirely.
*
* @param generatedElement `element` after children are appended
* @returns The final representation of this node in the exported DOM
*/
after?: (
generatedElement: HTMLElement | DocumentFragment | Text | null | undefined,
) => HTMLElement | DocumentFragment | Text | null | undefined;
/**
* A DOM node for this lexical node, or null to skip it
*/
element: HTMLElement | DocumentFragment | Text | null;
/**
* An optional override to change how and where DOM nodes for this
* ElementNode's children are appended, particularly useful if
* this node's children are not direct ancestors.
*
* @param element The DOM of a child node to append
*/
append?: (element: HTMLElement | DocumentFragment | Text) => void;
/**
* If defined, will be used instead of `node.getChildren()` to determine
* which children to render for this LexicalNode.
*
* @returns The children to export
*/
$getChildNodes?: () => Iterable<LexicalNode>;
}
export type NodeKey = string;
const EPHEMERAL = Symbol.for('ephemeral');
/**
* @internal
* @param node any LexicalNode
* @returns true if the node was created with {@link $cloneWithPropertiesEphemeral}
*/
export function $isEphemeral(
node: LexicalNode & {readonly [EPHEMERAL]?: boolean},
): boolean {
return node[EPHEMERAL] || false;
}
/**
* @internal
* Mark this node as ephemeral, its instance always returns this
* for getLatest and getWritable. It must not be added to an EditorState.
*/
export function $markEphemeral<T extends LexicalNode>(
node: T & {[EPHEMERAL]?: boolean},
): T {
node[EPHEMERAL] = true;
return node;
}
/** @internal */
const NON_ENUMERABLE_PROP_DESC: PropertyDescriptor = {
configurable: true,
enumerable: false,
value: undefined,
writable: true,
};
/**
* A node that can host named slots, implemented by {@link ElementNode} and
* {@link DecoratorNode}. The map is allocated lazily (null until the first
* {@link $setSlot}) since most nodes have none. Declaring this off the base
* {@link LexicalNode} is what lets {@link $setSlot} / {@link $removeSlot}
* reject a non-host at compile time.
*
* @experimental
*/
export interface SlotHostNode {
/** @internal */
__slots: null | Map<string, NodeKey>;
}
/**
* A node that can occupy a named slot, implemented by {@link ElementNode} and
* {@link DecoratorNode}. Its up-pointer is `__slotHost` rather than `__parent`
* (the two are mutually exclusive), so the slot boundary behaves like a shadow
* root.
*
* @experimental
*/
export interface SlotChildNode {
/** @internal */
__slotHost: null | NodeKey;
}
export class LexicalNode {
/** @internal Allow us to look up the type including static props */
declare ['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;
/** @internal */
__state?: NodeState<this>;
/** @internal */
[CACHED_TEXT_SIZE_KEY]?: number;
// 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 {
const {ownNodeType} = getStaticNodeConfig(this);
invariant(
ownNodeType !== undefined,
'LexicalNode: Node %s does not implement .getType().',
this.name,
);
return ownNodeType;
}
/**
* 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,
);
}
/**
* Override this to implement the new static node configuration protocol,
* this method is called directly on the prototype and must not depend
* on anything initialized in the constructor. Generally it should be
* a trivial implementation.
*
* @example
* ```ts
* class MyNode extends TextNode {
* $config() {
* return this.config('my-node', {extends: TextNode});
* }
* }
* ```
*/
$config(): BaseStaticNodeConfig {
return {};
}
/**
* This is a convenience method for $config that
* aids in type inference. See {@link LexicalNode.$config}
* for example usage.
*
* An abstract base class that has no concrete node `type` may pass a
* well-known symbol (by convention `Symbol.for(<NodeClassName>)`) instead of
* a string `type` to declare configuration shared with its subclasses.
*/
config<const Config extends StaticNodeConfigValue<this, string>>(
type: symbol,
config: Config,
): AbstractStaticNodeConfigRecord<Config>;
config<
Type extends string,
const Config extends StaticNodeConfigValue<this, Type>,
>(type: Type, config: Config): StaticNodeConfigRecord<Type, Config>;
config(
type: string | symbol,
config: AnyStaticNodeConfigValue,
): BaseStaticNodeConfig {
const parentKlass = config.extends || getSuperclassOf(this.constructor);
Object.assign(config, {extends: parentKlass});
// A concrete node records its string `type`; an abstract base class is
// keyed by a well-known symbol (e.g. Symbol.for('ElementNode')) and has no
// concrete node `type`.
if (typeof type === 'string') {
Object.assign(config, {type});
}
return {[type]: config} as BaseStaticNodeConfig;
}
/**
* 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): void {
if (this.__key === prevNode.__key) {
this.__parent = prevNode.__parent;
this.__next = prevNode.__next;
this.__prev = prevNode.__prev;
this.__state = prevNode.__state;
} else if (prevNode.__state) {
this.__state = prevNode.__state.getWritable(this);
}
}
/**
* Reset state in this copy of originalNode, if necessary
*
* @param originalNode
*/
resetOnCopyNodeFrom(originalNode: this): void {
if (this.__state) {
this.__state = this.__state.getWritable(this).resetOnCopyNode();
}
}
// 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;
Object.defineProperty(this, '__state', NON_ENUMERABLE_PROP_DESC);
// Pre-initialize the reconciler's cached-text-size slot so subsequent
// assignments on the V8 hot path slot into a stable hidden class
// instead of triggering per-instance shape transitions.
Object.defineProperty(this, CACHED_TEXT_SIZE_KEY, NON_ENUMERABLE_PROP_DESC);
$setNodeKey(this, key);
if (__DEV__) {
if (this.__type !== 'root') {
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 ultimately be cleaned up by the Lexical GC.
*/
isAttached(): boolean {
let nodeKey: string | null = this.__key;
while (nodeKey !== null) {
if (nodeKey === 'root') {
return true;
}
// Annotation breaks a circular inference through the loop (TS7022),
// remove when the deprecated generic signatures from #8661 are removed
const node: LexicalNode | null = $getNodeByKey(nodeKey);
if (node === null) {
break;
}
// A slotted node has no __parent; follow its slot host up toward root.
nodeKey = node.__parent !== null ? node.__parent : $getSlotHostKey(node);
}
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;
if (
parentNode.is(firstPoint.getNode()) &&
firstPoint.offset === parentNode.getChildrenSize() &&
this.is(parentNode.getLastChild())
) {
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(): ElementNode | null;
/**
* @deprecated The type parameter is an unchecked and unsafe cast,
* equivalent to `node.getParent() as T | null`, and will be removed
* in a future release. Call this method without a type argument and
* narrow the result with a type guard instead.
*/
getParent<T extends ElementNode>(): T | null;
getParent(): ElementNode | null {
const parent = this.getLatest().__parent;
if (parent === null) {
return null;
}
// Cast: a parent key always refers to an ElementNode
return $getNodeByKey(parent) as ElementNode | null;
}
/**
* Returns the parent of this node, or throws if none is found.
*/
getParentOrThrow(): ElementNode;
/**
* @deprecated The type parameter is an unchecked and unsafe cast,
* equivalent to `node.getParentOrThrow() as T`, and will be removed
* in a future release. Call this method without a type argument and
* narrow the result with a type guard instead.
*/
getParentOrThrow<T extends ElementNode>(): T;
getParentOrThrow(): ElementNode {
const parent = this.getParent();
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) {
// Annotation breaks a circular inference through the loop (TS7022),
// remove when the deprecated generic signatures from #8661 are removed
const parent: ElementNode | null = node.getParent();
// A slot value's host acts as a shadow root, so the slot boundary is
// the top of the isolated scope and this node is its top-level element.
if ($isRootOrShadowRoot(parent) || $getSlotHostKey(node) !== null) {
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(): ElementNode[] {
const parents: 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(): NodeKey[] {
const parents = [];
let node = this.getParent();
while (node !== null) {
parents.push(node.__key);
node = node.getParent();
}
return parents;
}
/**
* Returns the node before this one in the same parent, or null
* if there is no such node.
*/
getPreviousSibling(): LexicalNode | null;
/**
* @deprecated The type parameter is an unchecked and unsafe cast,
* equivalent to `node.getPreviousSibling() as T | null`, and will be
* removed in a future release. Call this method without a type argument
* and narrow the result with a type guard instead.
*/
getPreviousSibling<T extends LexicalNode>(): T | null;
getPreviousSibling(): LexicalNode | null {
const self = this.getLatest();
const prevKey = self.__prev;
return prevKey === null ? null : $getNodeByKey(prevKey);
}
/**
* Returns all nodes before this one in the same parent,
* in document order.
*/
getPreviousSiblings(): LexicalNode[];
/**
* @deprecated The type parameter is an unchecked and unsafe cast,
* equivalent to `node.getPreviousSiblings() as T[]`, and will be
* removed in a future release. Call this method without a type argument
* and narrow the results with a type guard instead.
*/
getPreviousSiblings<T extends LexicalNode>(): T[];
getPreviousSiblings(): LexicalNode[] {
const siblings: LexicalNode[] = [];
const parent = this.getParent();
if (parent === null) {
return siblings;
}
let node = parent.getFirstChild();
while (node !== null) {
if (node.is(this)) {
break;
}
siblings.push(node);
node = node.getNextSibling();
}
return siblings;
}
/**
* Returns the node after this one in the same parent, or null
* if there is no such node.
*/
getNextSibling(): LexicalNode | null;
/**
* @deprecated The type parameter is an unchecked and unsafe cast,
* equivalent to `node.getNextSibling() as T | null`, and will be
* removed in a future release. Call this method without a type argument
* and narrow the result with a type guard instead.
*/
getNextSibling<T extends LexicalNode>(): T | null;
getNextSibling(): LexicalNode | null {
const self = this.getLatest();
const nextKey = self.__next;
return nextKey === null ? null : $getNodeByKey(nextKey);
}
/**
* Returns all nodes after this one in the same parent,
* in document order.
*/
getNextSiblings(): LexicalNode[];
/**
* @deprecated The type parameter is an unchecked and unsafe cast,
* equivalent to `node.getNextSiblings() as T[]`, and will be
* removed in a future release. Call this method without a type argument
* and narrow the results with a type guard instead.
*/
getNextSiblings<T extends LexicalNode>(): T[];
getNextSiblings(): LexicalNode[] {
const siblings: LexicalNode[] = [];
let node = this.getNextSibling();
while (node !== null) {
siblings.push(node);
node = node.getNextSibling();
}
return siblings;
}
/**
* @deprecated use {@link $getCommonAncestor}
*
* 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 = $isElementNode(this) ? this : this.getParent();
const b = $isElementNode(node) ? node : node.getParent();
const result = a && b ? $getCommonAncestor(a, b) : null;
return result
? (result.commonAncestor as T) /* TODO this type cast is a lie, but fixing it would break backwards compatibility */
: 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 logically precedes the target node in the
* editor state, false otherwise (including if there is no common ancestor).
*
* Note that this notion of isBefore is based on post-order; a descendant
* node is always before its ancestors. See also
* {@link $getCommonAncestor} and {@link $comparePointCaretNext} for
* more flexible ways to determine the relative positions of nodes.
*
* @param targetNode - the node we're testing to see if it's after this one.
*/
isBefore(targetNode: LexicalNode): boolean {
const compare = $getCommonAncestor(this, targetNode);
if (compare === null) {
return false;
}
if (compare.type === 'descendant') {
return true;
}
if (compare.type === 'branch') {
return $getCommonAncestorResultBranchOrder(compare) === -1;
}
invariant(
compare.type === 'same' || compare.type === 'ancestor',
'LexicalNode.isBefore: exhaustiveness check',
);
return false;
}
/**
* Returns true if this node is an ancestor of and distinct from the target node, false otherwise.
*
* @param targetNode - the would-be child node.
*/
isParentOf(targetNode: LexicalNode): boolean {
return $hasAncestor(targetNode, this);
}
// 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): 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 {
if ($isEphemeral(this)) {
return this;
}
// Cast: the nodeMap entry for this key is always the same node class
const latest = $getNodeByKey(this.__key) as this | null;
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 {
if ($isEphemeral(this)) {
return 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 $getSlotsTextContent(this);
}
/**
* Returns the length of the string produced by calling getTextContent on this node.
*
*/
getTextContentSize(): number {
// Decorator slot hosts use this base impl: slot text is folded into
// getTextContent, so .length is the size — counted by length, not by each
// slot's own getTextContentSize (the ElementNode override sums those).
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 lifecycle.
*
* @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');
}
/**
* Returns a {@link DOMSlot} pointing at the content-bearing element of this
* node's DOM. The default returns a slot wrapping the keyed DOM as-is.
*
* Override this when {@link createDOM} returns a wrapper around the
* content-bearing element (e.g. `<span><br/></span>` for a styled line
* break), so selection / reconciliation logic can target the inner element.
*
* {@link ElementNode} overrides this to return an {@link ElementDOMSlot}
* with children-management semantics (used by the reconciler to place
* managed children).
*
* @experimental
*/
getDOMSlot(element: HTMLElement): DOMSlot<HTMLElement> {
return new DOMSlot(element);
}
/**
* Controls how the this node is serialized to HTML. This is important for
* copy