lexical
Version:
Lexical is an extensible text editor framework that provides excellent reliability, accessible and performance.
505 lines (504 loc) • 20.8 kB
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 { AnyStaticNodeConfigValue, GetStaticNodeOwnConfig, GetStaticNodeType } from './LexicalNode';
import { type Klass, type LexicalNode, type LexicalNodeConfig, type LexicalUpdateJSON, NODE_STATE_KEY, type SerializedLexicalNode, type Spread } from '.';
/**
* Read the state directly from the given object without `node.getLatest()`.
* Safe to use outside of editor state context or to read a previous version,
* equivalent to reading the property directly.
*/
export declare const NODE_STATE_DIRECT = "direct";
/**
* Use `node.getLatest()` before reading the state, per the lexical convention
* of only working with the latest version of a node.
*/
export declare const NODE_STATE_LATEST = "latest";
export type NodeStateVersion = typeof NODE_STATE_DIRECT | typeof NODE_STATE_LATEST;
/**
* Get the value type (V) from a StateConfig
*/
export type StateConfigValue<S extends AnyStateConfig> = S extends StateConfig<infer _K, infer V> ? V : never;
/**
* Get the key type (K) from a StateConfig
*/
export type StateConfigKey<S extends AnyStateConfig> = S extends StateConfig<infer K, infer _V> ? K : never;
/**
* A value type, or an updater for that value type. For use with
* {@link $setState} or any user-defined wrappers around it.
*/
export type ValueOrUpdater<V> = V | ((prevValue: V) => V);
/**
* A type alias to make it easier to define setter methods on your node class
*
* @example
* ```ts
* const fooState = createState("foo", { parse: ... });
* class MyClass extends TextNode {
* // ...
* setFoo(valueOrUpdater: StateValueOrUpdater<typeof fooState>): this {
* return $setState(this, fooState, valueOrUpdater);
* }
* }
* ```
*/
export type StateValueOrUpdater<Cfg extends AnyStateConfig> = ValueOrUpdater<StateConfigValue<Cfg>>;
export interface NodeStateConfig<S extends AnyStateConfig> {
stateConfig: S;
flat?: boolean;
}
export type RequiredNodeStateConfig = NodeStateConfig<AnyStateConfig> | AnyStateConfig;
export type StateConfigJSON<S> = S extends StateConfig<infer K, infer V> ? {
[Key in K]?: V;
} : Record<never, never>;
export type RequiredNodeStateConfigJSON<Config extends RequiredNodeStateConfig, Flat extends boolean> = StateConfigJSON<Config extends NodeStateConfig<infer S> ? Spread<Config, {
flat: false;
}> extends {
flat: Flat;
} ? S : never : false extends Flat ? Config : never>;
export type Prettify<T> = {
[K in keyof T]: T[K];
} & {};
export type UnionToIntersection<T> = (T extends any ? (x: T) => any : never) extends (x: infer R) => any ? R : never;
export type CollectStateJSON<Tuple extends readonly RequiredNodeStateConfig[], Flat extends boolean> = UnionToIntersection<{
[K in keyof Tuple]: RequiredNodeStateConfigJSON<Tuple[K], Flat>;
}[number]>;
type GetStaticNodeConfig<T extends LexicalNode> = [
GetStaticNodeOwnConfig<T>
] extends [never] ? GetStaticNodeType<T> extends infer Type extends string ? string extends Type ? never : {
[P in Type]: NonNullable<ReturnType<T[typeof PROTOTYPE_CONFIG_METHOD]>[P]>;
}[Type] extends infer Config extends AnyStaticNodeConfigValue ? Config & {
readonly type: Type;
} : never : never : GetStaticNodeOwnConfig<T> extends infer Config extends AnyStaticNodeConfigValue ? Config & {
readonly type: GetStaticNodeType<T>;
} : never;
type GetStaticNodeConfigs<T extends LexicalNode> = GetStaticNodeConfig<T> extends infer OwnConfig ? [
OwnConfig
] extends [never] ? [] : OwnConfig extends {
extends: Klass<infer Parent>;
} ? [GetStaticNodeConfig<Parent>] extends [never] ? [OwnConfig] : [OwnConfig, ...GetStaticNodeConfigs<Parent>] : [OwnConfig] : [];
type CollectStateConfigs<Configs> = Configs extends [
infer OwnConfig,
...infer ParentConfigs
] ? OwnConfig extends {
stateConfigs: infer StateConfigs;
} ? StateConfigs extends readonly RequiredNodeStateConfig[] ? [...StateConfigs, ...CollectStateConfigs<ParentConfigs>] : CollectStateConfigs<ParentConfigs> : CollectStateConfigs<ParentConfigs> : [];
export type GetNodeStateConfig<T extends LexicalNode> = CollectStateConfigs<GetStaticNodeConfigs<T>>;
/**
* The NodeState JSON produced by this LexicalNode
*/
export type NodeStateJSON<T extends LexicalNode> = Prettify<{
[NODE_STATE_KEY]?: Prettify<CollectStateJSON<GetNodeStateConfig<T>, false>>;
} & CollectStateJSON<GetNodeStateConfig<T>, true>>;
/**
* Configure a value to be used with StateConfig.
*
* The value type should be inferred from the definition of parse.
*
* If the value type is not JSON serializable, then unparse must also be provided.
*
* Values should be treated as immutable, much like React.useState. Mutating
* stored values directly will cause unpredictable behavior, is not supported,
* and may trigger errors in the future.
*
* @example
* ```ts
* const numberOrNullState = createState('numberOrNull', {parse: (v) => typeof v === 'number' ? v : null});
* // ^? State<'numberOrNull', StateValueConfig<number | null>>
* const numberState = createState('number', {parse: (v) => typeof v === 'number' ? v : 0});
* // ^? State<'number', StateValueConfig<number>>
* ```
*
* Only the parse option is required, it is generally not useful to
* override `unparse` or `isEqual`. However, if you are using
* non-primitive types such as Array, Object, Date, or something
* more exotic then you would want to override this. In these
* cases you might want to reach for third party libraries.
*
* @example
* ```ts
* const isoDateState = createState('isoDate', {
* parse: (v): null | Date => {
* const date = typeof v === 'string' ? new Date(v) : null;
* return date && !isNaN(date.valueOf()) ? date : null;
* }
* isEqual: (a, b) => a === b || (a && b && a.valueOf() === b.valueOf()),
* unparse: (v) => v && v.toString()
* });
* ```
*
* You may find it easier to write a parse function using libraries like
* zod, valibot, ajv, Effect, TypeBox, etc. perhaps with a wrapper function.
*/
export interface StateValueConfig<V> {
/**
* This function must return a default value when called with undefined,
* otherwise it should parse the given JSON value to your type V. Note
* that it is not required to copy or clone the given value, you can
* pass it directly through if it matches the expected type.
*
* When you encounter an invalid value, it's up to you to decide
* as to whether to ignore it and return the default value,
* return some non-default error value, or throw an error.
*
* It is possible for V to include undefined, but if it does, then
* it should also be considered the default value since undefined
* can not be serialized to JSON so it is indistinguishable from the
* default.
*
* Similarly, if your V is a function, then usage of {@link $setState}
* must use an updater function because your type will be indistinguishable
* from an updater function.
*/
parse: (jsonValue: unknown) => V;
/**
* This is optional and for advanced use cases only.
*
* You may specify a function that converts V back to JSON.
* This is mandatory when V is not a JSON serializable type.
*/
unparse?: (parsed: V) => unknown;
/**
* This is optional and for advanced use cases only.
*
* Used to define the equality function so you can use an Array or Object
* as V and still omit default values from the exported JSON.
*
* The default is `Object.is`, but something like `fast-deep-equal` might be
* more appropriate for your use case.
*/
isEqual?: (a: V, b: V) => boolean;
/**
* When a node is copied with {@link $copyNode} (not cloned), reset this
* value to the default.
*/
resetOnCopyNode?: boolean;
}
/**
* The return value of {@link createState}, for use with
* {@link $getState} and {@link $setState}.
*/
export declare class StateConfig<K extends string | symbol, V> {
/** The string key used when serializing this state to JSON */
readonly key: K;
/** The parse function from the StateValueConfig passed to createState */
readonly parse: (value?: unknown) => V;
/**
* The unparse function from the StateValueConfig passed to createState,
* with a default that is simply a pass-through that assumes the value is
* JSON serializable.
*/
readonly unparse: (value: V) => unknown;
/**
* An equality function from the StateValueConfig, with a default of
* Object.is.
*/
readonly isEqual: (a: V, b: V) => boolean;
/**
* The result of `stateValueConfig.parse(undefined)`, which is computed only
* once and used as the default value. When the current value `isEqual` to
* the `defaultValue`, it will not be serialized to JSON.
*/
readonly defaultValue: V;
readonly resetOnCopyNode: boolean;
constructor(key: K, stateValueConfig: StateValueConfig<V>);
}
/**
* For advanced use cases, using this type is not recommended unless
* it is required (due to TypeScript's lack of features like
* higher-kinded types).
*
* A {@link StateConfig} type with any key and any value that can be
* used in situations where the key and value type can not be known,
* such as in a generic constraint when working with a collection of
* StateConfig.
*
* {@link StateConfigKey} and {@link StateConfigValue} will be
* useful when this is used as a generic constraint.
*/
export type AnyStateConfig = StateConfig<any, any>;
/**
* Create a StateConfig for the given string key and StateValueConfig.
*
* The key must be locally unique. In dev you will get a key collision error
* when you use two separate StateConfig on the same node with the same key.
*
* The returned StateConfig value should be used with {@link $getState} and
* {@link $setState}.
*
* @param key The key to use
* @param valueConfig Configuration for the value type
* @returns a StateConfig
*
* @__NO_SIDE_EFFECTS__
*/
export declare function createState<K extends symbol | string, V>(key: K, valueConfig: StateValueConfig<V>): StateConfig<K, V>;
/**
* The accessor for working with node state. This will read the value for the
* state on the given node, and will return `stateConfig.defaultValue` if the
* state has never been set on this node.
*
* The `version` parameter is optional and should generally be {@link NODE_STATE_LATEST},
* consistent with the behavior of other node methods and functions,
* but for certain use cases such as `updateDOM` you may have a need to
* use {@link NODE_STATE_DIRECT} to read the state from a previous version of the node.
*
* For very advanced use cases, you can expect that {@link NODE_STATE_DIRECT} does not
* require an editor state, just like directly accessing other properties
* of a node without an accessor (e.g. `textNode.__text`).
*
* @param node Any LexicalNode
* @param stateConfig The configuration of the state to read
* @param version The default value {@link NODE_STATE_LATEST} will read the latest version of the node state, {@link NODE_STATE_DIRECT} will read the version that is stored on this LexicalNode which not reflect the version used in the current editor state
* @returns The current value from the state, or the default value provided by the configuration.
*/
export declare function $getState<K extends string, V>(node: LexicalNode, stateConfig: StateConfig<K, V>, version?: NodeStateVersion): V;
/**
* Given two versions of a node and a stateConfig, compare their state values
* using `$getState(nodeVersion, stateConfig, NODE_STATE_DIRECT)`.
* If the values are equal according to `stateConfig.isEqual`, return `null`,
* otherwise return `[value, prevValue]`.
*
* This is useful for implementing updateDOM. Note that the `NODE_STATE_DIRECT`
* version argument is used for both nodes.
*
* @param node Any LexicalNode
* @param prevNode A previous version of node
* @param stateConfig The configuration of the state to read
* @returns `[value, prevValue]` if changed, otherwise `null`
*/
export declare function $getStateChange<T extends LexicalNode, K extends string, V>(node: T, prevNode: T, stateConfig: StateConfig<K, V>): null | [value: V, prevValue: V];
/**
* Set the state defined by stateConfig on node. Like with `React.useState`
* you may directly specify the value or use an updater function that will
* be called with the previous value of the state on that node (which will
* be the `stateConfig.defaultValue` if not set).
*
* When an updater function is used, the node will only be marked dirty if
* `stateConfig.isEqual(prevValue, value)` is false.
*
* @example
* ```ts
* const toggle = createState('toggle', {parse: Boolean});
* // set it direction
* $setState(node, counterState, true);
* // use an updater
* $setState(node, counterState, (prev) => !prev);
* ```
*
* @param node The LexicalNode to set the state on
* @param stateConfig The configuration for this state
* @param valueOrUpdater The value or updater function
* @returns node
*/
export declare function $setState<Node extends LexicalNode, K extends string, V>(node: Node, stateConfig: StateConfig<K, V>, valueOrUpdater: ValueOrUpdater<V>): Node;
/**
* @internal
*
* Opaque state to be stored on the editor's RegisterNode for use by NodeState
*/
export type SharedNodeState = {
sharedConfigMap: SharedConfigMap;
flatKeys: Set<string>;
};
/**
* @internal
*
* Create the state to store on RegisteredNode
*/
export declare function createSharedNodeState(nodeConfig: LexicalNodeConfig): SharedNodeState;
type KnownStateMap = Map<AnyStateConfig, unknown>;
type UnknownStateRecord = Record<string, unknown>;
/**
* @internal
*
* A Map of string keys to state configurations to be shared across nodes
* and/or node versions.
*/
type SharedConfigMap = Map<string, AnyStateConfig>;
/**
* @internal
*/
export declare class NodeState<T extends LexicalNode> {
/**
* @internal
*
* Track the (versioned) node that this NodeState was created for, to
* facilitate copy-on-write for NodeState. When a LexicalNode is cloned,
* it will *reference* the NodeState from its prevNode. From the nextNode
* you can continue to read state without copying, but the first $setState
* will trigger a copy of the prevNode's NodeState with the node property
* updated.
*/
readonly node: LexicalNode;
/**
* @internal
*
* State that has already been parsed in a get state, so it is safe. (can be returned with
* just a cast since the proof was given before).
*
* Note that it uses StateConfig, so in addition to (1) the CURRENT VALUE, it has access to
* (2) the State key (3) the DEFAULT VALUE and (4) the PARSE FUNCTION
*/
readonly knownState: KnownStateMap;
/**
* @internal
*
* A copy of serializedNode[NODE_STATE_KEY] that is made when JSON is
* imported but has not been parsed yet.
*
* It stays here until a get state requires us to parse it, and since we
* then know the value is safe we move it to knownState.
*
* Note that since only string keys are used here, we can only allow this
* state to pass-through on export or on the next version since there is
* no known value configuration. This pass-through is to support scenarios
* where multiple versions of the editor code are working in parallel so
* an old version of your code doesnt erase metadata that was
* set by a newer version of your code.
*/
unknownState: undefined | UnknownStateRecord;
/**
* @internal
*
* This sharedNodeState is preserved across all instances of a given
* node type in an editor and remains writable. It is how keys are resolved
* to configuration.
*/
readonly sharedNodeState: SharedNodeState;
/**
* @internal
*
* The count of known or unknown keys in this state, ignoring the
* intersection between the two sets.
*/
size: number;
/**
* @internal
*/
constructor(node: T, sharedNodeState: SharedNodeState, unknownState?: undefined | UnknownStateRecord, knownState?: KnownStateMap, size?: number | undefined);
/**
* @internal
*
* Get the value from knownState, or parse it from unknownState
* if it contains the given key.
*
* Updates the sharedConfigMap when no known state is found.
* Updates unknownState and knownState when an unknownState is parsed.
*/
getValue<K extends string, V>(stateConfig: StateConfig<K, V>): V;
/**
* @internal
*
* Used only for advanced use cases, such as collab. The intent here is to
* allow you to diff states with a more stable interface than the properties
* of this class.
*/
getInternalState(): [
{
readonly [k in string]: unknown;
} | undefined,
ReadonlyMap<AnyStateConfig, unknown>
];
/**
* Encode this NodeState to JSON in the format that its node expects.
* This returns `{[NODE_STATE_KEY]?: UnknownStateRecord}` rather than
* `UnknownStateRecord | undefined` so that we can support flattening
* specific entries in the future when nodes can declare what
* their required StateConfigs are.
*/
toJSON(): NodeStateJSON<T>;
/**
* @internal
*
* A NodeState is writable when the node to update matches
* the node associated with the NodeState. This basically
* mirrors how the EditorState NodeMap works, but in a
* bottom-up organization rather than a top-down organization.
*
* This allows us to implement the same "copy on write"
* pattern for state, without having the state version
* update every time the node version changes (e.g. when
* its parent or siblings change).
*
* @param node The node to associate with the state
* @returns The next writable state
*/
getWritable(node: T): NodeState<T>;
/** @internal */
resetOnCopyNode(): this;
/** @internal */
updateFromKnown<K extends string, V>(stateConfig: StateConfig<K, V>, value: V): void;
/**
* @internal
*
* This is intended for advanced use cases only, such
* as collab or dev tools.
*
* Update a single key value pair from unknown state,
* parsing it if the key is known to this node. This is
* basically like updateFromJSON, but the effect is
* isolated to a single entry.
*
* @param k The string key from an UnknownStateRecord
* @param v The unknown value from an UnknownStateRecord
*/
updateFromUnknown(k: string, v: unknown): void;
/**
* @internal
*
* Reset all existing state to default or empty values,
* and perform any updates from the given unknownState.
*
* This is used when initializing a node's state from JSON,
* or when resetting a node's state from JSON.
*
* @param unknownState The new state in serialized form
*/
updateFromJSON(unknownState: undefined | UnknownStateRecord): void;
}
/**
* @internal
*
* Only for direct use in very advanced integrations, such as lexical-yjs.
* Typically you would only use {@link createState}, {@link $getState}, and
* {@link $setState}. This is effectively the preamble for {@link $setState}.
*/
export declare function $getWritableNodeState<T extends LexicalNode>(node: T): NodeState<T>;
/**
* @internal
*
* Get the SharedNodeState for a node on this editor
*/
export declare function $getSharedNodeState<T extends LexicalNode>(node: T): SharedNodeState;
/**
* @internal
*
* This is used to implement LexicalNode.updateFromJSON and is
* not intended to be exported from the package.
*
* @param node any LexicalNode
* @param unknownState undefined or a serialized State
* @returns A writable version of node, with the state set.
*/
export declare function $updateStateFromJSON<T extends LexicalNode>(node: T, serialized: LexicalUpdateJSON<SerializedLexicalNode>): T;
/**
* @internal
*
* Return true if the two nodes have equivalent NodeState, to be used
* to determine when TextNode are being merged, not a lot of use cases
* otherwise.
*/
export declare function nodeStatesAreEquivalent<T extends LexicalNode>(a: undefined | NodeState<T>, b: undefined | NodeState<T>): boolean;
/**
* @internal
*
* Clones the NodeState for a given node. Handles aliasing if the state references the from node.
*/
export declare function $cloneNodeState<T extends LexicalNode>(from: T, to: T): undefined | NodeState<T>;
export {};