143 lines
3.8 KiB
TypeScript
143 lines
3.8 KiB
TypeScript
'use client';
|
|
|
|
import { useState, useRef, useEffect } from 'react';
|
|
import { Button } from '@/components/ui/button';
|
|
import {
|
|
Bold,
|
|
Italic,
|
|
Underline,
|
|
List,
|
|
ListOrdered,
|
|
Quote,
|
|
Link,
|
|
Undo,
|
|
Redo,
|
|
Type,
|
|
AlignLeft,
|
|
AlignCenter,
|
|
AlignRight
|
|
} from 'lucide-react';
|
|
|
|
interface SimpleEditorProps {
|
|
data?: string;
|
|
onChange?: (data: string) => void;
|
|
placeholder?: string;
|
|
className?: string;
|
|
disabled?: boolean;
|
|
}
|
|
|
|
export default function SimpleEditor({
|
|
data = '',
|
|
onChange,
|
|
placeholder = 'Start typing...',
|
|
className = '',
|
|
disabled = false
|
|
}: SimpleEditorProps) {
|
|
const [content, setContent] = useState(data);
|
|
const editorRef = useRef<HTMLDivElement>(null);
|
|
const [isMounted, setIsMounted] = useState(false);
|
|
|
|
useEffect(() => {
|
|
setIsMounted(true);
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
setContent(data);
|
|
}, [data]);
|
|
|
|
const handleInput = () => {
|
|
if (editorRef.current) {
|
|
const html = editorRef.current.innerHTML;
|
|
setContent(html);
|
|
onChange?.(html);
|
|
}
|
|
};
|
|
|
|
const execCommand = (command: string, value?: string) => {
|
|
document.execCommand(command, false, value);
|
|
editorRef.current?.focus();
|
|
handleInput();
|
|
};
|
|
|
|
const insertLink = () => {
|
|
const url = prompt('Enter URL:');
|
|
if (url) {
|
|
execCommand('createLink', url);
|
|
}
|
|
};
|
|
|
|
const toolbarButtons = [
|
|
{ command: 'bold', icon: Bold, label: 'Bold' },
|
|
{ command: 'italic', icon: Italic, label: 'Italic' },
|
|
{ command: 'underline', icon: Underline, label: 'Underline' },
|
|
{ command: 'insertUnorderedList', icon: List, label: 'Bullet List' },
|
|
{ command: 'insertOrderedList', icon: ListOrdered, label: 'Numbered List' },
|
|
{ command: 'formatBlock', icon: Quote, label: 'Quote', value: 'blockquote' },
|
|
{ command: 'justifyLeft', icon: AlignLeft, label: 'Align Left' },
|
|
{ command: 'justifyCenter', icon: AlignCenter, label: 'Align Center' },
|
|
{ command: 'justifyRight', icon: AlignRight, label: 'Align Right' },
|
|
{ command: 'undo', icon: Undo, label: 'Undo' },
|
|
{ command: 'redo', icon: Redo, label: 'Redo' },
|
|
];
|
|
|
|
if (!isMounted) {
|
|
return (
|
|
<div className={`flex items-center justify-center p-8 border border-gray-200 rounded-lg ${className}`}>
|
|
<div className="text-gray-500">Loading editor...</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className={`border border-gray-200 rounded-lg overflow-hidden ${className}`}>
|
|
{/* Toolbar */}
|
|
<div className="flex flex-wrap items-center gap-1 p-2 bg-gray-50 border-b border-gray-200">
|
|
{toolbarButtons.map((button) => {
|
|
const IconComponent = button.icon;
|
|
return (
|
|
<Button
|
|
key={button.command}
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
if (button.command === 'createLink') {
|
|
insertLink();
|
|
} else {
|
|
execCommand(button.command, button.value);
|
|
}
|
|
}}
|
|
className="h-8 w-8 p-0"
|
|
disabled={disabled}
|
|
title={button.label}
|
|
>
|
|
<IconComponent className="h-4 w-4" />
|
|
</Button>
|
|
);
|
|
})}
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={insertLink}
|
|
className="h-8 w-8 p-0"
|
|
disabled={disabled}
|
|
title="Insert Link"
|
|
>
|
|
<Link className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
|
|
{/* Editor Content */}
|
|
<div
|
|
ref={editorRef}
|
|
contentEditable={!disabled}
|
|
onInput={handleInput}
|
|
className="min-h-[200px] p-4 focus:outline-none"
|
|
style={{ minHeight: '200px' }}
|
|
dangerouslySetInnerHTML={{ __html: content }}
|
|
data-placeholder={placeholder}
|
|
suppressContentEditableWarning={true}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|