diff --git a/package-lock.json b/package-lock.json index bad17e21..52e57b40 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "date-fns": "^3.3.1", "leaflet": "^1.9.4", "leaflet.locatecontrol": "^0.79.0", + "prosemirror-markdown": "^1.13.2", "radash": "^12.1.0", "react-colorful": "^5.6.1", "react-dropzone": "^14.3.8", diff --git a/package.json b/package.json index 0140d289..c94ec60d 100644 --- a/package.json +++ b/package.json @@ -123,6 +123,7 @@ "date-fns": "^3.3.1", "leaflet": "^1.9.4", "leaflet.locatecontrol": "^0.79.0", + "prosemirror-markdown": "^1.13.2", "radash": "^12.1.0", "react-colorful": "^5.6.1", "react-dropzone": "^14.3.8", diff --git a/src/Components/Input/RichTextEditor.tsx b/src/Components/Input/RichTextEditor.tsx index 6ea4907e..b442003f 100644 --- a/src/Components/Input/RichTextEditor.tsx +++ b/src/Components/Input/RichTextEditor.tsx @@ -13,11 +13,16 @@ import { TaskItem } from '@tiptap/extension-task-item' import { TaskList } from '@tiptap/extension-task-list' import { EditorContent, useEditor } from '@tiptap/react' import { StarterKit } from '@tiptap/starter-kit' +import { MarkdownSerializer } from 'prosemirror-markdown' import { useEffect } from 'react' import { Markdown } from 'tiptap-markdown' import { TextEditorMenu } from './TextEditorMenu' +import type { Editor } from '@tiptap/react' +import type { MarkdownSerializerState } from 'prosemirror-markdown' +import type { Node as ProseMirrorNode } from 'prosemirror-model' + interface RichTextEditorProps { labelTitle?: string labelStyle?: string @@ -28,6 +33,20 @@ interface RichTextEditorProps { updateFormValue?: (value: string) => void } +interface ImageAttrs { + src: string + alt?: string + title?: string + style?: string +} + +type NodeSerializerFn = ( + state: MarkdownSerializerState, + node: ProseMirrorNode, + parent: ProseMirrorNode, + index: number, +) => void + /** * @category Input */ @@ -41,12 +60,15 @@ export function RichTextEditor({ updateFormValue, }: RichTextEditorProps) { const handleChange = () => { - let newValue: string | undefined = editor?.storage.markdown.getMarkdown() - - const regex = /!\[.*?\]\(.*?\)/g - newValue = newValue?.replace(regex, (match: string) => match + '\n\n') - if (updateFormValue && newValue) { - updateFormValue(newValue) + if (editor) { + let newValue: string | undefined = getStyledMarkdown(editor) + ? getStyledMarkdown(editor) + : undefined + const regex = /!\[.*?\]\(.*?\)/g + newValue = newValue?.replace(regex, (match: string) => match + '\n\n') + if (updateFormValue && newValue) { + updateFormValue(newValue) + } } } @@ -64,6 +86,7 @@ export function RichTextEditor({ }, }), Markdown.configure({ + html: true, linkify: true, transformCopiedText: true, transformPastedText: true, @@ -76,7 +99,7 @@ export function RichTextEditor({ TableRow, TaskList, TaskItem, - Image, + CustomImage, Link, Placeholder.configure({ placeholder, @@ -122,3 +145,63 @@ export function RichTextEditor({ ) } + +const CustomImage = Image.extend({ + addAttributes() { + return { + // alle Standard-Attribute (src, alt, title) behalten + ...this.parent?.(), + // das style-Attribut zulassen und weiterreichen + style: { + default: null, + parseHTML: (element) => element.getAttribute('style'), + renderHTML: (attributes) => { + if (!attributes.style) { + return {} + } + return { style: attributes.style } + }, + }, + // optional: Breite und Höhe separat behandeln + width: { + default: null, + parseHTML: (element) => element.getAttribute('width'), + renderHTML: (attrs) => (attrs.width ? { width: attrs.width } : {}), + }, + height: { + default: null, + parseHTML: (element) => element.getAttribute('height'), + renderHTML: (attrs) => (attrs.height ? { height: attrs.height } : {}), + }, + } + }, +}) + +export function getStyledMarkdown(editor: Editor): string { + // Den internen Serializer sauber casten + const { serializer } = editor.storage.markdown as { serializer: MarkdownSerializer } + + // Die Basis-Nodes/Marks zum Überschreiben herausziehen + const baseNodes = serializer.nodes as Record + const marks = serializer.marks + + // Unsere Image-Funktion mit korrekter Signatur + const customImage: NodeSerializerFn = (state, node) => { + const { src, alt, title, style } = node.attrs as ImageAttrs + + // Per String-Konkatenation, damit kein ESLint-Fehler mehr kommt + let tag = '