diff --git a/web/RichText.tsx b/web/RichText.tsx index 697201f..5e01cb4 100644 --- a/web/RichText.tsx +++ b/web/RichText.tsx @@ -179,6 +179,32 @@ const Toolbar = ({ editor }: { editor: Editor }) => ( ); +/** + * Stored alignment classes, back into the inline styles tiptap understands. + * + * ── The round trip was LOSSY, and this is the half that was missing ── + * + * The editor emits `style="text-align: right"`. The site's sanitiser refuses style attributes — one is + * a way to position something over the rest of the page — and converts alignment to `class="rt-right"` + * on the way in. That half has always worked. + * + * Coming back out, tiptap parses styles and knows nothing about those classes. So a document loaded + * from the database arrived with every alignment gone, and the next save wrote that loss back. Editing + * one paragraph silently un-aligned the others: a right-aligned line lost its alignment because + * somebody justified something else. + * + * Converting here makes the two ends symmetric — class in, style out, style in, class out — and the + * conversion is the sanitiser's own table read backwards. + * + * `rt-font-*` is NOT converted, and that is a known gap rather than an oversight: this editor has no + * font control, so a stored font class has nothing to become and would be dropped either way. It wants + * a font button before it wants a parser. + */ +const ALIGNMENT_CLASS = /class="rt-(left|center|right|justify)"/g; + +const toEditorHtml = (html: string): string => + html.replace(ALIGNMENT_CLASS, (_whole, align: string) => `style="text-align: ${align}"`); + export const RichText = ({ value, onChange, @@ -196,7 +222,7 @@ export const RichText = ({ // clicking a link to edit its text opens it and loses whatever was unsaved. Link.configure({ openOnClick: false }), ], - content: value, + content: toEditorHtml(value), editorProps: { attributes: { class: @@ -216,7 +242,11 @@ export const RichText = ({ jumps to the end on every keystroke. */ useEffect(() => { - if (editor && value !== editor.getHTML()) editor.commands.setContent(value, { emitUpdate: false }); + if (!editor) return; + // Compared in the EDITOR's dialect, or every load looks like a change: the stored html says + // `class="rt-right"` and getHTML() says `style="text-align: right"`, which never match. + const incoming = toEditorHtml(value); + if (incoming !== editor.getHTML()) editor.commands.setContent(incoming, { emitUpdate: false }); }, [editor, value]); if (!editor) return null;