diff --git a/packages/core/src/Editor.ts b/packages/core/src/Editor.ts index d3378a47b0..dd93c5def6 100644 --- a/packages/core/src/Editor.ts +++ b/packages/core/src/Editor.ts @@ -400,6 +400,7 @@ export class Editor extends EventEmitter { } this.view.setProps({ + markViews: this.extensionManager.markViews, nodeViews: this.extensionManager.nodeViews, }) } diff --git a/packages/core/src/ExtensionManager.ts b/packages/core/src/ExtensionManager.ts index a0fda44be2..db42d715ef 100644 --- a/packages/core/src/ExtensionManager.ts +++ b/packages/core/src/ExtensionManager.ts @@ -1,7 +1,7 @@ import { keymap } from '@tiptap/pm/keymap' import { Schema } from '@tiptap/pm/model' import { Plugin } from '@tiptap/pm/state' -import { NodeViewConstructor } from '@tiptap/pm/view' +import { MarkViewConstructor, NodeViewConstructor } from '@tiptap/pm/view' import type { Editor } from './Editor.js' import { getAttributesFromExtensions } from './helpers/getAttributesFromExtensions.js' @@ -12,7 +12,7 @@ import { getSchemaByResolvedExtensions } from './helpers/getSchemaByResolvedExte import { getSchemaTypeByName } from './helpers/getSchemaTypeByName.js' import { isExtensionRulesEnabled } from './helpers/isExtensionRulesEnabled.js' import { splitExtensions } from './helpers/splitExtensions.js' -import type { NodeConfig } from './index.js' +import { type MarkConfig, type NodeConfig, getMarkType } from './index.js' import { InputRule, inputRulesPlugin } from './InputRule.js' import { Mark } from './Mark.js' import { PasteRule, pasteRulesPlugin } from './PasteRule.js' @@ -317,6 +317,58 @@ export class ExtensionManager { ) } + get markViews(): Record { + const { editor } = this + const { markExtensions } = splitExtensions(this.extensions) + + return Object.fromEntries( + markExtensions + .filter(extension => !!getExtensionField(extension, 'addMarkView')) + .map(extension => { + const extensionAttributes = this.attributes.filter( + attribute => attribute.type === extension.name, + ) + const context = { + name: extension.name, + options: extension.options, + storage: extension.storage, + editor, + type: getMarkType(extension.name, this.schema), + } + const addMarkView = getExtensionField( + extension, + 'addMarkView', + context, + ) + + if (!addMarkView) { + return [] + } + + const markView: MarkViewConstructor = ( + mark, + view, + inline, + ) => { + const HTMLAttributes = getRenderedAttributes(mark, extensionAttributes) + + return addMarkView()({ + // pass-through + mark, + view, + inline, + // tiptap-specific + editor, + extension, + HTMLAttributes, + }) + } + + return [extension.name, markView] + }), + ) + } + /** * Go through all extensions, create extension storages & setup marks * & bind editor event listener. diff --git a/packages/core/src/Mark.ts b/packages/core/src/Mark.ts index ea57edd7f3..db39eed9fb 100644 --- a/packages/core/src/Mark.ts +++ b/packages/core/src/Mark.ts @@ -15,6 +15,7 @@ import { Extensions, GlobalAttributes, KeyboardShortcutCommand, + MarkViewRenderer, ParentConfig, RawCommands, } from './types.js' @@ -410,6 +411,20 @@ declare module '@tiptap/core' { }) => void) | null + /** + * Node View + */ + addMarkView?: + | ((this: { + name: string + options: Options + storage: Storage + editor: Editor + type: MarkType + parent: ParentConfig>['addMarkView'] + }) => MarkViewRenderer) + | null + /** * Keep mark after split node */ diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 3077058d72..19df7ecf24 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -11,6 +11,7 @@ import { DecorationAttrs, EditorProps, EditorView, + MarkViewConstructor, NodeView, NodeViewConstructor, } from '@tiptap/pm/view' @@ -305,6 +306,37 @@ export interface NodeViewRendererProps { export type NodeViewRenderer = (props: NodeViewRendererProps) => NodeView; +export interface MarkViewRendererProps { + // pass-through from prosemirror + /** + * The node that is being rendered. + */ + mark: Parameters[0]; + /** + * The editor's view. + */ + view: Parameters[1]; + /** + * indicates whether the mark's content is inline + */ + inline: Parameters[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; +} + +export type MarkViewRenderer = (props: MarkViewRendererProps) => ReturnType; + export type AnyCommands = Record Command>; export type UnionCommands = UnionToIntersection<