/** * 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 {DOMImportContextSymbol, DOMRenderContextSymbol} from './constants'; import type { BaseSelection, DOMExportOutput, DOMSlotForNode, EditorDOMRenderConfig, Klass, LexicalEditor, LexicalNode, StateConfig, } from 'lexical'; /** * @experimental * * Any ContextSymbol for {@link ContextConfig} (DOM render or DOM import). */ export type AnyContextSymbol = | typeof DOMRenderContextSymbol | typeof DOMImportContextSymbol; /** * @experimental * * Context with a phantom type for its purpose (such as {@link DOMRenderContextSymbol}). * * A ContextRecord is a data structure used in the export and import pipelines * to allow for information to be passed throughout the chain without explicit * argument passing, e.g. to specify whether the intended use case for HTML * export is for serialization or for clipboard copy. */ export type ContextRecord<_K extends symbol> = Record; /** * @experimental * * A data structure much like StateConfig (they share implementation details) * but for managing context during an export or import pipeline rather than * individual node state. */ export type ContextConfig = StateConfig & { readonly [K in Sym]?: true; }; /** * @experimental * * Update the context at `cfg` with updater, constructed with {@link contextUpdater} */ export type ContextConfigUpdater = { readonly cfg: ContextConfig; /** * @param prev The current or default value * @returns The new value */ readonly updater: (prev: V) => V; }; /** * @experimental * * Set the context at `cfg` to a specific value, constructed with {@link contextValue} */ export type ContextConfigPair = readonly [ ContextConfig, V, ]; /** * @experimental * * Set or update a context value, constructed with {@link contextValue} or {@link contextUpdater} */ export type ContextPairOrUpdater = | ContextConfigPair | ContextConfigUpdater; /** @experimental */ export type AnyContextConfigPairOrUpdater = // eslint-disable-next-line @typescript-eslint/no-explicit-any ContextPairOrUpdater; /** @experimental */ export interface DOMRenderExtensionOutput { /** @internal */ defaults: undefined | ContextRecord; /** @internal */ runtime: DOMRenderRuntime; } /** * @experimental * * A read-only view of a render context layer, passed to the * {@link DOMOverrideOptions} predicates so they can decide whether an * override should be installed based only on context values. */ export interface RenderContextReader { get(cfg: RenderStateConfig): V; } /** * @experimental * @internal * * Per-editor runtime state for {@link DOMRenderExtension} that backs the * imperative editor context ({@link createRenderState} writes via * `$setRenderContextValue`) and the conditional install of overrides. */ export interface DOMRenderRuntime { /** * The mutable, persistent editor-level context record. Reads of a * {@link RenderStateConfig} during reconciliation (and as the base layer * during a session) fall through to this record. It is also the layer * that {@link DOMOverrideOptions.disabledForEditor} predicates read from. */ readonly editorContext: ContextRecord; /** * Imperatively set a value in the editor context. If the change flips any * override's `disabledForEditor` result, the resident render config is * recompiled and the affected nodes are re-rendered (recreating DOM for * structural overrides). */ setContextValue(cfg: RenderStateConfig, value: V): void; /** * Resolve the {@link EditorDOMRenderConfig} for the current export/generate * session, applying any {@link DOMOverrideOptions.disabledForSession} * predicates against the active session context. Returns the resident * config when no session gating applies. */ getSessionConfig(): EditorDOMRenderConfig; } /** * @experimental * * Options for {@link domOverride} controlling *whether* an override is * installed, based only on render context. Both predicates default to * "not disabled". */ export interface DOMOverrideOptions { /** * Gate residency in the editor's render config (used by reconciliation and * as the base for export/generate). Evaluated against the persistent editor * context at compile time, and re-evaluated when that context changes via * `$setRenderContextValue`; a change recompiles the config and re-renders * affected nodes. Return `true` to remove the override. Default: not disabled. */ disabledForEditor?: (ctx: RenderContextReader) => boolean; /** * Gate participation in a single export/generate session. Evaluated once at * the start of each session against that session's context. Has no effect on * live reconciliation (which is not a session). Return `true` to remove the * override for that session. Default: not disabled. */ disabledForSession?: (ctx: RenderContextReader) => boolean; } /** * @experimental * * Context configuration for render context, created with {@link createRenderState} */ export type RenderStateConfig = ContextConfig< typeof DOMRenderContextSymbol, V >; /** * @experimental * * Any setter or updater for {@link RenderStateConfig} */ export type AnyRenderStateConfigPairOrUpdater = AnyContextConfigPairOrUpdater< typeof DOMRenderContextSymbol >; /** * @experimental * * Any {@link RenderStateConfig} */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AnyRenderStateConfig = RenderStateConfig; /** * @experimental * * Configuration for {@link DOMRenderExtension} */ export interface DOMRenderConfig { /** * {@link DOMRenderMatch} overrides to customize node behavior, * the final priority of these will be based on the following criteria: * * - Wildcards (`'*'`) have highest priority * - Predicates (`$isParagraphNode`) have next priority * - Subclasses have higher priority (e.g. `ParagraphNode` before `ElementNode`) * - Extensions closer to the root have higher priority * - Extensions depended on later have higher priority * - Overrides defined later have higher priority */ overrides: AnyDOMRenderMatch[]; /** * Default context to provide in all exports, the configurations are created * with {@link createRenderState} and should be created at the module-level. * * Only specify these if overriding the default value globally, since each * configuration has a built-in default value that will be used if not * already present in the context. */ contextDefaults: AnyRenderStateConfigPairOrUpdater[]; } /** * @experimental * Any {@link DOMRenderMatch} */ // eslint-disable-next-line @typescript-eslint/no-explicit-any export type AnyDOMRenderMatch = DOMRenderMatch; /** * @experimental * * Match a node (and any subclass of that node) by its LexicalNode class, * or with a guard (e.g. `ElementNode` or `$isElementNode`). * * Note that using the class compiles to significantly more efficient code * than using a guard. */ export type NodeMatch = | Klass | ((node: LexicalNode) => node is T); /** * @experimental * * Used to define overrides for the render and export * behavior for nodes matching the `nodes` predicate. * * All of these overrides are in a middleware style where you may use the * result of `$next()` to enhance the result of the default implementation * (or a lower priority override) by calling it and manipulating the result, * or you may choose not to call `$next()` to entirely replace the behavior. * * It is not permitted to update the lexical editor state during any of * these calls, you should only be doing read-only operations. */ export interface DOMRenderMatch { /** * '*' for all nodes, or an array of `NodeClass | $isNodeGuard` to match * nodes more specifically. Using classes is more efficient, but will * also target subclasses. */ readonly nodes: '*' | readonly NodeMatch[]; /** * Control where an ElementNode's children are inserted into the DOM, * this is useful to add a wrapping node or accessory nodes before or * after the children. The root of the node returned by createDOM must * still be exactly one HTMLElement. * * Generally you will call `$next()` to get a slot and then use its methods * to create a new one. The slot type is narrowed via {@link DOMSlotForNode}: * for `ElementNode` it resolves to {@link ElementDOMSlot} with * children-management semantics; for non-Element nodes the base * {@link DOMSlot} pointing at the keyed DOM. * * @param node The LexicalNode * @param dom The rendered HTMLElement * @param $next Call the next implementation * @param editor The editor * @returns The slot for this node */ $getDOMSlot?: ( node: T, dom: HTMLElement, $next: () => DOMSlotForNode, editor: LexicalEditor, ) => DOMSlotForNode; /** * Called during the reconciliation process to determine which nodes * to insert into the DOM for this Lexical Node. This is also the default * implementation of `$exportDOM` for most nodes. * * This method must return exactly one `HTMLElement`. * * Nested elements are not supported except with `DecoratorNode` * (which have unmanaged contents) or `ElementNode` using an appropriate * `$getDOMSlot` return value. * * @param node The LexicalNode * @param $next Call the next implementation * @param editor The editor * @returns The HTMLElement for this node to be rendered in the editor */ $createDOM?: ( node: T, $next: () => HTMLElement, editor: LexicalEditor, ) => HTMLElement; /** * 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. * * @param nextNode The current version of this node * @param prevNode The previous version of this node * @param dom The previously rendered HTMLElement for this node * @param $next Call the next implementation * @param editor The editor * @returns `false` if no update needed or was performed in-place, `true` if `$createDOM` should be called to re-create the node */ $updateDOM?: ( nextNode: T, prevNode: T, dom: HTMLElement, $next: () => boolean, editor: LexicalEditor, ) => boolean; /** * Called after a node is created or updated and should make any in-place * updates to the DOM in whatever way is necessary to make it align with * any changes that might have happened during the `$createDOM` or * `$updateDOM`. This also runs after any children have been reconciled. * * Use this when you have code that you would need to duplicate in both * methods, or if there is a need to ensure that the children are also * reconciled before performing this in-place update. * * Unlike other overrides, all applicable `$decorateDOM` functions are * called unconditionally. There is no `$next` argument, because there * are no known use cases for avoiding the next implementation and due * to the void return value it would be error-prone and add boilerplate * to require calling it. * * The ordering here is equivalent to an implicit `$next` call *first*. * * @param nextNode The current version of this node * @param prevNode The previous version of this node if `$updateDOM` returned `false`, or `null` if `$createDOM` was just called * @param dom The previously rendered `HTMLElement` for this node * @param editor The editor */ $decorateDOM?: ( nextNode: T, prevNode: null | T, dom: HTMLElement, editor: LexicalEditor, ) => void; /** * Controls how the this node is serialized to HTML. This is important for * copy and paste between Lexical and non-Lexical editors, or Lexical * editors with different namespaces, in which case the primary transfer * format is HTML. It's also important if you're serializing to HTML for * any other reason via {@link @lexical/html!$generateHtmlFromNodes}. * * @param node The LexicalNode * @param $next Call the next implementation * @param editor The editor * @returns A {@link DOMExportOutput} structure that defines how the node should be exported to HTML */ $exportDOM?: ( node: T, $next: () => DOMExportOutput, editor: LexicalEditor, ) => DOMExportOutput; /** * Equivalent to `ElementNode.excludeFromCopy`, if it returns `true` this * lexical node will not be exported to DOM (but if it's an `ElementNode` * its children may still be inserted in its place). * * Has higher precedence than `$shouldInclude` and `$extractWithChild`. * * @param node The LexicalNode * @param selection The current selection * @param $next The next implementation * @param editor The editor * @returns true to exclude this node, false otherwise */ $shouldExclude?: ( node: T, selection: null | BaseSelection, $next: () => boolean, editor: LexicalEditor, ) => boolean; /** * Return `true` if this node should be included in the export, typically based * on the current selection (all nodes by default are included when there * is no selection). * * The default implementation is equivalent to * `selection ? node.isSelected(selection) : true`. * * This has lower precedence than `$extractWithChild` and `$shouldExclude`. * * @param node The current node * @param selection The current selection * @param $next The next implementation * @param editor The editor * @returns `true` if this node should be included in the export, `false` otherwise */ $shouldInclude?: ( node: T, selection: null | BaseSelection, $next: () => boolean, editor: LexicalEditor, ) => boolean; /** * Return `true` if this node should be included in the export based on * `childNode`, even if it would not otherwise be included based on its * `$shouldInclude` result. * * Typically used to ensure that required wrapping nodes are always * present with its children, e.g. a ListNode when some of its ListItemNode * children are selected. * * This has higher precedence than `$extractWithChild` and lower precedence * than `$shouldExclude`. * * @param node The lexical node * @param childNode A child of this lexical node * @param selection The current selection * @param destination Currently always `'html'` * @param $next The next implementation * @param editor The editor * @returns true if this */ $extractWithChild?: ( node: T, childNode: LexicalNode, selection: null | BaseSelection, destination: 'clone' | 'html', $next: () => boolean, editor: LexicalEditor, ) => boolean; /** * Where a named slot's container should attach, for hosts rendered * entirely in-lexical (no chrome framework). Consulted by the * reconciler whenever it creates or reconciles the slot's container, * synchronously within the same commit: a non-null return attaches * the container to that element and reveals it (returning `hostDom` * reveals the slot in its default slots-first position). Call * `$next()` to defer to lower-priority overrides / the default * (null — a hidden placeholder for explicit imperative mounting via * `mountSlotContainer` or lexical-react's `useLexicalSlotRef`). * * @experimental named-slots */ $getSlotTargetElement?: ( node: T, slotName: string, hostDom: HTMLElement, $next: () => HTMLElement | null, editor: LexicalEditor, ) => HTMLElement | null; /** * Set via {@link domOverride}'s options argument, not directly. See * {@link DOMOverrideOptions.disabledForEditor}. */ disabledForEditor?: (ctx: RenderContextReader) => boolean; /** * Set via {@link domOverride}'s options argument, not directly. See * {@link DOMOverrideOptions.disabledForSession}. */ disabledForSession?: (ctx: RenderContextReader) => boolean; } /** * @experimental * * The hook fields of a {@link DOMRenderMatch} — i.e. without `nodes` or the * {@link DOMOverrideOptions} predicates, which are passed separately to * {@link domOverride}. */ export type DOMRenderMatchConfig = Omit< DOMRenderMatch, 'nodes' | keyof DOMOverrideOptions >;