import { DOMOutputSpec, Mark as ProseMirrorMark, MarkSpec, MarkType, } from 'prosemirror-model' import { Plugin, Transaction } from 'prosemirror-state' import { MarkConfig } from '.' import { Editor } from './Editor' import { getExtensionField } from './helpers/getExtensionField' import { InputRule } from './InputRule' import { Node } from './Node' import { PasteRule } from './PasteRule' import { AnyConfig, Attributes, Extensions, GlobalAttributes, KeyboardShortcutCommand, ParentConfig, RawCommands, } from './types' import { callOrReturn } from './utilities/callOrReturn' import { mergeDeep } from './utilities/mergeDeep' declare module '@tiptap/core' { export interface MarkConfig { [key: string]: any; /** * Name */ name: string, /** * Priority */ priority?: number, /** * Default options */ defaultOptions?: Options, /** * Default Options */ addOptions?: (this: { name: string, parent: Exclude>['addOptions'], undefined>, }) => Options, /** * Default Storage */ addStorage?: (this: { name: string, options: Options, parent: Exclude>['addStorage'], undefined>, }) => Storage, /** * Global attributes */ addGlobalAttributes?: (this: { name: string, options: Options, storage: Storage, parent: ParentConfig>['addGlobalAttributes'], }) => GlobalAttributes | {}, /** * Raw */ addCommands?: (this: { name: string, options: Options, storage: Storage, editor: Editor, type: MarkType, parent: ParentConfig>['addCommands'], }) => Partial, /** * Keyboard shortcuts */ addKeyboardShortcuts?: (this: { name: string, options: Options, storage: Storage, editor: Editor, type: MarkType, parent: ParentConfig>['addKeyboardShortcuts'], }) => { [key: string]: KeyboardShortcutCommand, }, /** * Input rules */ addInputRules?: (this: { name: string, options: Options, storage: Storage, editor: Editor, type: MarkType, parent: ParentConfig>['addInputRules'], }) => InputRule[], /** * Paste rules */ addPasteRules?: (this: { name: string, options: Options, storage: Storage, editor: Editor, type: MarkType, parent: ParentConfig>['addPasteRules'], }) => PasteRule[], /** * ProseMirror plugins */ addProseMirrorPlugins?: (this: { name: string, options: Options, storage: Storage, editor: Editor, type: MarkType, parent: ParentConfig>['addProseMirrorPlugins'], }) => Plugin[], /** * Extensions */ addExtensions?: (this: { name: string, options: Options, storage: Storage, parent: ParentConfig>['addExtensions'], }) => Extensions, /** * Extend Node Schema */ extendNodeSchema?: (( this: { name: string, options: Options, storage: Storage, parent: ParentConfig>['extendNodeSchema'], }, extension: Node, ) => Record) | null, /** * Extend Mark Schema */ extendMarkSchema?: (( this: { name: string, options: Options, storage: Storage, parent: ParentConfig>['extendMarkSchema'], }, extension: Mark, ) => Record) | null, /** * The editor is not ready yet. */ onBeforeCreate?: ((this: { name: string, options: Options, storage: Storage, editor: Editor, type: MarkType, parent: ParentConfig>['onBeforeCreate'], }) => void) | null, /** * The editor is ready. */ onCreate?: ((this: { name: string, options: Options, storage: Storage, editor: Editor, type: MarkType, parent: ParentConfig>['onCreate'], }) => void) | null, /** * The content has changed. */ onUpdate?: ((this: { name: string, options: Options, storage: Storage, editor: Editor, type: MarkType, parent: ParentConfig>['onUpdate'], }) => void) | null, /** * The selection has changed. */ onSelectionUpdate?: ((this: { name: string, options: Options, storage: Storage, editor: Editor, type: MarkType, parent: ParentConfig>['onSelectionUpdate'], }) => void) | null, /** * The editor state has changed. */ onTransaction?: (( this: { name: string, options: Options, storage: Storage, editor: Editor, type: MarkType, parent: ParentConfig>['onTransaction'], }, props: { transaction: Transaction, }, ) => void) | null, /** * The editor is focused. */ onFocus?: (( this: { name: string, options: Options, storage: Storage, editor: Editor, type: MarkType, parent: ParentConfig>['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>['onBlur'], }, props: { event: FocusEvent, }, ) => void) | null, /** * The editor is destroyed. */ onDestroy?: ((this: { name: string, options: Options, storage: Storage, editor: Editor, type: MarkType, parent: ParentConfig>['onDestroy'], }) => void) | null, /** * Keep mark after split node */ keepOnSplit?: boolean | (() => boolean), /** * Inclusive */ inclusive?: MarkSpec['inclusive'] | ((this: { name: string, options: Options, storage: Storage, parent: ParentConfig>['inclusive'], }) => MarkSpec['inclusive']), /** * Excludes */ excludes?: MarkSpec['excludes'] | ((this: { name: string, options: Options, storage: Storage, parent: ParentConfig>['excludes'], }) => MarkSpec['excludes']), /** * Marks this Mark as exitable */ exitable?: boolean | (() => boolean), /** * Group */ group?: MarkSpec['group'] | ((this: { name: string, options: Options, storage: Storage, parent: ParentConfig>['group'], }) => MarkSpec['group']), /** * Spanning */ spanning?: MarkSpec['spanning'] | ((this: { name: string, options: Options, storage: Storage, parent: ParentConfig>['spanning'], }) => MarkSpec['spanning']), /** * Code */ code?: boolean | ((this: { name: string, options: Options, storage: Storage, parent: ParentConfig>['code'], }) => boolean), /** * Parse HTML */ parseHTML?: ( this: { name: string, options: Options, storage: Storage, parent: ParentConfig>['parseHTML'], }, ) => MarkSpec['parseDOM'], /** * Render HTML */ renderHTML?: (( this: { name: string, options: Options, storage: Storage, parent: ParentConfig>['renderHTML'], }, props: { mark: ProseMirrorMark, HTMLAttributes: Record, }, ) => DOMOutputSpec) | null, /** * Attributes */ addAttributes?: ( this: { name: string, options: Options, storage: Storage, parent: ParentConfig>['addAttributes'], }, ) => Attributes | {}, } } export class Mark { 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> = {}) { this.config = { ...this.config, ...config, } this.name = this.config.name if (config.defaultOptions) { 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( this, 'addOptions', { name: this.name, }, )) } this.storage = callOrReturn(getExtensionField( this, 'addStorage', { name: this.name, options: this.options, }, )) || {} } static create(config: Partial> = {}) { return new Mark(config) } configure(options: Partial = {}) { // return a new instance so we can use the same extension // with different calls of `configure` const extension = this.extend() extension.options = mergeDeep(this.options as Record, options) as Options extension.storage = callOrReturn(getExtensionField( extension, 'addStorage', { name: extension.name, options: extension.options, }, )) return extension } extend(extendedConfig: Partial> = {}) { const extension = new Mark(extendedConfig) extension.parent = this this.child = extension extension.name = extendedConfig.name ? extendedConfig.name : extension.parent.name if (extendedConfig.defaultOptions) { console.warn(`[tiptap warn]: BREAKING CHANGE: "defaultOptions" is deprecated. Please use "addOptions" instead. Found in extension: "${extension.name}".`) } extension.options = callOrReturn(getExtensionField( extension, 'addOptions', { name: extension.name, }, )) extension.storage = callOrReturn(getExtensionField( 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 } }