My-Weekly-ToDo-List/src/components/RichTextEditor.tsx

151 lines
6.5 KiB
TypeScript

'use client';
import React, { useEffect } from 'react';
import { useEditor, EditorContent } from '@tiptap/react';
import StarterKit from '@tiptap/starter-kit';
import Underline from '@tiptap/extension-underline';
import Link from '@tiptap/extension-link';
import Placeholder from '@tiptap/extension-placeholder';
interface RichTextEditorProps {
value: string;
onChange: (html: string) => void;
placeholder?: string;
minHeight?: string;
}
export default function RichTextEditor({ value, onChange, placeholder = 'Notes...', minHeight = '120px' }: RichTextEditorProps) {
const editor = useEditor({
immediatelyRender: false,
extensions: [
StarterKit.configure({
heading: { levels: [1, 2, 3] },
}),
Underline,
Link.configure({
openOnClick: false,
HTMLAttributes: { rel: 'noopener noreferrer', target: '_blank' },
}),
Placeholder.configure({ placeholder }),
],
content: value,
onUpdate: ({ editor }) => {
// When content is just empty paragraph, return empty string
const html = editor.isEmpty ? '' : editor.getHTML();
onChange(html);
},
editorProps: {
attributes: {
class: 'rte-editor-content',
style: `min-height: ${minHeight}; outline: none;`,
},
},
});
// Sync external value changes (e.g. when editing an existing event)
useEffect(() => {
if (editor && editor.getHTML() !== value && !editor.isFocused) {
editor.commands.setContent(value || '');
}
}, [value, editor]);
if (!editor) return null;
const btn = (active: boolean) => ({
padding: '3px 7px',
border: '1px solid var(--weekly-border, #ddd)',
borderRadius: '4px',
background: active ? 'var(--weekly-accent, #2563eb)' : 'var(--weekly-bg, white)',
color: active ? 'white' : 'var(--weekly-text, #333)',
cursor: 'pointer',
fontSize: '0.78rem',
fontWeight: active ? '700' : '500',
lineHeight: 1,
transition: 'all 0.15s',
} as React.CSSProperties);
const handleLink = () => {
const prev = editor.getAttributes('link').href || '';
const url = window.prompt('Enter URL:', prev);
if (url === null) return;
if (url === '') {
editor.chain().focus().unsetLink().run();
} else {
editor.chain().focus().setLink({ href: url }).run();
}
};
return (
<div style={{
border: '1px solid var(--weekly-border, #ddd)',
borderRadius: '6px',
overflow: 'hidden',
background: 'var(--weekly-bg, white)',
}}>
{/* Toolbar */}
<div style={{
display: 'flex',
flexWrap: 'wrap',
gap: '4px',
padding: '6px 8px',
borderBottom: '1px solid var(--weekly-border, #ddd)',
background: 'var(--weekly-settings-item-bg, #f8f8f8)',
}}>
<button type="button" style={btn(editor.isActive('bold'))} onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleBold().run(); }} title="Bold">
<b>B</b>
</button>
<button type="button" style={btn(editor.isActive('italic'))} onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleItalic().run(); }} title="Italic">
<i>I</i>
</button>
<button type="button" style={btn(editor.isActive('underline'))} onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleUnderline().run(); }} title="Underline">
<u>U</u>
</button>
<div style={{ width: 1, background: 'var(--weekly-border, #ddd)', margin: '0 2px' }} />
<button type="button" style={btn(editor.isActive('bulletList'))} onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleBulletList().run(); }} title="Bullet list">
</button>
<button type="button" style={btn(editor.isActive('orderedList'))} onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleOrderedList().run(); }} title="Numbered list">
1.
</button>
<div style={{ width: 1, background: 'var(--weekly-border, #ddd)', margin: '0 2px' }} />
<button type="button" style={btn(editor.isActive('blockquote'))} onMouseDown={e => { e.preventDefault(); editor.chain().focus().toggleBlockquote().run(); }} title="Blockquote">
&ldquo;
</button>
<button type="button" style={btn(editor.isActive('link'))} onMouseDown={e => { e.preventDefault(); handleLink(); }} title="Link">
🔗
</button>
<div style={{ width: 1, background: 'var(--weekly-border, #ddd)', margin: '0 2px' }} />
<button type="button" style={btn(false)} onMouseDown={e => { e.preventDefault(); editor.chain().focus().undo().run(); }} title="Undo">
</button>
<button type="button" style={btn(false)} onMouseDown={e => { e.preventDefault(); editor.chain().focus().redo().run(); }} title="Redo">
</button>
</div>
{/* Editor content */}
<EditorContent editor={editor} style={{ padding: '10px 12px' }} />
<style>{`
.rte-editor-content p { margin: 0 0 0.5em; }
.rte-editor-content p:last-child { margin-bottom: 0; }
.rte-editor-content ul, .rte-editor-content ol { padding-left: 1.4em; margin: 0.3em 0; }
.rte-editor-content blockquote {
border-left: 3px solid var(--weekly-accent, #2563eb);
margin: 0.3em 0;
padding-left: 0.8em;
color: var(--weekly-text-muted, #666);
}
.rte-editor-content a { color: var(--weekly-accent, #2563eb); text-decoration: underline; }
.rte-editor-content .is-editor-empty:first-child::before {
content: attr(data-placeholder);
float: left;
color: var(--weekly-text-muted, #adb5bd);
pointer-events: none;
height: 0;
}
`}</style>
</div>
);
}