252 lines
8.3 KiB
TypeScript
252 lines
8.3 KiB
TypeScript
import React, { useRef } from 'react';
|
|
import {
|
|
Bold,
|
|
Italic,
|
|
Underline,
|
|
Heading1,
|
|
Heading2,
|
|
Heading3,
|
|
List,
|
|
ListOrdered,
|
|
Link,
|
|
AlignRight,
|
|
AlignCenter,
|
|
AlignLeft,
|
|
Palette,
|
|
Undo,
|
|
Redo,
|
|
} from 'lucide-react';
|
|
|
|
interface RichTextEditorProps {
|
|
value: string;
|
|
onChange: (html: string) => void;
|
|
placeholder?: string;
|
|
className?: string;
|
|
}
|
|
|
|
const COLORS = [
|
|
{ label: 'سیاه / پیشفرض', value: '#111827' },
|
|
{ label: 'خاکستری', value: '#4b5563' },
|
|
{ label: 'آبی کنینا', value: '#1d4ed8' },
|
|
{ label: 'بنفش اختصاصی', value: '#7c3aed' },
|
|
{ label: 'سبز درمانی', value: '#059669' },
|
|
{ label: 'قرمز هشدار', value: '#dc2626' },
|
|
{ label: 'کهربایی / زرد', value: '#d97706' },
|
|
];
|
|
|
|
export default function RichTextEditor({
|
|
value,
|
|
onChange,
|
|
placeholder,
|
|
className = '',
|
|
}: RichTextEditorProps) {
|
|
const editorRef = useRef<HTMLDivElement>(null);
|
|
|
|
const isMountedRef = useRef(false);
|
|
|
|
useEffect(() => {
|
|
if (editorRef.current) {
|
|
if (!isMountedRef.current) {
|
|
editorRef.current.innerHTML = value || '';
|
|
isMountedRef.current = true;
|
|
} else if (editorRef.current.innerHTML !== (value || '')) {
|
|
editorRef.current.innerHTML = value || '';
|
|
}
|
|
}
|
|
}, [value]);
|
|
|
|
const exec = (command: string, val: string | undefined = undefined) => {
|
|
document.execCommand(command, false, val);
|
|
if (editorRef.current) {
|
|
onChange(editorRef.current.innerHTML);
|
|
}
|
|
};
|
|
|
|
const handleLink = () => {
|
|
const url = prompt('آدرس لینک (URL) را وارد کنید:', 'https://');
|
|
if (url) {
|
|
exec('createLink', url);
|
|
}
|
|
};
|
|
|
|
const handleInput = () => {
|
|
if (editorRef.current) {
|
|
onChange(editorRef.current.innerHTML);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className={`border border-gray-200 rounded-2xl bg-white overflow-hidden shadow-xs ${className}`}>
|
|
{/* Toolbar */}
|
|
<div className="bg-gray-50/80 border-b border-gray-100 p-2 flex flex-wrap items-center gap-1 text-gray-700">
|
|
{/* Headings */}
|
|
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200">
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('formatBlock', '<h1>')}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg text-xs font-bold transition-colors cursor-pointer"
|
|
title="تگ تیتر H1 (برای سئو)"
|
|
>
|
|
<Heading1 className="w-4 h-4 text-purple-700" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('formatBlock', '<h2>')}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg text-xs font-bold transition-colors cursor-pointer"
|
|
title="تگ تیتر H2"
|
|
>
|
|
<Heading2 className="w-4 h-4 text-purple-600" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('formatBlock', '<h3>')}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg text-xs font-bold transition-colors cursor-pointer"
|
|
title="تگ تیتر H3"
|
|
>
|
|
<Heading3 className="w-4 h-4 text-purple-500" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('formatBlock', '<p>')}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg text-[11px] font-bold transition-colors cursor-pointer text-gray-600 px-2"
|
|
title="پاراگراف عادی (P)"
|
|
>
|
|
P
|
|
</button>
|
|
</div>
|
|
|
|
{/* Basic Formats */}
|
|
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200">
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('bold')}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
|
title="Bold"
|
|
>
|
|
<Bold className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('italic')}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
|
title="Italic"
|
|
>
|
|
<Italic className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('underline')}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
|
title="Underline"
|
|
>
|
|
<Underline className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Alignment */}
|
|
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200">
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('justifyRight')}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
|
title="راستچین"
|
|
>
|
|
<AlignRight className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('justifyCenter')}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
|
title="وسطچین"
|
|
>
|
|
<AlignCenter className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('justifyLeft')}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
|
title="چپچین"
|
|
>
|
|
<AlignLeft className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Lists & Link */}
|
|
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200">
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('insertUnorderedList')}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
|
title="لیست بالتدار"
|
|
>
|
|
<List className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('insertOrderedList')}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
|
title="لیست شمارهدار"
|
|
>
|
|
<ListOrdered className="w-4 h-4" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={handleLink}
|
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer text-blue-600"
|
|
title="درج لینک"
|
|
>
|
|
<Link className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Color Palette */}
|
|
<div className="flex items-center gap-1 bg-white px-2 py-1 rounded-xl border border-gray-200">
|
|
<Palette className="w-3.5 h-3.5 text-gray-400" />
|
|
<div className="flex items-center gap-1">
|
|
{COLORS.map((c) => (
|
|
<button
|
|
key={c.value}
|
|
type="button"
|
|
onClick={() => exec('foreColor', c.value)}
|
|
className="w-3.5 h-3.5 rounded-full border border-black/10 cursor-pointer hover:scale-125 transition-transform"
|
|
style={{ backgroundColor: c.value }}
|
|
title={c.label}
|
|
/>
|
|
))}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Undo / Redo */}
|
|
<div className="flex items-center gap-0.5 ms-auto">
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('undo')}
|
|
className="p-1.5 hover:bg-gray-200 rounded-lg transition-colors cursor-pointer"
|
|
title="Undo"
|
|
>
|
|
<Undo className="w-3.5 h-3.5 text-gray-500" />
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={() => exec('redo')}
|
|
className="p-1.5 hover:bg-gray-200 rounded-lg transition-colors cursor-pointer"
|
|
title="Redo"
|
|
>
|
|
<Redo className="w-3.5 h-3.5 text-gray-500" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Editable Area */}
|
|
<div
|
|
ref={editorRef}
|
|
contentEditable
|
|
onInput={handleInput}
|
|
data-placeholder={placeholder}
|
|
className="p-4 min-h-[140px] max-h-[350px] overflow-y-auto outline-none text-xs leading-relaxed text-gray-800 font-vazir empty:before:content-[attr(data-placeholder)] empty:before:text-gray-400"
|
|
dir="rtl"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|