canina/frontend/admin-panel/src/components/ui/RichTextEditor.tsx

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>
);
}