/** * 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 {@link LexicalExtension} or {@link NormalizedLexicalExtensionArgument} */ export type AnyLexicalExtensionArgument = AnyLexicalExtension | AnyNormalizedLexicalExtensionArgument; /** * The default extension configuration of an empty object */ export type ExtensionConfigBase = Record; /** * The result of {@link declarePeerDependency}, a tuple of a peer dependency * name and its associated configuration. */ export type NormalizedPeerDependency = [ Extension['name'], Partial> | undefined ] & { readonly [peerDependencySymbol]?: Extension; }; /** * A tuple of `[extension, ...configOverrides]` */ export type NormalizedLexicalExtensionArgument = [LexicalExtension, ...Partial[]]; /** * Any {@link NormalizedLexicalExtensionArgument} */ export type AnyNormalizedLexicalExtensionArgument = NormalizedLexicalExtensionArgument; /** * 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: (name: Dependency['name']) => undefined | Omit, 'output' | 'init'>; /** * Get the configuration of a dependency by extension * (must be a direct dependency of this extension) */ getDependency: (dep: Dependency) => Omit, 'output' | 'init'>; /** * Get the names of any direct dependents of this * Extension, typically only used for error messages. */ getDirectDependentNames: () => ReadonlySet; /** * 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; } export interface ExtensionBuildState extends Omit { /** * Get the result of a peerDependency by name, if it exists * (must be a peerDependency of this extension) */ getPeer: (name: Dependency['name']) => undefined | LexicalExtensionDependency; /** * Get the configuration of a dependency by extension * (must be a direct dependency of this extension) */ getDependency: (dep: Dependency) => LexicalExtensionDependency; /** * 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 extends ExtensionBuildState { /** 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 = LexicalExtension | NormalizedLexicalExtensionArgument; export interface LexicalExtensionDependency { init: LexicalExtensionInit; config: LexicalExtensionConfig; output: LexicalExtensionOutput; } /** * 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 extends InitialEditorConfig, LexicalExtensionInternal { /** 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[]; /** * 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; /** * 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) => 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) => () => 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) => () => void; } /** * Extract the Config type from an Extension */ export type LexicalExtensionConfig = NonNullable; /** * Extract the Name type from an Extension */ export type LexicalExtensionName = Extension['name']; /** * Extract the Output type from an Extension */ export type LexicalExtensionOutput = NonNullable; /** * Extract the Init type from an Extension */ export type LexicalExtensionInit = NonNullable; /** * An Extension that has an OutputComponent of the given type (e.g. React.ComponentType) */ export type OutputComponentExtension = OutputExtension<{ Component: ComponentType; }>; /** * An Extension that has an Output of the given type */ export type OutputExtension = LexicalExtension; /** * 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. * * Can be a function to defer the access of the nodes to editor construction * which may be useful in cases when the node and extension are defined in * different modules and have depenendencies on each other, depending on the * bundler configuration. */ nodes?: CreateEditorArgs['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; }