UNPKG

@tiptap/core

Version:

headless rich text editor

914 lines (839 loc) 25.9 kB
import type { Mark as ProseMirrorMark, Node as ProseMirrorNode, ParseOptions, Slice } from '@tiptap/pm/model' import type { EditorState, Transaction } from '@tiptap/pm/state' import type { Mappable, Transform } from '@tiptap/pm/transform' import type { Decoration, DecorationAttrs, EditorProps, EditorView, MarkView, MarkViewConstructor, NodeView, NodeViewConstructor, ViewMutationRecord, } from '@tiptap/pm/view' import type { Editor } from './Editor.js' import type { Extendable } from './Extendable.js' import type { Commands, ExtensionConfig, MarkConfig, NodeConfig } from './index.js' import type { Mark } from './Mark.js' import type { Node } from './Node.js' export type AnyConfig = ExtensionConfig | NodeConfig | MarkConfig export type AnyExtension = Extendable export type Extensions = AnyExtension[] export type ParentConfig<T> = Partial<{ [P in keyof T]: Required<T>[P] extends (...args: any) => any ? (...args: Parameters<Required<T>[P]>) => ReturnType<Required<T>[P]> : T[P] }> export type Primitive = null | undefined | string | number | boolean | symbol | bigint export type RemoveThis<T> = T extends (...args: any) => any ? (...args: Parameters<T>) => ReturnType<T> : T export type MaybeReturnType<T> = T extends (...args: any) => any ? ReturnType<T> : T export type MaybeThisParameterType<T> = Exclude<T, Primitive> extends (...args: any) => any ? ThisParameterType<Exclude<T, Primitive>> : any export interface EditorEvents { mount: { /** * The editor instance */ editor: Editor } unmount: { /** * The editor instance */ editor: Editor } beforeCreate: { /** * The editor instance */ editor: Editor } create: { /** * The editor instance */ editor: Editor } contentError: { /** * The editor instance */ editor: Editor /** * The error that occurred while parsing the content */ error: Error /** * If called, will re-initialize the editor with the collaboration extension removed. * This will prevent syncing back deletions of content not present in the current schema. */ disableCollaboration: () => void } update: { /** * The editor instance */ editor: Editor /** * The transaction that caused the update */ transaction: Transaction /** * Appended transactions that were added to the initial transaction by plugins */ appendedTransactions: Transaction[] } selectionUpdate: { /** * The editor instance */ editor: Editor /** * The transaction that caused the selection update */ transaction: Transaction } beforeTransaction: { /** * The editor instance */ editor: Editor /** * The transaction that will be applied */ transaction: Transaction /** * The next state of the editor after the transaction is applied */ nextState: EditorState } transaction: { /** * The editor instance */ editor: Editor /** * The initial transaction */ transaction: Transaction /** * Appended transactions that were added to the initial transaction by plugins */ appendedTransactions: Transaction[] } focus: { /** * The editor instance */ editor: Editor /** * The focus event */ event: FocusEvent /** * The transaction that caused the focus */ transaction: Transaction } blur: { /** * The editor instance */ editor: Editor /** * The focus event */ event: FocusEvent /** * The transaction that caused the blur */ transaction: Transaction } destroy: void paste: { /** * The editor instance */ editor: Editor /** * The clipboard event */ event: ClipboardEvent /** * The slice that was pasted */ slice: Slice } drop: { /** * The editor instance */ editor: Editor /** * The drag event */ event: DragEvent /** * The slice that was dropped */ slice: Slice /** * Whether the content was moved (true) or copied (false) */ moved: boolean } delete: { /** * The editor instance */ editor: Editor /** * The range of the deleted content (before the deletion) */ deletedRange: Range /** * The new range of positions of where the deleted content was in the new document (after the deletion) */ newRange: Range /** * The transaction that caused the deletion */ transaction: Transaction /** * The combined transform (including all appended transactions) that caused the deletion */ combinedTransform: Transform /** * Whether the deletion was partial (only a part of this content was deleted) */ partial: boolean /** * This is the start position of the mark in the document (before the deletion) */ from: number /** * This is the end position of the mark in the document (before the deletion) */ to: number } & ( | { /** * The content that was deleted */ type: 'node' /** * The node which the deletion occurred in * @note This can be a parent node of the deleted content */ node: ProseMirrorNode /** * The new start position of the node in the document (after the deletion) */ newFrom: number /** * The new end position of the node in the document (after the deletion) */ newTo: number } | { /** * The content that was deleted */ type: 'mark' /** * The mark that was deleted */ mark: ProseMirrorMark } ) } export type EnableRules = (AnyExtension | string)[] | boolean export interface EditorOptions { /** * The element to bind the editor to: * - If an `Element` is passed, the editor will be mounted appended to that element * - If `null` is passed, the editor will not be mounted automatically * - If an object with a `mount` property is passed, the editor will be mounted to that element * - If a function is passed, it will be called with the editor's element, which should place the editor within the document */ element: Element | { mount: HTMLElement } | ((editor: HTMLElement) => void) | null /** * The content of the editor (HTML, JSON, or a JSON array) */ content: Content /** * The extensions to use */ extensions: Extensions /** * Whether to inject base CSS styles */ injectCSS: boolean /** * A nonce to use for CSP while injecting styles */ injectNonce: string | undefined /** * The editor's initial focus position */ autofocus: FocusPosition /** * Whether the editor is editable */ editable: boolean /** * The editor's props */ editorProps: EditorProps /** * The editor's content parser options */ parseOptions: ParseOptions /** * The editor's core extension options */ coreExtensionOptions?: { clipboardTextSerializer?: { blockSeparator?: string } delete?: { /** * Whether the `delete` extension should be called asynchronously to avoid blocking the editor while processing deletions * @default true deletion events are called asynchronously */ async?: boolean /** * Allows filtering the transactions that are processed by the `delete` extension. * If the function returns `true`, the transaction will be ignored. */ filterTransaction?: (transaction: Transaction) => boolean } } /** * Whether to enable input rules behavior */ enableInputRules: EnableRules /** * Whether to enable paste rules behavior */ enablePasteRules: EnableRules /** * Determines whether core extensions are enabled. * * If set to `false`, all core extensions will be disabled. * To disable specific core extensions, provide an object where the keys are the extension names and the values are `false`. * Extensions not listed in the object will remain enabled. * * @example * // Disable all core extensions * enabledCoreExtensions: false * * @example * // Disable only the keymap core extension * enabledCoreExtensions: { keymap: false } * * @default true */ enableCoreExtensions?: | boolean | Partial< Record< | 'editable' | 'clipboardTextSerializer' | 'commands' | 'focusEvents' | 'keymap' | 'tabindex' | 'drop' | 'paste' | 'delete', false > > /** * If `true`, the editor will check the content for errors on initialization. * Emitting the `contentError` event if the content is invalid. * Which can be used to show a warning or error message to the user. * @default false */ enableContentCheck: boolean /** * If `true`, the editor will emit the `contentError` event if invalid content is * encountered but `enableContentCheck` is `false`. This lets you preserve the * invalid editor content while still showing a warning or error message to * the user. * * @default false */ emitContentError: boolean /** * Called before the editor is constructed. */ onBeforeCreate: (props: EditorEvents['beforeCreate']) => void /** * Called after the editor is constructed. */ onCreate: (props: EditorEvents['create']) => void /** * Called when the editor is mounted. */ onMount: (props: EditorEvents['mount']) => void /** * Called when the editor is unmounted. */ onUnmount: (props: EditorEvents['unmount']) => void /** * Called when the editor encounters an error while parsing the content. * Only enabled if `enableContentCheck` is `true`. */ onContentError: (props: EditorEvents['contentError']) => void /** * Called when the editor's content is updated. */ onUpdate: (props: EditorEvents['update']) => void /** * Called when the editor's selection is updated. */ onSelectionUpdate: (props: EditorEvents['selectionUpdate']) => void /** * Called after a transaction is applied to the editor. */ onTransaction: (props: EditorEvents['transaction']) => void /** * Called on focus events. */ onFocus: (props: EditorEvents['focus']) => void /** * Called on blur events. */ onBlur: (props: EditorEvents['blur']) => void /** * Called when the editor is destroyed. */ onDestroy: (props: EditorEvents['destroy']) => void /** * Called when content is pasted into the editor. */ onPaste: (e: ClipboardEvent, slice: Slice) => void /** * Called when content is dropped into the editor. */ onDrop: (e: DragEvent, slice: Slice, moved: boolean) => void /** * Called when content is deleted from the editor. */ onDelete: (props: EditorEvents['delete']) => void } /** * The editor's content as HTML */ export type HTMLContent = string /** * Loosely describes a JSON representation of a Prosemirror document or node */ export type JSONContent = { type?: string attrs?: Record<string, any> | undefined content?: JSONContent[] marks?: { type: string attrs?: Record<string, any> [key: string]: any }[] text?: string [key: string]: any } /** * A mark type is either a JSON representation of a mark or a Prosemirror mark instance */ export type MarkType< Type extends string | { name: string } = any, TAttributes extends undefined | Record<string, any> = any, > = { type: Type attrs: TAttributes } /** * A node type is either a JSON representation of a node or a Prosemirror node instance */ export type NodeType< Type extends string | { name: string } = any, TAttributes extends undefined | Record<string, any> = any, NodeMarkType extends MarkType = any, TContent extends (NodeType | TextType)[] = any, > = { type: Type attrs: TAttributes content?: TContent marks?: NodeMarkType[] } /** * A node type is either a JSON representation of a doc node or a Prosemirror doc node instance */ export type DocumentType< TDocAttributes extends Record<string, any> | undefined = Record<string, any>, TContentType extends NodeType[] = NodeType[], > = Omit<NodeType<'doc', TDocAttributes, never, TContentType>, 'marks' | 'content'> & { content: TContentType } /** * A node type is either a JSON representation of a text node or a Prosemirror text node instance */ export type TextType<TMarkType extends MarkType = MarkType> = { type: 'text' text: string marks: TMarkType[] } /** * Describes the output of a `renderHTML` function in prosemirror * @see https://prosemirror.net/docs/ref/#model.DOMOutputSpec */ export type DOMOutputSpecArray = | [string] | [string, Record<string, any>] | [string, 0] | [string, Record<string, any>, 0] | [string, Record<string, any>, DOMOutputSpecArray | 0] | [string, DOMOutputSpecArray] export type Content = HTMLContent | JSONContent | JSONContent[] | null export type CommandProps = { editor: Editor tr: Transaction commands: SingleCommands can: () => CanCommands chain: () => ChainedCommands state: EditorState view: EditorView dispatch: ((args?: any) => any) | undefined } export type Command = (props: CommandProps) => boolean export type CommandSpec = (...args: any[]) => Command export type KeyboardShortcutCommand = (props: { editor: Editor }) => boolean export type Attribute = { default?: any validate?: string | ((value: any) => void) rendered?: boolean renderHTML?: ((attributes: Record<string, any>) => Record<string, any> | null) | null parseHTML?: ((element: HTMLElement) => any | null) | null keepOnSplit?: boolean isRequired?: boolean } export type Attributes = { [key: string]: Attribute } export type ExtensionAttribute = { type: string name: string attribute: Required<Omit<Attribute, 'validate'>> & Pick<Attribute, 'validate'> } export type GlobalAttributes = { /** * The node & mark types this attribute should be applied to. */ types: string[] /** * The attributes to add to the node or mark types. */ attributes: Record<string, Attribute | undefined> }[] export type PickValue<T, K extends keyof T> = T[K] export type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never export type Diff<T extends keyof any, U extends keyof any> = ({ [P in T]: P } & { [P in U]: never } & { [x: string]: never })[T] export type Overwrite<T, U> = Pick<T, Diff<keyof T, keyof U>> & U export type ValuesOf<T> = T[keyof T] export type KeysWithTypeOf<T, Type> = { [P in keyof T]: T[P] extends Type ? P : never }[keyof T] export type DOMNode = InstanceType<typeof window.Node> /** * prosemirror-view does not export the `type` property of `Decoration`. * So, this defines the `DecorationType` interface to include the `type` property. */ export interface DecorationType { spec: any map(mapping: Mappable, span: Decoration, offset: number, oldOffset: number): Decoration | null valid(node: Node, span: Decoration): boolean eq(other: DecorationType): boolean destroy(dom: DOMNode): void readonly attrs: DecorationAttrs } /** * prosemirror-view does not export the `type` property of `Decoration`. * This adds the `type` property to the `Decoration` type. */ export type DecorationWithType = Decoration & { type: DecorationType } export interface NodeViewProps extends NodeViewRendererProps { // TODO this type is not technically correct, but it's the best we can do for now since prosemirror doesn't expose the type of decorations decorations: readonly DecorationWithType[] selected: boolean updateAttributes: (attributes: Record<string, any>) => void deleteNode: () => void } export interface NodeViewRendererOptions { stopEvent: ((props: { event: Event }) => boolean) | null ignoreMutation: ((props: { mutation: ViewMutationRecord }) => boolean) | null contentDOMElementTag: string } export interface NodeViewRendererProps { // pass-through from prosemirror /** * The node that is being rendered. */ node: Parameters<NodeViewConstructor>[0] /** * The editor's view. */ view: Parameters<NodeViewConstructor>[1] /** * A function that can be called to get the node's current position in the document. */ getPos: Parameters<NodeViewConstructor>[2] /** * is an array of node or inline decorations that are active around the node. * They are automatically drawn in the normal way, and you will usually just want to ignore this, but they can also be used as a way to provide context information to the node view without adding it to the document itself. */ decorations: Parameters<NodeViewConstructor>[3] /** * holds the decorations for the node's content. You can safely ignore this if your view has no content or a contentDOM property, since the editor will draw the decorations on the content. * But if you, for example, want to create a nested editor with the content, it may make sense to provide it with the inner decorations. */ innerDecorations: Parameters<NodeViewConstructor>[4] // tiptap-specific /** * The editor instance. */ editor: Editor /** * The extension that is responsible for the node. */ extension: Node /** * The HTML attributes that should be added to the node's DOM element. */ HTMLAttributes: Record<string, any> } export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface MarkViewProps extends MarkViewRendererProps {} export interface MarkViewRendererProps { // pass-through from prosemirror /** * The node that is being rendered. */ mark: Parameters<MarkViewConstructor>[0] /** * The editor's view. */ view: Parameters<MarkViewConstructor>[1] /** * indicates whether the mark's content is inline */ inline: Parameters<MarkViewConstructor>[2] // tiptap-specific /** * The editor instance. */ editor: Editor /** * The extension that is responsible for the mark. */ extension: Mark /** * The HTML attributes that should be added to the mark's DOM element. */ HTMLAttributes: Record<string, any> updateAttributes: (attrs: Record<string, any>) => void } export type MarkViewRenderer<Props = MarkViewRendererProps> = (props: Props) => MarkView export interface MarkViewRendererOptions { ignoreMutation: ((props: { mutation: ViewMutationRecord }) => boolean) | null } export type AnyCommands = Record<string, (...args: any[]) => Command> export type UnionCommands<T = Command> = UnionToIntersection< ValuesOf<Pick<Commands<T>, KeysWithTypeOf<Commands<T>, object>>> > export type RawCommands = { [Item in keyof UnionCommands]: UnionCommands<Command>[Item] } export type SingleCommands = { [Item in keyof UnionCommands]: UnionCommands<boolean>[Item] } export type ChainedCommands = { [Item in keyof UnionCommands]: UnionCommands<ChainedCommands>[Item] } & { run: () => boolean } export type CanCommands = SingleCommands & { chain: () => ChainedCommands } export type FocusPosition = 'start' | 'end' | 'all' | number | boolean | null export type Range = { from: number to: number } export type NodeRange = { node: ProseMirrorNode from: number to: number } export type MarkRange = { mark: ProseMirrorMark from: number to: number } export type Predicate = (node: ProseMirrorNode) => boolean export type NodeWithPos = { node: ProseMirrorNode pos: number } export type TextSerializer = (props: { node: ProseMirrorNode pos: number parent: ProseMirrorNode index: number range: Range }) => string export type ExtendedRegExpMatchArray = RegExpMatchArray & { data?: Record<string, any> } export type Dispatch = ((args?: any) => any) | undefined /** Markdown related types */ // Shared markdown-related types for the MarkdownManager and extensions. export type MarkdownToken = { type?: string raw?: string text?: string tokens?: MarkdownToken[] depth?: number items?: MarkdownToken[] [key: string]: any } export type MarkdownHelpers = { // When used during parsing these helpers return JSON-like node objects // (not ProseMirror Node instances). Use `any` to represent that shape. parseInline: (tokens: MarkdownToken[]) => any[] /** * Render children. The second argument may be a legacy separator string * or a RenderContext (preferred). */ renderChildren: (node: Node[] | Node, ctxOrSeparator?: RenderContext | string) => string text: (token: MarkdownToken) => any } /** * Helpers specifically for parsing markdown tokens into Tiptap JSON. * These are provided to extension parse handlers. */ export type MarkdownParseHelpers = { /** Parse an array of inline tokens into text nodes with marks */ parseInline: (tokens: MarkdownToken[]) => JSONContent[] /** Parse an array of block-level tokens */ parseChildren: (tokens: MarkdownToken[]) => JSONContent[] /** Create a text node with optional marks */ createTextNode: (text: string, marks?: Array<{ type: string; attrs?: any }>) => JSONContent /** Create any node type with attributes and content */ createNode: (type: string, attrs?: any, content?: JSONContent[]) => JSONContent /** Apply a mark to content (used for inline marks like bold, italic) */ applyMark: ( markType: string, content: JSONContent[], attrs?: any, ) => { mark: string; content: JSONContent[]; attrs?: any } } /** * Full runtime helpers object provided by MarkdownManager to handlers. * This includes the small author-facing helpers plus internal helpers * that can be useful for advanced handlers. */ export type FullMarkdownHelpers = MarkdownHelpers & { // parseChildren returns JSON-like nodes when invoked during parsing. parseChildren: (tokens: MarkdownToken[]) => any[] getExtension: (name: string) => any // createNode returns a JSON-like node during parsing; render-time helpers // may instead work with real ProseMirror Node instances. createNode: (type: string, attrs?: any, content?: any[]) => any /** Current render context when calling renderers; undefined during parse. */ currentContext?: RenderContext /** Indent a multi-line string according to the provided RenderContext. */ indent: (text: string, ctx?: RenderContext) => string /** Return the indent string for a given level (e.g. ' ' or '\t'). */ getIndentString: (level?: number) => string } export default MarkdownHelpers /** * Return shape for parser-level `parse` handlers. * - a single JSON-like node * - an array of JSON-like nodes * - or a `{ mark: string, content: JSONLike[] }` shape to apply a mark */ export type MarkdownParseResult = JSONContent | JSONContent[] | { mark: string; content: JSONContent[]; attrs?: any } export type RenderContext = { index: number level: number meta?: Record<string, any> parentType?: string | null } /** Extension contract for markdown parsing/serialization. */ export interface MarkdownExtensionSpec { /** Token name used for parsing (e.g., 'codespan', 'code', 'strong') */ tokenName?: string /** Node/mark name used for rendering (typically the extension name) */ nodeName?: string parseMarkdown?: (token: MarkdownToken, helpers: MarkdownParseHelpers) => MarkdownParseResult renderMarkdown?: (node: any, helpers: MarkdownRendererHelpers, ctx: RenderContext) => string isIndenting?: boolean /** Custom tokenizer for marked.js to handle non-standard markdown syntax */ tokenizer?: MarkdownTokenizer } /** * Configuration object passed to custom marked.js tokenizers */ export type MarkdownLexerConfiguration = { /** * Can be used to transform source text into inline tokens - useful while tokenizing child tokens. * @param src * @returns Array of inline tokens */ inlineTokens: (src: string) => MarkdownToken[] /** * Can be used to transform source text into block-level tokens - useful while tokenizing child tokens. * @param src * @returns Array of block-level tokens */ blockTokens: (src: string) => MarkdownToken[] } /** Custom tokenizer function for marked.js extensions */ export type MarkdownTokenizer = { /** Token name this tokenizer creates */ name: string /** Priority level for tokenizer ordering (higher = earlier) */ level?: 'block' | 'inline' /** A string to look for or a function that returns the start index of the token in the source string */ start?: string | ((src: string) => number) /** Function that attempts to parse custom syntax from start of text */ tokenize: ( src: string, tokens: MarkdownToken[], lexer: MarkdownLexerConfiguration, ) => MarkdownToken | undefined | void } export type MarkdownRendererHelpers = { /** * Render children nodes to a markdown string, optionally separated by a string. * @param nodes The node or array of nodes to render * @param separator An optional separator string (legacy) or RenderContext * @returns The rendered markdown string */ renderChildren: (nodes: JSONContent | JSONContent[], separator?: string) => string /** * Render a text token to a markdown string * @param prefix The prefix to add before the content * @param content The content to wrap * @returns The wrapped content */ wrapInBlock: (prefix: string, content: string) => string /** * Indent a markdown string according to the provided RenderContext * @param content The content to indent * @returns The indented content */ indent: (content: string) => string }