Files
paperlessmanager/paperless-frontend/src/components/WysiwygEditor.tsx
T
bjoernpoettker 0c94e7b999
Build and Push Multi-Platform Images / build-and-push (push) Successful in 23s
feat: extract WysiwygEditor into reusable component and add advanced formatting options
2026-05-07 08:12:28 +02:00

219 lines
5.8 KiB
TypeScript

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 (
<button
type="button"
title={title}
disabled={disabled}
onMouseDown={(e) => {
e.preventDefault();
if (!disabled) onClick();
}}
className={`pm-tool-btn${active ? ' pm-tool-btn--active' : ''}`}
>
{children}
</button>
);
}
function Divider() {
return <span className="pm-tool-divider" />;
}
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<WysiwygEditorHandle, WysiwygEditorProps>(
({ 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 (
<div className="pm-editor-wrap">
<div className="pm-toolbar">
<ToolBtn
title="Rückgängig (Strg+Z)"
onClick={() => editor.chain().focus().undo().run()}
>
<UndoOutlined />
</ToolBtn>
<ToolBtn
title="Wiederholen (Strg+Y)"
onClick={() => editor.chain().focus().redo().run()}
>
<RedoOutlined />
</ToolBtn>
<Divider />
<Select
size="small"
value={headingLevel}
style={{ width: 128 }}
onMouseDown={(e) => e.preventDefault()}
onChange={(v) => {
if (v === 'p') {
editor.chain().focus().setParagraph().run();
} else {
editor.chain().focus().setHeading({ level: parseInt(v) as 1 | 2 | 3 }).run();
}
}}
options={HEADING_OPTIONS}
/>
<Divider />
<ToolBtn
active={editor.isActive('bold')}
onClick={() => editor.chain().focus().toggleBold().run()}
title="Fett (Strg+B)"
>
<BoldOutlined />
</ToolBtn>
<ToolBtn
active={editor.isActive('italic')}
onClick={() => editor.chain().focus().toggleItalic().run()}
title="Kursiv (Strg+I)"
>
<ItalicOutlined />
</ToolBtn>
<ToolBtn
active={editor.isActive('underline')}
onClick={() => editor.chain().focus().toggleUnderline().run()}
title="Unterstrichen (Strg+U)"
>
<UnderlineOutlined />
</ToolBtn>
<ToolBtn
active={editor.isActive('strike')}
onClick={() => editor.chain().focus().toggleStrike().run()}
title="Durchgestrichen"
>
<StrikethroughOutlined />
</ToolBtn>
<Divider />
<ToolBtn
active={editor.isActive('bulletList')}
onClick={() => editor.chain().focus().toggleBulletList().run()}
title="Aufzählungsliste"
>
<UnorderedListOutlined />
</ToolBtn>
<ToolBtn
active={editor.isActive('orderedList')}
onClick={() => editor.chain().focus().toggleOrderedList().run()}
title="Nummerierte Liste"
>
<OrderedListOutlined />
</ToolBtn>
<Divider />
<ToolBtn
active={editor.isActive({ textAlign: 'left' })}
onClick={() => editor.chain().focus().setTextAlign('left').run()}
title="Linksbündig"
>
<AlignLeftOutlined />
</ToolBtn>
<ToolBtn
active={editor.isActive({ textAlign: 'center' })}
onClick={() => editor.chain().focus().setTextAlign('center').run()}
title="Zentriert"
>
<AlignCenterOutlined />
</ToolBtn>
<ToolBtn
active={editor.isActive({ textAlign: 'right' })}
onClick={() => editor.chain().focus().setTextAlign('right').run()}
title="Rechtsbündig"
>
<AlignRightOutlined />
</ToolBtn>
</div>
<div className="pm-document-surround" style={{ minHeight: minHeight + 32 }}>
<div className="pm-document-page" style={{ minHeight }}>
<EditorContent editor={editor} />
</div>
</div>
</div>
);
},
);