From 0c94e7b999a54490fa054495505974e7fa95236a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20P=C3=B6ttker?= Date: Thu, 7 May 2026 08:12:28 +0200 Subject: [PATCH] feat: extract WysiwygEditor into reusable component and add advanced formatting options --- paperless-frontend/package-lock.json | 14 ++ paperless-frontend/package.json | 1 + .../src/components/WysiwygEditor.tsx | 218 ++++++++++++++++++ paperless-frontend/src/index.css | 106 +++++++++ .../src/pages/InboxDetailPage.tsx | 48 +--- .../src/pages/UserSettingsPage.tsx | 47 +--- 6 files changed, 357 insertions(+), 77 deletions(-) create mode 100644 paperless-frontend/src/components/WysiwygEditor.tsx diff --git a/paperless-frontend/package-lock.json b/paperless-frontend/package-lock.json index 10c4a1a..18b52ba 100644 --- a/paperless-frontend/package-lock.json +++ b/paperless-frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.0", "dependencies": { "@ant-design/icons": "^6.1.1", + "@tiptap/extension-text-align": "^3.22.5", "@tiptap/extension-underline": "^3.22.5", "@tiptap/react": "^3.22.5", "@tiptap/starter-kit": "^3.22.5", @@ -2281,6 +2282,19 @@ "@tiptap/core": "3.22.5" } }, + "node_modules/@tiptap/extension-text-align": { + "version": "3.22.5", + "resolved": "https://registry.npmjs.org/@tiptap/extension-text-align/-/extension-text-align-3.22.5.tgz", + "integrity": "sha512-LNUinhsZJC+/Vm7ugtghSjqlO64FQuww8oJkHq54Oh135fJ+kY2yBRakYFeH0P0gl4VPJH13AUetLF696CM2UQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "3.22.5" + } + }, "node_modules/@tiptap/extension-underline": { "version": "3.22.5", "resolved": "https://registry.npmjs.org/@tiptap/extension-underline/-/extension-underline-3.22.5.tgz", diff --git a/paperless-frontend/package.json b/paperless-frontend/package.json index 7a8639b..a7c0d3a 100644 --- a/paperless-frontend/package.json +++ b/paperless-frontend/package.json @@ -11,6 +11,7 @@ }, "dependencies": { "@ant-design/icons": "^6.1.1", + "@tiptap/extension-text-align": "^3.22.5", "@tiptap/extension-underline": "^3.22.5", "@tiptap/react": "^3.22.5", "@tiptap/starter-kit": "^3.22.5", diff --git a/paperless-frontend/src/components/WysiwygEditor.tsx b/paperless-frontend/src/components/WysiwygEditor.tsx new file mode 100644 index 0000000..7a05874 --- /dev/null +++ b/paperless-frontend/src/components/WysiwygEditor.tsx @@ -0,0 +1,218 @@ +import { forwardRef, useImperativeHandle } from 'react'; +import { useEditor, EditorContent } from '@tiptap/react'; +import StarterKit from '@tiptap/starter-kit'; +import Underline from '@tiptap/extension-underline'; +import TextAlign from '@tiptap/extension-text-align'; +import { Select } from 'antd'; +import { + AlignCenterOutlined, + AlignLeftOutlined, + AlignRightOutlined, + BoldOutlined, + ItalicOutlined, + OrderedListOutlined, + RedoOutlined, + StrikethroughOutlined, + UnderlineOutlined, + UndoOutlined, + UnorderedListOutlined, +} from '@ant-design/icons'; + +export interface WysiwygEditorHandle { + getHTML: () => string; + getText: () => string; + setContent: (html: string) => void; +} + +interface WysiwygEditorProps { + minHeight?: number; +} + +function ToolBtn({ + active, + disabled, + onClick, + title, + children, +}: { + active?: boolean; + disabled?: boolean; + onClick: () => void; + title?: string; + children: React.ReactNode; +}) { + return ( + + ); +} + +function Divider() { + return ; +} + +const HEADING_OPTIONS = [ + { value: 'p', label: 'Normal' }, + { value: '1', label: 'Überschrift 1' }, + { value: '2', label: 'Überschrift 2' }, + { value: '3', label: 'Überschrift 3' }, +]; + +export const WysiwygEditor = forwardRef( + ({ minHeight = 200 }, ref) => { + const editor = useEditor({ + extensions: [ + StarterKit, + Underline, + TextAlign.configure({ types: ['heading', 'paragraph'] }), + ], + content: '', + }); + + useImperativeHandle( + ref, + () => ({ + getHTML: () => editor?.getHTML() ?? '', + getText: () => editor?.getText() ?? '', + setContent: (html: string) => { + editor?.commands.setContent(html); + }, + }), + [editor], + ); + + if (!editor) return null; + + const headingLevel = editor.isActive('heading', { level: 1 }) + ? '1' + : editor.isActive('heading', { level: 2 }) + ? '2' + : editor.isActive('heading', { level: 3 }) + ? '3' + : 'p'; + + return ( +
+
+ editor.chain().focus().undo().run()} + > + + + editor.chain().focus().redo().run()} + > + + + + + + -
- - -
+
diff --git a/paperless-frontend/src/pages/UserSettingsPage.tsx b/paperless-frontend/src/pages/UserSettingsPage.tsx index 75f03d2..f56fa0a 100644 --- a/paperless-frontend/src/pages/UserSettingsPage.tsx +++ b/paperless-frontend/src/pages/UserSettingsPage.tsx @@ -1,34 +1,11 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { Button, Card, Form, Input, InputNumber, Space, Switch, Tabs, Typography, message } from 'antd'; import { CheckCircleOutlined, CloseCircleOutlined, LoadingOutlined } from '@ant-design/icons'; -import { useEditor, EditorContent } from '@tiptap/react'; -import StarterKit from '@tiptap/starter-kit'; -import Underline from '@tiptap/extension-underline'; +import { WysiwygEditor, type WysiwygEditorHandle } from '../components/WysiwygEditor'; import { userSettingsApi } from '../api/userSettings'; const { Title } = Typography; -function TiptapToolbar({ editor }: { editor: ReturnType }) { - if (!editor) return null; - const btn = (active: boolean): React.CSSProperties => ({ - padding: '2px 8px', - border: '1px solid #d9d9d9', - borderRadius: 4, - cursor: 'pointer', - background: active ? '#e6f4ff' : '#fff', - fontWeight: active ? 600 : 400, - }); - return ( -
- - - - - -
- ); -} - function MailSettingsTab() { const [form] = Form.useForm(); const [loading, setLoading] = useState(true); @@ -36,11 +13,7 @@ function MailSettingsTab() { const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState<{ ok: boolean; error?: string } | null>(null); const [passSet, setPassSet] = useState(false); - - const editor = useEditor({ - extensions: [StarterKit, Underline], - content: '', - }); + const editorRef = useRef(null); useEffect(() => { userSettingsApi.get().then((data) => { @@ -54,7 +27,7 @@ function MailSettingsTab() { }); setPassSet(data.smtpPassSet); if (data.mailSignatureHtml) { - editor?.commands.setContent(data.mailSignatureHtml); + editorRef.current?.setContent(data.mailSignatureHtml); } }).catch(() => { message.error('Einstellungen konnten nicht geladen werden'); @@ -74,7 +47,7 @@ function MailSettingsTab() { smtpPass: values.smtpPass || undefined, smtpFrom: values.smtpFrom || null, smtpFromName: values.smtpFromName || null, - mailSignatureHtml: editor?.getHTML() ?? null, + mailSignatureHtml: editorRef.current?.getHTML() ?? null, }); setPassSet(updated.smtpPassSet); form.setFieldValue('smtpPass', ''); @@ -126,7 +99,10 @@ function MailSettingsTab() { - + @@ -136,10 +112,7 @@ function MailSettingsTab() { -
- - -
+