UNPKG

@tiptap/core

Version:

headless rich text editor

669 lines (601 loc) 16.7 kB
import { DOMOutputSpec, Mark as ProseMirrorMark, MarkSpec, MarkType, } from '@tiptap/pm/model' import { Plugin, Transaction } from '@tiptap/pm/state' import { Editor } from './Editor.js' import { getExtensionField } from './helpers/getExtensionField.js' import { MarkConfig } from './index.js' import { InputRule } from './InputRule.js' import { Node } from './Node.js' import { PasteRule } from './PasteRule.js' import { AnyConfig, Attributes, Extensions, GlobalAttributes, KeyboardShortcutCommand, ParentConfig, RawCommands, } from './types.js' import { callOrReturn } from './utilities/callOrReturn.js' import { mergeDeep } from './utilities/mergeDeep.js' declare module '@tiptap/core' { export interface MarkConfig<Options = any, Storage = any> { // @ts-ignore - this is a dynamic key [key: string]: any /** * The extension name - this must be unique. * It will be used to identify the extension. * * @example 'myExtension' */ name: string /** * The priority of your extension. The higher, the earlier it will be called * and will take precedence over other extensions with a lower priority. * @default 100 * @example 101 */ priority?: number /** * The default options for this extension. * @example * defaultOptions: { * myOption: 'foo', * myOtherOption: 10, * } */ defaultOptions?: Options /** * This method will add options to this extension * @see https://tiptap.dev/guide/custom-extensions#settings * @example * addOptions() { * return { * myOption: 'foo', * myOtherOption: 10, * } */ addOptions?: (this: { name: string parent: Exclude<ParentConfig<MarkConfig<Options, Storage>>['addOptions'], undefined> }) => Options /** * The default storage this extension can save data to. * @see https://tiptap.dev/guide/custom-extensions#storage * @example * defaultStorage: { * prefetchedUsers: [], * loading: false, * } */ addStorage?: (this: { name: string options: Options parent: Exclude<ParentConfig<MarkConfig<Options, Storage>>['addStorage'], undefined> }) => Storage /** * This function adds globalAttributes to specific nodes. * @see https://tiptap.dev/guide/custom-extensions#global-attributes * @example * addGlobalAttributes() { * return [ * { // Extend the following extensions * types: [ * 'heading', * 'paragraph', * ], * // … with those attributes * attributes: { * textAlign: { * default: 'left', * renderHTML: attributes => ({ * style: `text-align: ${attributes.textAlign}`, * }), * parseHTML: element => element.style.textAlign || 'left', * }, * }, * }, * ] * } */ addGlobalAttributes?: (this: { name: string options: Options storage: Storage extensions: (Node | Mark)[] parent: ParentConfig<MarkConfig<Options, Storage>>['addGlobalAttributes'] }) => GlobalAttributes /** * This function adds commands to the editor * @see https://tiptap.dev/guide/custom-extensions#keyboard-shortcuts * @example * addCommands() { * return { * myCommand: () => ({ chain }) => chain().setMark('type', 'foo').run(), * } * } */ addCommands?: (this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['addCommands'] }) => Partial<RawCommands> /** * This function registers keyboard shortcuts. * @see https://tiptap.dev/guide/custom-extensions#keyboard-shortcuts * @example * addKeyboardShortcuts() { * return { * 'Mod-l': () => this.editor.commands.toggleBulletList(), * } * }, */ addKeyboardShortcuts?: (this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['addKeyboardShortcuts'] }) => { [key: string]: KeyboardShortcutCommand } /** * This function adds input rules to the editor. * @see https://tiptap.dev/guide/custom-extensions#input-rules * @example * addInputRules() { * return [ * markInputRule({ * find: inputRegex, * type: this.type, * }), * ] * }, */ addInputRules?: (this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['addInputRules'] }) => InputRule[] /** * This function adds paste rules to the editor. * @see https://tiptap.dev/guide/custom-extensions#paste-rules * @example * addPasteRules() { * return [ * markPasteRule({ * find: pasteRegex, * type: this.type, * }), * ] * }, */ addPasteRules?: (this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['addPasteRules'] }) => PasteRule[] /** * This function adds Prosemirror plugins to the editor * @see https://tiptap.dev/guide/custom-extensions#prosemirror-plugins * @example * addProseMirrorPlugins() { * return [ * customPlugin(), * ] * } */ addProseMirrorPlugins?: (this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['addProseMirrorPlugins'] }) => Plugin[] /** * This function adds additional extensions to the editor. This is useful for * building extension kits. * @example * addExtensions() { * return [ * BulletList, * OrderedList, * ListItem * ] * } */ addExtensions?: (this: { name: string options: Options storage: Storage parent: ParentConfig<MarkConfig<Options, Storage>>['addExtensions'] }) => Extensions /** * This function extends the schema of the node. * @example * extendNodeSchema() { * return { * group: 'inline', * selectable: false, * } * } */ extendNodeSchema?: | (( this: { name: string options: Options storage: Storage parent: ParentConfig<MarkConfig<Options, Storage>>['extendNodeSchema'] }, extension: Node, ) => Record<string, any>) | null /** * This function extends the schema of the mark. * @example * extendMarkSchema() { * return { * group: 'inline', * selectable: false, * } * } */ extendMarkSchema?: | (( this: { name: string options: Options storage: Storage parent: ParentConfig<MarkConfig<Options, Storage>>['extendMarkSchema'] }, extension: Mark, ) => Record<string, any>) | null /** * The editor is not ready yet. */ onBeforeCreate?: | ((this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['onBeforeCreate'] }) => void) | null /** * The editor is ready. */ onCreate?: | ((this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['onCreate'] }) => void) | null /** * The content has changed. */ onUpdate?: | ((this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['onUpdate'] }) => void) | null /** * The selection has changed. */ onSelectionUpdate?: | ((this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['onSelectionUpdate'] }) => void) | null /** * The editor state has changed. */ onTransaction?: | (( this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['onTransaction'] }, props: { editor: Editor transaction: Transaction }, ) => void) | null /** * The editor is focused. */ onFocus?: | (( this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['onFocus'] }, props: { event: FocusEvent }, ) => void) | null /** * The editor isn’t focused anymore. */ onBlur?: | (( this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['onBlur'] }, props: { event: FocusEvent }, ) => void) | null /** * The editor is destroyed. */ onDestroy?: | ((this: { name: string options: Options storage: Storage editor: Editor type: MarkType parent: ParentConfig<MarkConfig<Options, Storage>>['onDestroy'] }) => void) | null /** * Keep mark after split node */ keepOnSplit?: boolean | (() => boolean) /** * Inclusive */ inclusive?: | MarkSpec['inclusive'] | ((this: { name: string options: Options storage: Storage parent: ParentConfig<MarkConfig<Options, Storage>>['inclusive'] editor?: Editor }) => MarkSpec['inclusive']) /** * Excludes */ excludes?: | MarkSpec['excludes'] | ((this: { name: string options: Options storage: Storage parent: ParentConfig<MarkConfig<Options, Storage>>['excludes'] editor?: Editor }) => MarkSpec['excludes']) /** * Marks this Mark as exitable */ exitable?: boolean | (() => boolean) /** * Group */ group?: | MarkSpec['group'] | ((this: { name: string options: Options storage: Storage parent: ParentConfig<MarkConfig<Options, Storage>>['group'] editor?: Editor }) => MarkSpec['group']) /** * Spanning */ spanning?: | MarkSpec['spanning'] | ((this: { name: string options: Options storage: Storage parent: ParentConfig<MarkConfig<Options, Storage>>['spanning'] editor?: Editor }) => MarkSpec['spanning']) /** * Code */ code?: | boolean | ((this: { name: string options: Options storage: Storage parent: ParentConfig<MarkConfig<Options, Storage>>['code'] editor?: Editor }) => boolean) /** * Parse HTML */ parseHTML?: (this: { name: string options: Options storage: Storage parent: ParentConfig<MarkConfig<Options, Storage>>['parseHTML'] editor?: Editor }) => MarkSpec['parseDOM'] /** * Render HTML */ renderHTML?: | (( this: { name: string options: Options storage: Storage parent: ParentConfig<MarkConfig<Options, Storage>>['renderHTML'] editor?: Editor }, props: { mark: ProseMirrorMark HTMLAttributes: Record<string, any> }, ) => DOMOutputSpec) | null /** * Attributes */ addAttributes?: (this: { name: string options: Options storage: Storage parent: ParentConfig<MarkConfig<Options, Storage>>['addAttributes'] editor?: Editor }) => Attributes | {} } } /** * The Mark class is used to create custom mark extensions. * @see https://tiptap.dev/api/extensions#create-a-new-extension */ export class Mark<Options = any, Storage = any> { type = 'mark' name = 'mark' parent: Mark | null = null child: Mark | null = null options: Options storage: Storage config: MarkConfig = { name: this.name, defaultOptions: {}, } constructor(config: Partial<MarkConfig<Options, Storage>> = {}) { this.config = { ...this.config, ...config, } this.name = this.config.name if (config.defaultOptions && Object.keys(config.defaultOptions).length > 0) { console.warn( `[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${this.name}".`, ) } // TODO: remove `addOptions` fallback this.options = this.config.defaultOptions if (this.config.addOptions) { this.options = callOrReturn( getExtensionField<AnyConfig['addOptions']>(this, 'addOptions', { name: this.name, }), ) } this.storage = callOrReturn( getExtensionField<AnyConfig['addStorage']>(this, 'addStorage', { name: this.name, options: this.options, }), ) || {} } static create<O = any, S = any>(config: Partial<MarkConfig<O, S>> = {}) { return new Mark<O, S>(config) } configure(options: Partial<Options> = {}) { // return a new instance so we can use the same extension // with different calls of `configure` const extension = this.extend<Options, Storage>({ ...this.config, addOptions: () => { return mergeDeep(this.options as Record<string, any>, options) as Options }, }) // Always preserve the current name extension.name = this.name // Set the parent to be our parent extension.parent = this.parent return extension } extend<ExtendedOptions = Options, ExtendedStorage = Storage>( extendedConfig: Partial<MarkConfig<ExtendedOptions, ExtendedStorage>> = {}, ) { const extension = new Mark<ExtendedOptions, ExtendedStorage>(extendedConfig) extension.parent = this this.child = extension extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name if (extendedConfig.defaultOptions && Object.keys(extendedConfig.defaultOptions).length > 0) { console.warn( `[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`, ) } extension.options = callOrReturn( getExtensionField<AnyConfig['addOptions']>(extension, 'addOptions', { name: extension.name, }), ) extension.storage = callOrReturn( getExtensionField<AnyConfig['addStorage']>(extension, 'addStorage', { name: extension.name, options: extension.options, }), ) return extension } static handleExit({ editor, mark }: { editor: Editor; mark: Mark }) { const { tr } = editor.state const currentPos = editor.state.selection.$from const isAtEnd = currentPos.pos === currentPos.end() if (isAtEnd) { const currentMarks = currentPos.marks() const isInMark = !!currentMarks.find(m => m?.type.name === mark.name) if (!isInMark) { return false } const removeMark = currentMarks.find(m => m?.type.name === mark.name) if (removeMark) { tr.removeStoredMark(removeMark) } tr.insertText(' ', currentPos.pos) editor.view.dispatch(tr) return true } return false } }