@tiptap/core
Version:
headless rich text editor
914 lines (839 loc) • 25.9 kB
text/typescript
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
}