UNPKG

lexical

Version:

Lexical is an extensible text editor framework that provides excellent reliability, accessible and performance.

505 lines (504 loc) • 20.8 kB
/** * 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 {};