UNPKG

@portabletext/editor

Version:

Portable Text Editor made in React

688 lines 23.5 kB
import { FieldDefinition, OfDefinition, PortableTextObject, PortableTextSpan, PortableTextTextBlock, Schema } from "@portabletext/schema"; import { ReactElement } from "react"; /** * A segment in a path that identifies an element by its `_key` property. * @public */ interface KeyedSegment { _key: string; } /** * A tuple representing a range selection, e.g., `[0, 5]` or `['', 3]`. * @public */ type IndexTuple = [number | '', number | '']; /** * A single segment in a path. Can be: * - A string (property name) * - A number (array index) * - A KeyedSegment (object with `_key`) * - An IndexTuple (range selection) * @public */ type PathSegment$1 = string | number | KeyedSegment | IndexTuple; /** * A path is an array of path segments that describes a location in a document. * @public */ type Path$1 = PathSegment$1[]; /** * A path to a block in the value. * * @public */ type BlockPath = Path$1; /** * A path to an annotation markDef on a block. * * @public */ type AnnotationPath = Path$1; /** * A path to a child of a text block. * * @public */ type ChildPath = Path$1; /** * A path segment identifies a position in the document tree. * * - `KeyedSegment` (`{_key: string}`) identifies a node by its key * - `string` identifies a child field name (e.g. 'children', 'rows', 'cells') * - `number` identifies a position in an array (used for empty container inserts * and rendering indexed paths) * - `IndexTuple` (`[number | '', number | '']`) represents a range selection */ type PathSegment = KeyedSegment | string | number | IndexTuple; /** * A `Path` is a list of segments that describe a node's exact position in * the document tree. Segments alternate between keyed node references and * field names: `[{_key: 'b1'}, 'children', {_key: 's1'}]`. */ type Path = PathSegment[]; /** * @alpha * * Narrow the container node type by the registered `_type`. Containers * always render portable text objects: `'span'` is always a leaf and * `'block'` is always a text block; both are excluded. */ type ContainerNodeForType<TType extends string> = TType extends 'span' | 'block' ? never : PortableTextObject; /** * @alpha * * A container's render function receives a node and renders an element * that wraps its editable children. The render is positional: it fires for * nodes of `type` whose parent permits this container at `arrayField`. * * `node` is `PortableTextObject` because containers cannot register the * built-in `'span'` or `'block'` types (those are leaves and text blocks * respectively). */ type ContainerRenderProps = { attributes: Record<string, unknown>; children: ReactElement; focused: boolean; node: PortableTextObject; path: Path; readOnly: boolean; selected: boolean; /** * Render this position with the engine's default wrapper. Call from * inside a custom render to fall back to or wrap the default: * * ```ts * render: ({renderDefault, ...rest}) => renderDefault(rest) * ``` * * The default is the engine's minimal wrapper. It does not chain * back to a globally-registered render: PTE has one user layer plus * positional overrides, and the engine default is the canonical * fallback at any position. */ renderDefault: (props: ContainerRenderProps) => ReactElement; }; type ContainerRender = (props: ContainerRenderProps) => ReactElement; /** * @alpha * * A span's render function. Receives a portable text span node and * wraps it. `children` carries the styled text already decorated by * `renderDecorator`/`renderAnnotation`/range decorations. */ type SpanRenderProps = { attributes: Record<string, unknown>; children: ReactElement; focused: boolean; node: PortableTextSpan; path: Path; readOnly: boolean; selected: boolean; /** * Render this position with the engine's default wrapper. * See {@link ContainerRenderProps.renderDefault}. */ renderDefault: (props: SpanRenderProps) => ReactElement; }; type SpanRender = (props: SpanRenderProps) => ReactElement; /** * @alpha * * A block object's render function. Receives a non-editable block-level * portable text object. `children` carries an engine-emitted void * spacer that the browser uses to anchor the caret next to the * element. Dropping `children` makes the caret unable to land on the * element. */ type BlockObjectRenderProps = { attributes: Record<string, unknown>; children: ReactElement; focused: boolean; node: PortableTextObject; path: Path; readOnly: boolean; selected: boolean; /** * Render this position with the engine's default wrapper. * See {@link ContainerRenderProps.renderDefault}. */ renderDefault: (props: BlockObjectRenderProps) => ReactElement; }; type BlockObjectRender = (props: BlockObjectRenderProps) => ReactElement; /** * @alpha * * An inline object's render function. Receives a non-editable inline * portable text object. `children` carries an engine-emitted void * spacer that the browser uses to anchor the caret next to the * element. Dropping `children` makes the caret unable to land on the * element. */ type InlineObjectRenderProps = { attributes: Record<string, unknown>; children: ReactElement; focused: boolean; node: PortableTextObject; path: Path; readOnly: boolean; selected: boolean; /** * Render this position with the engine's default wrapper. * See {@link ContainerRenderProps.renderDefault}. */ renderDefault: (props: InlineObjectRenderProps) => ReactElement; }; type InlineObjectRender = (props: InlineObjectRenderProps) => ReactElement; /** * @alpha * * A container registration. Identifies a block object `_type` whose value * holds editable children in `arrayField`. The optional `of` array carries * nested registrations that override how immediate children of this * container render at this lexical scope. * * `of` overrides apply ONE level down only. Children at deeper levels fall * through to global registrations. * * The `kind` field is injected by `defineContainer` and discriminates * containers from other registration kinds at runtime. */ type Container = { kind: 'container'; type: string; arrayField: string; /** * Outer render. Two modes: * - omitted: fall through to global registered render (or engine default) * - function: use this render. The function receives a `renderDefault` * prop that returns the engine default when called. */ render?: ContainerRender; /** * Block-level positional overrides. Inline-content kinds (`Span`, * `InlineObject`) belong in `TextBlock.of`, not here. */ of?: ReadonlyArray<Container | TextBlock | BlockObject>; }; /** * @alpha * * A text block registration. The text block `_type` is `'block'` at the * top level. Positional overrides nested in a container's `of` array can * register a different `_type` to render at that lexical scope. * * `defineTextBlock` opts the text block into the new render pipeline. * The consumer's `render` callback owns the outer wrapper entirely: * the engine emits `data-pt-*` attributes only - no `pt-*` CSS classes, * no legacy `data-block-*` attributes - and the block-level * `renderStyle`/`renderListItem`/`renderBlock` props on * `<PortableTextEditable>` do not compose under this registration. * * Span-level render props - `renderDecorator`, `renderAnnotation`, * `renderPlaceholder`, and range decorations - keep working. They fire * on the spans inside `children` regardless of which text block outer * wrapper renders them. */ type TextBlock = { kind: 'textBlock'; type: string; /** * Outer render. Two modes: * - omitted: fall through to global registered render (or engine default) * - function: use this render. The function receives a `renderDefault` * prop that returns the engine default when called. */ render?: TextBlockRender; /** * Inline-content positional overrides. A `Span` or `InlineObject` * placed here scopes the inline render to this text block (or any * text block of this `type` if registered at the top level). */ of?: ReadonlyArray<Span | InlineObject>; }; /** * @alpha * * Text block render function. `children` carries the rendered spans - * `renderDecorator`, `renderAnnotation`, `renderPlaceholder`, and range * decorations have already fired at the leaf level. The render's job * is the outer wrapper element and any block-level composition (style, * list-item) the consumer wants. */ type TextBlockRenderProps = { attributes: Record<string, unknown>; children: ReactElement; focused: boolean; node: PortableTextTextBlock; path: Path; readOnly: boolean; selected: boolean; /** * Render this position with the engine's default wrapper. * See {@link ContainerRenderProps.renderDefault}. */ renderDefault: (props: TextBlockRenderProps) => ReactElement; }; type TextBlockRender = (props: TextBlockRenderProps) => ReactElement; /** * @alpha * * A span registration. The span `_type` is `'span'` at the top level. * Positional overrides nested in a container's `of` array can register * a different `_type` for a span-like inline at that lexical scope * (e.g. a `code-span` inside a `code-block`). */ type Span = { kind: 'span'; type: string; /** * Outer render. Two modes: * - omitted: fall through to global registered render (or engine default) * - function: use this render. The function receives a `renderDefault` * prop that returns the engine default when called. */ render?: SpanRender; }; /** * @alpha * * A non-editable block-level object registration. Identifies a `_type` * whose value renders as a block-level void node (image, embed, etc.). */ type BlockObject = { kind: 'blockObject'; type: string; /** * Outer render. Two modes: * - omitted: fall through to global registered render (or engine default) * - function: use this render. The function receives a `renderDefault` * prop that returns the engine default when called. */ render?: BlockObjectRender; }; /** * @alpha * * A non-editable inline object registration. Identifies a `_type` whose * value renders as an inline void node (mention, inline image, etc.). */ type InlineObject = { kind: 'inlineObject'; type: string; /** * Outer render. Two modes: * - omitted: fall through to global registered render (or engine default) * - function: use this render. The function receives a `renderDefault` * prop that returns the engine default when called. */ render?: InlineObjectRender; }; /** * @alpha * * The discriminated union of every registration accepted by * `editor.registerNode` and the `<NodePlugin>` component. */ type RegistrableNode = Container | TextBlock | Span | BlockObject | InlineObject; /** * @alpha * * Define a container renderer. The returned registration is mounted via * the `<NodePlugin>` component at the top level, or nested inside * another container's `of` array as a positional override. * * `type` cannot be `'span'` (use {@link defineSpan}) nor `'block'` (use * {@link defineTextBlock}). The text block is not a container. * * The `node` argument of `render` narrows to a portable text object. * * @example * ```ts * defineContainer({ * type: 'table', * arrayField: 'rows', * render: ({children}) => ( * <table>{children}</table> * ), * of: [ * defineContainer({ * type: 'row', * arrayField: 'cells', * render: ({children}) => ( * <tr>{children}</tr> * ), * }), * ], * }) * ``` */ declare function defineContainer<const TType extends string>(config: { type: TType extends 'span' ? "Error: defineContainer({type: 'span'}) is forbidden -- 'span' is always a span, use defineSpan" : TType extends 'block' ? "Error: defineContainer({type: 'block'}) is forbidden -- 'block' is always a text block, use defineTextBlock" : TType; arrayField: string; render?: (props: { attributes: Record<string, unknown>; children: ReactElement; focused: boolean; node: ContainerNodeForType<TType>; path: Path; readOnly: boolean; selected: boolean; renderDefault: (props: ContainerRenderProps) => ReactElement; }) => ReactElement; of?: ReadonlyArray<Container | TextBlock | BlockObject>; }): Container; /** * @alpha * * Define a span renderer. The returned registration is mounted via the * `<NodePlugin>` component at the top level, or nested inside a * container's `of` array as a positional override. * * `type` is required even though there is only one top-level span type * (`'span'`) today. Keeping `type` required leaves the door open for * positional overrides of span-like inlines (e.g. a `code-span` inside * a `code-block` container). * * @example * ```ts * defineSpan({ * type: 'span', * render: ({attributes, children}) => ( * <span {...attributes}>{children}</span> * ), * }) * ``` */ declare function defineSpan<const TType extends string>(config: { type: TType extends 'block' ? "Error: defineSpan({type: 'block'}) is forbidden -- 'block' is always a text block, use defineTextBlock" : TType; render?: SpanRender; }): Span; /** * @alpha * * Define a non-editable block-level object renderer for a `_type` * declared in the schema's `blockObjects` array. * * The render must always render `children` somewhere inside the outer * element. `children` carries an engine-emitted void spacer the browser * uses to anchor the caret next to the element. Dropping `children` * makes the caret unable to land on the element. * * @example * ```ts * defineBlockObject({ * type: 'image', * render: ({attributes, children, node}) => ( * <div {...attributes}> * {children} * <img src={(node as {src?: string}).src} /> * </div> * ), * }) * ``` */ declare function defineBlockObject<const TType extends string>(config: { type: TType extends 'block' ? "Error: defineBlockObject({type: 'block'}) is forbidden -- 'block' is always a text block, use defineTextBlock" : TType extends 'span' ? "Error: defineBlockObject({type: 'span'}) is forbidden -- 'span' is always a span, use defineSpan" : TType; render?: BlockObjectRender; }): BlockObject; /** * @alpha * * Define a non-editable inline object renderer for a `_type` declared * in the schema's `inlineObjects` array. * * The render must always render `children` somewhere inside the outer * element. `children` carries an engine-emitted void spacer the browser * uses to anchor the caret next to the element. Dropping `children` * makes the caret unable to land on the element. * * @example * ```ts * defineInlineObject({ * type: 'mention', * render: ({attributes, children, node}) => ( * <span {...attributes}> * {children} * @{(node as {username?: string}).username} * </span> * ), * }) * ``` */ declare function defineInlineObject<const TType extends string>(config: { type: TType extends 'block' ? "Error: defineInlineObject({type: 'block'}) is forbidden -- 'block' is always a text block, use defineTextBlock" : TType extends 'span' ? "Error: defineInlineObject({type: 'span'}) is forbidden -- 'span' is always a span, use defineSpan" : TType; render?: InlineObjectRender; }): InlineObject; /** * @alpha * * Define a text block renderer. The returned registration is mounted * via the `<NodePlugin>` component, or nested inside a container's * `of` array as a positional override. * * `type` is required even though the top-level text block type is * always `'block'`. Keeping `type` required leaves the door open for * positional overrides of text-block-like elements (e.g. a `code-line` * inside a `code-block` container). * * @example * ```ts * defineTextBlock({ * type: 'block', * render: ({attributes, children}) => ( * <p {...attributes}>{children}</p> * ), * }) * ``` */ declare function defineTextBlock<const TType extends string>(config: { type: TType extends 'span' ? "Error: defineTextBlock({type: 'span'}) is forbidden -- 'span' is always a span, use defineSpan" : TType; render?: TextBlockRender; of?: ReadonlyArray<Span | InlineObject>; }): TextBlock; /** * @internal * * Resolved span config. */ type SpanConfig = { span: Span; }; /** * @internal * * Resolved block-object config. */ type BlockObjectConfig = { blockObject: BlockObject; }; /** * @internal * * Resolved inline-object config. */ type InlineObjectConfig = { inlineObject: InlineObject; }; /** * @internal * * Resolved container config carrying the pre-resolved `field` for the * activation position. Dispatch reads pre-resolved data without * re-walking the schema. */ type ContainerConfig = { container: Container; field: ChildArrayField; of?: ReadonlyArray<ContainerConfig | BlockObjectConfig | TextBlockConfig>; }; /** * @internal * * Resolved text block config. The optional `of` carries resolved * inline-content positional overrides (spans, inline-objects) for * children rendered inside this text block. */ type TextBlockConfig = { textBlock: TextBlock; of?: ReadonlyArray<SpanConfig | InlineObjectConfig>; }; type ChildArrayField = FieldDefinition & { type: 'array'; of: ReadonlyArray<OfDefinition>; }; /** * Public view of a registered editable container, surfaced on * {@link EditorContext.containers}. * * Two array properties named `of` live on the same entry with * different semantics: * * - `field.of` is the SCHEMA-DECLARED list of types this container's * child field accepts (from `@portabletext/schema`'s * `OfDefinition`). Tells you what the schema permits as children. * - `of` (top-level on `RegisteredContainer`) is the list of * POSITIONAL CHILD REGISTRATIONS - nested * {@link RegisteredContainer} or {@link RegisteredPositional} * entries - that override the global registration when the engine * descends into this parent. Tells you which child renderings are * scoped to this parent. * * The full container registration (including the render callback) * lives on the editor's internal {@link ResolvedContainers} map and * is not exposed on the public context. * * Two top-level entries with the same `_type` cannot coexist - the * register handler warns on duplicates. But the SAME `_type` * registered in two different parents' `of` arrays is supported as * a feature; `resolveContainerAt` walks the positional tree using * the path to return the entry that applies at a given position. * * @alpha */ type RegisteredContainer = { kind: 'container'; type: string; field: ChildArrayField; of?: ReadonlyArray<RegisteredContainer | RegisteredPositional>; }; /** * Public view of a registered span, surfaced inside a containing * {@link RegisteredContainer}'s `of` array as a positional * registration. The render function is engine-internal. * * @alpha */ type RegisteredSpan = { kind: 'span'; type: string; }; /** * Public view of a registered block object, surfaced inside a * containing {@link RegisteredContainer}'s `of` array as a positional * registration. The render function is engine-internal. * * @alpha */ type RegisteredBlockObject = { kind: 'blockObject'; type: string; }; /** * Public view of a registered inline object, surfaced inside a * containing {@link RegisteredContainer}'s `of` array as a positional * registration. The render function is engine-internal. * * @alpha */ type RegisteredInlineObject = { kind: 'inlineObject'; type: string; }; /** * Union of non-container positional registrations that may appear in * a {@link RegisteredContainer}'s `of` array. Text-block registrations * are NOT included here; they surface on `EditorContext.textBlocks`, * not on the containers tree. * * @alpha */ type RegisteredPositional = RegisteredSpan | RegisteredBlockObject | RegisteredInlineObject; /** * Map of registered editable containers carried on `EditorContext`. * * Keyed by bare block-object `_type` (e.g. `'callout'`, `'table'`). * Each entry is a rich {@link RegisteredContainer} carrying its * `field` plus any positional `of` registrations. * * The map preserves positional structure: a `_type` declared inside * a parent's `of` array surfaces as a nested entry on that parent's * `of`, NOT as a separate top-level entry. Path-driven resolution * (see `resolveContainerAt`) reaches positional entries by walking * the tree. * * Top-level entries are global fallbacks: when path-driven descent * does not find a positional override, the resolver falls back to the * top-level entry for the type if one is registered. * * @alpha */ type Containers = ReadonlyMap<string, RegisteredContainer>; /** * Engine-internal map carrying the fully-resolved container * configurations - including render functions and positional `of` * overrides. Lives on `editor.containers` and is consulted by render * dispatch and engine-internal helpers. * * Not exposed on {@link EditorContext}. */ type ResolvedContainers = Map<string, ContainerConfig>; /** * @public */ type EditorSchema = Schema; type Node = PortableTextTextBlock | PortableTextObject | PortableTextSpan; /** * Snapshot-shaped input for traversal utilities. * * Mirrors the shape of `EditorSnapshot`: ambient state lives under * `context`, the `blockIndexMap` perf cache sits as a sibling. */ type TraversalSnapshot = { context: { schema: EditorSchema; containers: Containers; value: Array<Node>; }; blockIndexMap: Map<string, number>; }; /** * Walk the editor value following `path` and return the * {@link RegisteredContainer} or {@link RegisteredPositional} that applies * at `path`'s target position. * * Resolution rules at each step: * * 1. **Positional override.** If the current parent declares the * child's `_type` in its `of`, the positional entry wins. * Used to resolve same-`_type` registered under different * parents with different `field` values. * * 2. **Global fallback.** If the parent has no positional override, * fall back to the top-level entry for `_type` in * `containers`. * * 3. **Chain validity.** If any ancestor along the path has no * resolved container entry (unregistered or not reachable as a * container at its position), return `undefined`. * * Returns `undefined` when the target's `_type` is not registered * at this position. Returns a {@link RegisteredPositional} when the target * resolves to a leaf in a positional `of` (terminal node with no * editable children). * * @alpha */ declare function resolveContainerAt(containers: Containers, value: ReadonlyArray<Node>, path: Path): RegisteredContainer | RegisteredPositional | undefined; export { ChildPath as A, defineContainer as C, Path as D, defineTextBlock as E, Path$1 as M, AnnotationPath as O, defineBlockObject as S, defineSpan as T, RegistrableNode as _, Containers as a, TextBlock as b, RegisteredInlineObject as c, ResolvedContainers as d, BlockObject as f, InlineObjectConfig as g, InlineObject as h, EditorSchema as i, KeyedSegment as j, BlockPath as k, RegisteredPositional as l, Container as m, TraversalSnapshot as n, RegisteredBlockObject as o, BlockObjectConfig as p, Node as r, RegisteredContainer as s, resolveContainerAt as t, RegisteredSpan as u, Span as v, defineInlineObject as w, TextBlockConfig as x, SpanConfig as y }; //# sourceMappingURL=resolve-containers.d.ts.map