diff --git a/.changeset/warm-bananas-call.md b/.changeset/warm-bananas-call.md new file mode 100644 index 0000000000..71457916a7 --- /dev/null +++ b/.changeset/warm-bananas-call.md @@ -0,0 +1,5 @@ +--- +"@tiptap/react": patch +--- + +ReactNodeViewRenderer now accepts a callback for attrs of the wrapping element to be updated on each node view update diff --git a/packages/react/src/ReactNodeViewRenderer.tsx b/packages/react/src/ReactNodeViewRenderer.tsx index 143704ff8f..488f8d7f9f 100644 --- a/packages/react/src/ReactNodeViewRenderer.tsx +++ b/packages/react/src/ReactNodeViewRenderer.tsx @@ -1,6 +1,7 @@ import { DecorationWithType, Editor, + getRenderedAttributes, NodeView, NodeViewProps, NodeViewRenderer, @@ -27,7 +28,12 @@ export interface ReactNodeViewRendererOptions extends NodeViewRendererOptions { | null as?: string className?: string - attrs?: Record + attrs?: + | Record + | ((props: { + node: ProseMirrorNode + HTMLAttributes: Record + }) => Record) } class ReactNodeView extends NodeView< @@ -110,8 +116,9 @@ class ReactNodeView extends NodeView< props, as, className: `node-${this.node.type.name} ${className}`.trim(), - attrs: this.options.attrs, }) + + this.updateElementAttributes() } get dom() { @@ -154,6 +161,9 @@ class ReactNodeView extends NodeView< update(node: ProseMirrorNode, decorations: DecorationWithType[]) { const updateProps = (props?: Record) => { this.renderer.updateProps(props) + if (typeof this.options.attrs === 'function') { + this.updateElementAttributes() + } } if (node.type !== this.node.type) { @@ -207,6 +217,23 @@ class ReactNodeView extends NodeView< this.editor.off('selectionUpdate', this.handleSelectionUpdate) this.contentDOMElement = null } + + updateElementAttributes() { + if (this.options.attrs) { + let attrsObj: Record = {} + + if (typeof this.options.attrs === 'function') { + const extensionAttributes = this.editor.extensionManager.attributes + const HTMLAttributes = getRenderedAttributes(this.node, extensionAttributes) + + attrsObj = this.options.attrs({ node: this.node, HTMLAttributes }) + } else { + attrsObj = this.options.attrs + } + + this.renderer.updateAttributes(attrsObj) + } + } } export function ReactNodeViewRenderer( diff --git a/packages/react/src/ReactRenderer.tsx b/packages/react/src/ReactRenderer.tsx index 7c40d2750e..22262f5006 100644 --- a/packages/react/src/ReactRenderer.tsx +++ b/packages/react/src/ReactRenderer.tsx @@ -57,14 +57,6 @@ export interface ReactRendererOptions { * @example 'foo bar' */ className?: string, - - /** - * The attributes of the element. - * @type {Record} - * @default {} - * @example { 'data-foo': 'bar' } - */ - attrs?: Record, } type ComponentType = @@ -103,7 +95,6 @@ export class ReactRenderer { props = {}, as = 'div', className = '', - attrs, }: ReactRendererOptions) { this.id = Math.floor(Math.random() * 0xFFFFFFFF).toString() this.component = component @@ -116,12 +107,6 @@ export class ReactRenderer { this.element.classList.add(...className.split(' ')) } - if (attrs) { - Object.keys(attrs).forEach(key => { - this.element.setAttribute(key, attrs[key]) - }) - } - if (this.editor.isInitialized) { // On first render, we need to flush the render synchronously // Renders afterwards can be async, but this fixes a cursor positioning issue @@ -163,4 +148,10 @@ export class ReactRenderer { editor?.contentComponent?.removeRenderer(this.id) } + + updateAttributes(attributes: Record): void { + Object.keys(attributes).forEach(key => { + this.element.setAttribute(key, attributes[key]) + }) + } }