UNPKG

lexical

Version:

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

299 lines (298 loc) 13 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 { configTypeSymbol, initTypeSymbol, LexicalExtensionInternal, outputTypeSymbol, peerDependencySymbol } from './internal'; import type { CreateEditorArgs, EditorState, LexicalEditor } from 'lexical'; /** * Any concrete {@link LexicalExtension} */ export type AnyLexicalExtension = LexicalExtension<any, string, any, any>; /** * Any {@link LexicalExtension} or {@link NormalizedLexicalExtensionArgument} */ export type AnyLexicalExtensionArgument = AnyLexicalExtension | AnyNormalizedLexicalExtensionArgument; /** * The default extension configuration of an empty object */ export type ExtensionConfigBase = Record<never, never>; /** * The result of {@link declarePeerDependency}, a tuple of a peer dependency * name and its associated configuration. */ export type NormalizedPeerDependency<Extension extends AnyLexicalExtension> = [ Extension['name'], Partial<LexicalExtensionConfig<Extension>> | undefined ] & { readonly [peerDependencySymbol]?: Extension; }; /** * A tuple of `[extension, ...configOverrides]` */ export type NormalizedLexicalExtensionArgument<Config extends ExtensionConfigBase, Name extends string, Output, Init> = [LexicalExtension<Config, Name, Output, Init>, ...Partial<Config>[]]; /** * Any {@link NormalizedLexicalExtensionArgument} */ export type AnyNormalizedLexicalExtensionArgument = NormalizedLexicalExtensionArgument<any, string, any, any>; /** * An object that the init method can use to access the * configuration for extension dependencies */ export interface ExtensionInitState { /** * Get the result of a peerDependency by name, if it exists * (must be a peerDependency of this extension) */ getPeer: <Dependency extends AnyLexicalExtension = never>(name: Dependency['name']) => undefined | Omit<LexicalExtensionDependency<Dependency>, 'output' | 'init'>; /** * Get the configuration of a dependency by extension * (must be a direct dependency of this extension) */ getDependency: <Dependency extends AnyLexicalExtension>(dep: Dependency) => Omit<LexicalExtensionDependency<Dependency>, 'output' | 'init'>; /** * Get the names of any direct dependents of this * Extension, typically only used for error messages. */ getDirectDependentNames: () => ReadonlySet<string>; /** * Get the names of all peer dependencies of this * Extension, even if they do not exist in the builder, * typically only used for devtools. */ getPeerNameSet: () => ReadonlySet<string>; } export interface ExtensionBuildState<Init> extends Omit<ExtensionInitState, 'getPeer' | 'getDependency'> { /** * Get the result of a peerDependency by name, if it exists * (must be a peerDependency of this extension) */ getPeer: <Dependency extends AnyLexicalExtension = never>(name: Dependency['name']) => undefined | LexicalExtensionDependency<Dependency>; /** * Get the configuration of a dependency by extension * (must be a direct dependency of this extension) */ getDependency: <Dependency extends AnyLexicalExtension>(dep: Dependency) => LexicalExtensionDependency<Dependency>; /** * The result of the init function */ getInitResult: () => Init; } /** * An object that the register method can use to detect unmount and access the * configuration for extension dependencies */ export interface ExtensionRegisterState<Init, Output> extends ExtensionBuildState<Init> { /** An AbortSignal that is aborted when this LexicalEditor registration is disposed */ getSignal: () => AbortSignal; /** * The result of the output function */ getOutput: () => Output; } /** * A {@link LexicalExtension} or {@link NormalizedLexicalExtensionArgument} (extension with config overrides) */ export type LexicalExtensionArgument<Config extends ExtensionConfigBase, Name extends string, Output, Init> = LexicalExtension<Config, Name, Output, Init> | NormalizedLexicalExtensionArgument<Config, Name, Output, Init>; export interface LexicalExtensionDependency<out Dependency extends AnyLexicalExtension> { init: LexicalExtensionInit<Dependency>; config: LexicalExtensionConfig<Dependency>; output: LexicalExtensionOutput<Dependency>; } /** * An Extension is a composable unit of LexicalEditor configuration * (nodes, theme, etc) used to create an editor, plus runtime behavior * that is registered after the editor is created. * * An Extension may depend on other Extensions, and provide functionality to other * extensions through its config. */ export interface LexicalExtension<Config extends ExtensionConfigBase, Name extends string, Output, Init> extends InitialEditorConfig, LexicalExtensionInternal<Config, Output, Init> { /** The name of the Extension, must be unique */ readonly name: Name; /** * Extension names that must not be loaded with this Extension. * If this extension and any of the conflicting extensions are configured * in the same editor then a runtime error will be thrown instead of * creating the editor. This is used to prevent extensions with incompatible * and overlapping functionality from being registered concurrently, such as * PlainTextExtension and RichTextExtension. **/ conflictsWith?: string[]; /** Other Extensions that this Extension depends on, can also be used to configure them */ dependencies?: AnyLexicalExtensionArgument[]; /** * Other Extensions, by name, that this Extension can optionally depend on or * configure, if they are directly depended on by another Extension */ peerDependencies?: NormalizedPeerDependency<AnyLexicalExtension>[]; /** * The default configuration specific to this Extension. This Config may be * seen by this Extension, or any Extension that uses it as a dependency. * * The config may be mutated on register, this is particularly useful * for vending functionality to other Extensions that depend on this Extension. */ config?: Config; /** * By default, Config is shallow merged `{...a, ...b}` with * {@link shallowMergeConfig}, if your Extension requires other strategies * (such as concatenating an Array) you can implement it here. * * @example * Merging an array * ```js * const extension = defineExtension({ * // ... * mergeConfig(config, overrides) { * const merged = shallowMergeConfig(config, overrides); * if (Array.isArray(overrides.decorators)) { * merged.decorators = [...config.decorators, ...overrides.decorators]; * } * return merged; * } * }); * ``` * * @param config - The current configuration * @param overrides - The partial configuration to merge * @returns The merged configuration */ mergeConfig?: (config: Config, overrides: Partial<Config>) => Config; /** * Perform any necessary initialization before the editor is created, * this runs after all configuration overrides for both the editor this * this extension have been merged. May be used validate the editor * configuration. * * @param editorConfig - The in-progress editor configuration (mutable) * @param config - The merged configuration specific to this extension (mutable) * @param state - An object containing methods for accessing the merged * configuration of dependencies and peerDependencies */ init?: (editorConfig: InitialEditorConfig, config: Config, state: ExtensionInitState) => Init; /** * Perform any tasks that require a LexicalEditor instance, but before * registration has taken place. May provide output to be used by * dependencies or the application (commands, components, etc.). * This will only be run once, and any work performed by the output * function must not require cleanup. */ build?: (editor: LexicalEditor, config: Config, state: ExtensionBuildState<Init>) => Output; /** * Add behavior to the editor (register transforms, listeners, etc.) after * the Editor is created, but before its initial state is set. * The register function may also mutate the config * in-place to expose data to other extensions that use it as a dependency. * * @param editor - The editor this Extension is being registered with * @param config - The merged configuration specific to this Extension * @param state - An object containing an AbortSignal that can be * used, and methods for accessing the merged configuration of * dependencies and peerDependencies * @returns A clean-up function */ register?: (editor: LexicalEditor, config: Config, state: ExtensionRegisterState<Init, Output>) => () => void; /** * Run any code that must happen after initialization of the * editor state (which happens after all register calls). * * @param editor - The editor this Extension is being registered with * @param config - The merged configuration specific to this Extension * @param state - An object containing an AbortSignal that can be * used, and methods for accessing the merged configuration of * dependencies and peerDependencies * @returns A clean-up function */ afterRegistration?: (editor: LexicalEditor, config: Config, state: ExtensionRegisterState<Init, Output>) => () => void; } /** * Extract the Config type from an Extension */ export type LexicalExtensionConfig<Extension extends AnyLexicalExtension> = NonNullable<Extension[configTypeSymbol]>; /** * Extract the Name type from an Extension */ export type LexicalExtensionName<Extension extends AnyLexicalExtension> = Extension['name']; /** * Extract the Output type from an Extension */ export type LexicalExtensionOutput<Extension extends AnyLexicalExtension> = NonNullable<Extension[outputTypeSymbol]>; /** * Extract the Init type from an Extension */ export type LexicalExtensionInit<Extension extends AnyLexicalExtension> = NonNullable<Extension[initTypeSymbol]>; /** * An Extension that has an OutputComponent of the given type (e.g. React.ComponentType) */ export type OutputComponentExtension<ComponentType> = LexicalExtension<any, any, { Component: ComponentType; }, any>; /** * A handle to the editor with an attached dispose function */ export interface LexicalEditorWithDispose extends LexicalEditor, Disposable { /** * Dispose the editor and perform all clean-up * (also available as Symbol.dispose via Disposable) */ dispose: () => void; } /** * All of the possible ways to initialize $initialEditorState: * - `null` an empty state, the default * - `string` an EditorState serialized to JSON * - `EditorState` an EditorState that has been deserialized already (not just parsed JSON) * - `((editor: LexicalEditor) => void)` A function that is called with the editor for you to mutate it */ export type InitialEditorStateType = null | string | EditorState | ((editor: LexicalEditor) => void); export interface InitialEditorConfig { /** * @internal Disable root element events (for internal Meta use) */ disableEvents?: CreateEditorArgs['disableEvents']; /** * Used when this editor is nested inside of another editor */ parentEditor?: CreateEditorArgs['parentEditor']; /** * The namespace of this Editor. If two editors share the same * namespace, JSON will be the clipboard interchange format. * Otherwise HTML will be used. */ namespace?: CreateEditorArgs['namespace']; /** * The nodes that this Extension adds to the Editor configuration, will be merged with other Extensions */ nodes?: CreateEditorArgs['nodes']; /** * EditorThemeClasses that will be deep merged with other Extensions */ theme?: CreateEditorArgs['theme']; /** * Overrides for HTML serialization (exportDOM) and * deserialization (importDOM) that does not require subclassing and node * replacement */ html?: CreateEditorArgs['html']; /** * Whether the initial state of the editor is editable or not */ editable?: CreateEditorArgs['editable']; /** * The editor will catch errors that happen during updates and * reconciliation and call this. It defaults to * `(error) => { throw error }`. * * @param error - The Error object * @param editor - The editor that this error came from */ onError?: (error: Error, editor: LexicalEditor) => void; /** * The initial EditorState as a JSON string, an EditorState, or a function * to update the editor (once). */ $initialEditorState?: InitialEditorStateType; }