diff --git a/src/RichTextEditor/RichTextEditor.jsx b/src/RichTextEditor/RichTextEditor.jsx index b33c8da7..9b3a2844 100644 --- a/src/RichTextEditor/RichTextEditor.jsx +++ b/src/RichTextEditor/RichTextEditor.jsx @@ -59,6 +59,7 @@ const RichTextEditor = ({ templateVariables, }) => { const oneLineExtension = isOneLine ? [OneLineLimit] : []; + const hasTemplateVariables = templateVariables.length > 0; const requiredExtensions = [ Document, @@ -75,7 +76,7 @@ const RichTextEditor = ({ }), ]; - const templateVariablesExtension = templateVariables.length > 0 ? + const templateVariablesExtension = hasTemplateVariables ? [TemplateVariable.configure({ HTMLAttributes: { class: 'RichTextEditor__TemplateVariable', @@ -134,6 +135,19 @@ const RichTextEditor = ({ options.allowedTags = allowedTags; } + // When using template variables, we need to whitelist some specific attributes so that + // the editor can re-initialize correctly from db state + if (hasTemplateVariables) { + options.allowedAttributes = { + ...(options.allowedAttributes || {}), + span: ['class', 'data-id', 'data-type'], + }; + + if (options.allowedTags && !options.allowedTags.includes('span')) { + options.allowedTags.push('span'); + } + } + const sanitizedHtml = sanitizeHtml(html, options); onChange(sanitizedHtml); diff --git a/src/RichTextEditor/TemplateVariable.tsx b/src/RichTextEditor/TemplateVariable.tsx index 36998097..d93453c4 100644 --- a/src/RichTextEditor/TemplateVariable.tsx +++ b/src/RichTextEditor/TemplateVariable.tsx @@ -7,7 +7,7 @@ import React, { useState, } from 'react'; import { mergeAttributes, Node } from '@tiptap/core'; -import { DOMOutputSpec, Node as ProseMirrorNode } from '@tiptap/pm/model'; +import { Node as ProseMirrorNode } from '@tiptap/pm/model'; import { PluginKey } from '@tiptap/pm/state'; import Suggestion, { SuggestionOptions } from '@tiptap/suggestion'; import { ReactRenderer } from '@tiptap/react'; @@ -15,10 +15,8 @@ import { ReactRenderer } from '@tiptap/react'; import './TemplateVariable.scss'; export type TemplateVariableOptions = { - // eslint-disable-next-line @typescript-eslint/no-explicit-any HTMLAttributes: Record; - renderText: (props: { options: TemplateVariableOptions; node: ProseMirrorNode }) => string; - renderHTML: (props: { options: TemplateVariableOptions; node: ProseMirrorNode }) => DOMOutputSpec; + renderLabel: (props: { options: TemplateVariableOptions; node: ProseMirrorNode }) => string; suggestion: Omit; } @@ -30,16 +28,9 @@ export const TemplateVariable = Node.create({ addOptions() { return { HTMLAttributes: {}, - renderText({ node }) { + renderLabel({ node }) { return `{{ ${node.attrs.label ?? node.attrs.id} }}`; }, - renderHTML({ node }) { - return [ - 'span', - this.HTMLAttributes, - `{{ ${node.attrs.label ?? node.attrs.id} }}`, - ]; - }, suggestion: { char: '/', pluginKey: TemplateVariablePluginKey, @@ -127,23 +118,18 @@ export const TemplateVariable = Node.create({ }, renderHTML({ node, HTMLAttributes }) { - const html = this.options.renderHTML({ - options: this.options, - node, - }); - - if (typeof html === 'string') { - return [ - 'span', - mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), - html, - ]; - } - return html; + return [ + 'span', + mergeAttributes({ 'data-type': this.name }, this.options.HTMLAttributes, HTMLAttributes), + this.options.renderLabel({ + options: this.options, + node, + }), + ]; }, renderText({ node }) { - return this.options.renderText({ + return this.options.renderLabel({ options: this.options, node, });