Compare commits
No commits in common. "5371f1ba22b93d184f0b806ebe5dc668cceb0147" and "79c72517c295e77e3614b026b98359fdb09c6265" have entirely different histories.
5371f1ba22
...
79c72517c2
@ -1,4 +1,4 @@
|
|||||||
import React, { useRef, useEffect, useState, useCallback } from 'react';
|
import React, { useRef, useEffect } from 'react';
|
||||||
import {
|
import {
|
||||||
Bold,
|
Bold,
|
||||||
Italic,
|
Italic,
|
||||||
@ -8,16 +8,13 @@ import {
|
|||||||
Heading3,
|
Heading3,
|
||||||
List,
|
List,
|
||||||
ListOrdered,
|
ListOrdered,
|
||||||
Link as LinkIcon,
|
Link,
|
||||||
AlignRight,
|
AlignRight,
|
||||||
AlignCenter,
|
AlignCenter,
|
||||||
AlignLeft,
|
AlignLeft,
|
||||||
Palette,
|
Palette,
|
||||||
PaintBucket,
|
|
||||||
Undo,
|
Undo,
|
||||||
Redo,
|
Redo,
|
||||||
Code,
|
|
||||||
RemoveFormatting,
|
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
|
|
||||||
interface RichTextEditorProps {
|
interface RichTextEditorProps {
|
||||||
@ -27,7 +24,7 @@ interface RichTextEditorProps {
|
|||||||
className?: string;
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PRESET_COLORS = [
|
const COLORS = [
|
||||||
{ label: 'سیاه / پیشفرض', value: '#111827' },
|
{ label: 'سیاه / پیشفرض', value: '#111827' },
|
||||||
{ label: 'خاکستری', value: '#4b5563' },
|
{ label: 'خاکستری', value: '#4b5563' },
|
||||||
{ label: 'آبی کنینا', value: '#1d4ed8' },
|
{ label: 'آبی کنینا', value: '#1d4ed8' },
|
||||||
@ -37,29 +34,6 @@ const PRESET_COLORS = [
|
|||||||
{ label: 'کهربایی / زرد', value: '#d97706' },
|
{ label: 'کهربایی / زرد', value: '#d97706' },
|
||||||
];
|
];
|
||||||
|
|
||||||
const PRESET_BG_COLORS = [
|
|
||||||
{ label: 'بیرنگ / حذف پسزمینه', value: 'transparent' },
|
|
||||||
{ label: 'هایلایت زرد', value: '#fef08a' },
|
|
||||||
{ label: 'هایلایت آبی', value: '#dbeafe' },
|
|
||||||
{ label: 'هایلایت بنفش', value: '#f3e8ff' },
|
|
||||||
{ label: 'هایلایت سبز', value: '#dcfce7' },
|
|
||||||
{ label: 'هایلایت قرمز', value: '#fee2e2' },
|
|
||||||
{ label: 'خاکستری ملایم', value: '#f3f4f6' },
|
|
||||||
];
|
|
||||||
|
|
||||||
interface ActiveStates {
|
|
||||||
isBold: boolean;
|
|
||||||
isItalic: boolean;
|
|
||||||
isUnderline: boolean;
|
|
||||||
isH1: boolean;
|
|
||||||
isH2: boolean;
|
|
||||||
isH3: boolean;
|
|
||||||
isP: boolean;
|
|
||||||
isUL: boolean;
|
|
||||||
isOL: boolean;
|
|
||||||
align: 'right' | 'center' | 'left' | 'justify';
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function RichTextEditor({
|
export default function RichTextEditor({
|
||||||
value,
|
value,
|
||||||
onChange,
|
onChange,
|
||||||
@ -69,81 +43,8 @@ export default function RichTextEditor({
|
|||||||
const editorRef = useRef<HTMLDivElement>(null);
|
const editorRef = useRef<HTMLDivElement>(null);
|
||||||
const isTypingRef = useRef(false);
|
const isTypingRef = useRef(false);
|
||||||
const isMountedRef = useRef(false);
|
const isMountedRef = useRef(false);
|
||||||
const [isCodeMode, setIsCodeMode] = useState(false);
|
|
||||||
const [showColorPicker, setShowColorPicker] = useState(false);
|
|
||||||
const [showBgColorPicker, setShowBgColorPicker] = useState(false);
|
|
||||||
const [customColor, setCustomColor] = useState('#111827');
|
|
||||||
const [customBgColor, setCustomBgColor] = useState('#fef08a');
|
|
||||||
|
|
||||||
const [activeStates, setActiveStates] = useState<ActiveStates>({
|
// Sync value from props on initial mount or when external change occurs (not while active)
|
||||||
isBold: false,
|
|
||||||
isItalic: false,
|
|
||||||
isUnderline: false,
|
|
||||||
isH1: false,
|
|
||||||
isH2: false,
|
|
||||||
isH3: false,
|
|
||||||
isP: false,
|
|
||||||
isUL: false,
|
|
||||||
isOL: false,
|
|
||||||
align: 'right',
|
|
||||||
});
|
|
||||||
|
|
||||||
// Query formatting states on cursor/selection change
|
|
||||||
const updateActiveStates = useCallback(() => {
|
|
||||||
if (!editorRef.current || isCodeMode) return;
|
|
||||||
try {
|
|
||||||
const doc = document as any;
|
|
||||||
const isBold = doc.queryCommandState ? doc.queryCommandState('bold') : false;
|
|
||||||
const isItalic = doc.queryCommandState ? doc.queryCommandState('italic') : false;
|
|
||||||
const isUnderline = doc.queryCommandState ? doc.queryCommandState('underline') : false;
|
|
||||||
const isUL = doc.queryCommandState ? doc.queryCommandState('insertUnorderedList') : false;
|
|
||||||
const isOL = doc.queryCommandState ? doc.queryCommandState('insertOrderedList') : false;
|
|
||||||
|
|
||||||
let block = (doc.queryCommandValue ? doc.queryCommandValue('formatBlock') : '') || '';
|
|
||||||
block = block.toLowerCase().replace(/[<>]/g, '');
|
|
||||||
|
|
||||||
// Check parent tag if queryCommandValue is ambiguous
|
|
||||||
const selection = window.getSelection();
|
|
||||||
let parentTag = '';
|
|
||||||
if (selection && selection.rangeCount > 0) {
|
|
||||||
let node: Node | null = selection.getRangeAt(0).commonAncestorContainer;
|
|
||||||
if (node && node.nodeType === 3 && node.parentNode) {
|
|
||||||
node = node.parentNode;
|
|
||||||
}
|
|
||||||
while (node && node !== editorRef.current) {
|
|
||||||
const tag = (node as HTMLElement).tagName?.toLowerCase();
|
|
||||||
if (['h1', 'h2', 'h3', 'p', 'ul', 'ol'].includes(tag)) {
|
|
||||||
parentTag = tag;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
node = node.parentNode;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const activeBlock = parentTag || block;
|
|
||||||
|
|
||||||
setActiveStates({
|
|
||||||
isBold,
|
|
||||||
isItalic,
|
|
||||||
isUnderline,
|
|
||||||
isH1: activeBlock === 'h1',
|
|
||||||
isH2: activeBlock === 'h2',
|
|
||||||
isH3: activeBlock === 'h3',
|
|
||||||
isP: activeBlock === 'p' || (!['h1', 'h2', 'h3'].includes(activeBlock) && !isUL && !isOL),
|
|
||||||
isUL,
|
|
||||||
isOL,
|
|
||||||
align: doc.queryCommandState && doc.queryCommandState('justifyCenter')
|
|
||||||
? 'center'
|
|
||||||
: doc.queryCommandState && doc.queryCommandState('justifyLeft')
|
|
||||||
? 'left'
|
|
||||||
: 'right',
|
|
||||||
});
|
|
||||||
} catch {
|
|
||||||
// Ignored if document commands aren't ready
|
|
||||||
}
|
|
||||||
}, [isCodeMode]);
|
|
||||||
|
|
||||||
// Sync value from props on initial mount or when external change occurs
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!editorRef.current) return;
|
if (!editorRef.current) return;
|
||||||
if (!isMountedRef.current) {
|
if (!isMountedRef.current) {
|
||||||
@ -152,43 +53,22 @@ export default function RichTextEditor({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only update innerHTML if it's different and not currently focused/typing
|
||||||
if (editorRef.current.innerHTML !== (value || '')) {
|
if (editorRef.current.innerHTML !== (value || '')) {
|
||||||
if (!isTypingRef.current && document.activeElement !== editorRef.current) {
|
if (!isTypingRef.current && document.activeElement !== editorRef.current) {
|
||||||
editorRef.current.innerHTML = value || '';
|
editorRef.current.innerHTML = value || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [value, isCodeMode]);
|
}, [value]);
|
||||||
|
|
||||||
const exec = (command: string, val: string | undefined = undefined) => {
|
const exec = (command: string, val: string | undefined = undefined) => {
|
||||||
if (isCodeMode) return;
|
|
||||||
editorRef.current?.focus();
|
|
||||||
document.execCommand(command, false, val);
|
document.execCommand(command, false, val);
|
||||||
if (editorRef.current) {
|
if (editorRef.current) {
|
||||||
onChange(editorRef.current.innerHTML);
|
onChange(editorRef.current.innerHTML);
|
||||||
}
|
}
|
||||||
setTimeout(updateActiveStates, 20);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Robust formatBlock handler for H1, H2, H3, P
|
|
||||||
const setBlockTag = (tagName: 'h1' | 'h2' | 'h3' | 'p') => {
|
|
||||||
if (isCodeMode) return;
|
|
||||||
editorRef.current?.focus();
|
|
||||||
|
|
||||||
// Cross-browser formatBlock
|
|
||||||
const blockValue = `<${tagName}>`;
|
|
||||||
const success = document.execCommand('formatBlock', false, blockValue);
|
|
||||||
if (!success) {
|
|
||||||
document.execCommand('formatBlock', false, tagName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (editorRef.current) {
|
|
||||||
onChange(editorRef.current.innerHTML);
|
|
||||||
}
|
|
||||||
setTimeout(updateActiveStates, 20);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleLink = () => {
|
const handleLink = () => {
|
||||||
if (isCodeMode) return;
|
|
||||||
const url = prompt('آدرس لینک (URL) را وارد کنید:', 'https://');
|
const url = prompt('آدرس لینک (URL) را وارد کنید:', 'https://');
|
||||||
if (url) {
|
if (url) {
|
||||||
exec('createLink', url);
|
exec('createLink', url);
|
||||||
@ -202,167 +82,99 @@ export default function RichTextEditor({
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
isTypingRef.current = false;
|
isTypingRef.current = false;
|
||||||
}, 50);
|
}, 50);
|
||||||
updateActiveStates();
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearFormatting = () => {
|
|
||||||
exec('removeFormat');
|
|
||||||
setBlockTag('p');
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`border border-gray-200 rounded-2xl bg-white overflow-hidden shadow-xs relative ${className}`}>
|
<div className={`border border-gray-200 rounded-2xl bg-white overflow-hidden shadow-xs ${className}`}>
|
||||||
{/* Toolbar */}
|
{/* Toolbar */}
|
||||||
<div className="bg-gray-50/80 border-b border-gray-100 p-2 flex flex-wrap items-center gap-1 text-gray-700 select-none">
|
<div className="bg-gray-50/80 border-b border-gray-100 p-2 flex flex-wrap items-center gap-1 text-gray-700">
|
||||||
{/* Headings */}
|
{/* Headings */}
|
||||||
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200 shadow-2xs">
|
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
onClick={() => exec('formatBlock', '<h1>')}
|
||||||
onClick={() => setBlockTag('h1')}
|
className="p-1.5 hover:bg-gray-100 rounded-lg text-xs font-bold transition-colors cursor-pointer"
|
||||||
className={`px-2 py-1.5 rounded-lg text-xs font-black transition-all flex items-center gap-1 cursor-pointer disabled:opacity-40 ${
|
title="تگ تیتر H1 (برای سئو)"
|
||||||
activeStates.isH1
|
|
||||||
? 'bg-purple-600 text-white shadow-xs'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100 hover:text-purple-700'
|
|
||||||
}`}
|
|
||||||
title="تگ تیتر بزرگ H1 (سئو)"
|
|
||||||
>
|
>
|
||||||
<Heading1 className="w-3.5 h-3.5" />
|
<Heading1 className="w-4 h-4 text-purple-700" />
|
||||||
<span>H1</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
onClick={() => exec('formatBlock', '<h2>')}
|
||||||
onClick={() => setBlockTag('h2')}
|
className="p-1.5 hover:bg-gray-100 rounded-lg text-xs font-bold transition-colors cursor-pointer"
|
||||||
className={`px-2 py-1.5 rounded-lg text-xs font-black transition-all flex items-center gap-1 cursor-pointer disabled:opacity-40 ${
|
|
||||||
activeStates.isH2
|
|
||||||
? 'bg-purple-600 text-white shadow-xs'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100 hover:text-purple-600'
|
|
||||||
}`}
|
|
||||||
title="تگ تیتر H2"
|
title="تگ تیتر H2"
|
||||||
>
|
>
|
||||||
<Heading2 className="w-3.5 h-3.5" />
|
<Heading2 className="w-4 h-4 text-purple-600" />
|
||||||
<span>H2</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
onClick={() => exec('formatBlock', '<h3>')}
|
||||||
onClick={() => setBlockTag('h3')}
|
className="p-1.5 hover:bg-gray-100 rounded-lg text-xs font-bold transition-colors cursor-pointer"
|
||||||
className={`px-2 py-1.5 rounded-lg text-xs font-black transition-all flex items-center gap-1 cursor-pointer disabled:opacity-40 ${
|
|
||||||
activeStates.isH3
|
|
||||||
? 'bg-purple-600 text-white shadow-xs'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100 hover:text-purple-500'
|
|
||||||
}`}
|
|
||||||
title="تگ تیتر H3"
|
title="تگ تیتر H3"
|
||||||
>
|
>
|
||||||
<Heading3 className="w-3.5 h-3.5" />
|
<Heading3 className="w-4 h-4 text-purple-500" />
|
||||||
<span>H3</span>
|
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
onClick={() => exec('formatBlock', '<p>')}
|
||||||
onClick={() => setBlockTag('p')}
|
className="p-1.5 hover:bg-gray-100 rounded-lg text-[11px] font-bold transition-colors cursor-pointer text-gray-600 px-2"
|
||||||
className={`px-2.5 py-1.5 rounded-lg text-xs font-bold transition-all cursor-pointer disabled:opacity-40 ${
|
|
||||||
activeStates.isP && !activeStates.isH1 && !activeStates.isH2 && !activeStates.isH3
|
|
||||||
? 'bg-purple-600 text-white shadow-xs'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
title="پاراگراف عادی (P)"
|
title="پاراگراف عادی (P)"
|
||||||
>
|
>
|
||||||
P
|
P
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Basic Formats (Bold, Italic, Underline) */}
|
{/* Basic Formats */}
|
||||||
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200 shadow-2xs">
|
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={() => exec('bold')}
|
onClick={() => exec('bold')}
|
||||||
className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
||||||
activeStates.isBold
|
title="Bold"
|
||||||
? 'bg-purple-600 text-white shadow-xs'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
title="بولد (ضخیم)"
|
|
||||||
>
|
>
|
||||||
<Bold className="w-4 h-4" />
|
<Bold className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={() => exec('italic')}
|
onClick={() => exec('italic')}
|
||||||
className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
||||||
activeStates.isItalic
|
title="Italic"
|
||||||
? 'bg-purple-600 text-white shadow-xs'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
title="ایتالیک (کج)"
|
|
||||||
>
|
>
|
||||||
<Italic className="w-4 h-4" />
|
<Italic className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={() => exec('underline')}
|
onClick={() => exec('underline')}
|
||||||
className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
||||||
activeStates.isUnderline
|
title="Underline"
|
||||||
? 'bg-purple-600 text-white shadow-xs'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
title="خط زیرین (Underline)"
|
|
||||||
>
|
>
|
||||||
<Underline className="w-4 h-4" />
|
<Underline className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={clearFormatting}
|
|
||||||
className="p-1.5 rounded-lg text-gray-500 hover:bg-gray-100 hover:text-red-600 transition-colors cursor-pointer disabled:opacity-40"
|
|
||||||
title="پاکسازی قالببندی و استایلها"
|
|
||||||
>
|
|
||||||
<RemoveFormatting className="w-4 h-4" />
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Alignment */}
|
{/* Alignment */}
|
||||||
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200 shadow-2xs">
|
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={() => exec('justifyRight')}
|
onClick={() => exec('justifyRight')}
|
||||||
className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
||||||
activeStates.align === 'right'
|
|
||||||
? 'bg-purple-50 text-purple-700 font-bold'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
title="راستچین"
|
title="راستچین"
|
||||||
>
|
>
|
||||||
<AlignRight className="w-4 h-4" />
|
<AlignRight className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={() => exec('justifyCenter')}
|
onClick={() => exec('justifyCenter')}
|
||||||
className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
||||||
activeStates.align === 'center'
|
|
||||||
? 'bg-purple-50 text-purple-700 font-bold'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
title="وسطچین"
|
title="وسطچین"
|
||||||
>
|
>
|
||||||
<AlignCenter className="w-4 h-4" />
|
<AlignCenter className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={() => exec('justifyLeft')}
|
onClick={() => exec('justifyLeft')}
|
||||||
className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
||||||
activeStates.align === 'left'
|
|
||||||
? 'bg-purple-50 text-purple-700 font-bold'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
title="چپچین"
|
title="چپچین"
|
||||||
>
|
>
|
||||||
<AlignLeft className="w-4 h-4" />
|
<AlignLeft className="w-4 h-4" />
|
||||||
@ -370,249 +182,81 @@ export default function RichTextEditor({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Lists & Link */}
|
{/* Lists & Link */}
|
||||||
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200 shadow-2xs">
|
<div className="flex items-center gap-0.5 bg-white p-0.5 rounded-xl border border-gray-200">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={() => exec('insertUnorderedList')}
|
onClick={() => exec('insertUnorderedList')}
|
||||||
className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
||||||
activeStates.isUL
|
title="لیست بالتدار"
|
||||||
? 'bg-purple-600 text-white shadow-xs'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
title="لیست بالتدار (نقطهای)"
|
|
||||||
>
|
>
|
||||||
<List className="w-4 h-4" />
|
<List className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={() => exec('insertOrderedList')}
|
onClick={() => exec('insertOrderedList')}
|
||||||
className={`p-1.5 rounded-lg transition-all cursor-pointer disabled:opacity-40 ${
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer"
|
||||||
activeStates.isOL
|
|
||||||
? 'bg-purple-600 text-white shadow-xs'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
title="لیست شمارهدار"
|
title="لیست شمارهدار"
|
||||||
>
|
>
|
||||||
<ListOrdered className="w-4 h-4" />
|
<ListOrdered className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={handleLink}
|
onClick={handleLink}
|
||||||
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer text-blue-600 disabled:opacity-40"
|
className="p-1.5 hover:bg-gray-100 rounded-lg transition-colors cursor-pointer text-blue-600"
|
||||||
title="درج لینک اینترنتی"
|
title="درج لینک"
|
||||||
>
|
>
|
||||||
<LinkIcon className="w-4 h-4" />
|
<Link className="w-4 h-4" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Text Color Picker & Custom HEX */}
|
{/* Color Palette */}
|
||||||
<div className="relative">
|
<div className="flex items-center gap-1 bg-white px-2 py-1 rounded-xl border border-gray-200">
|
||||||
<button
|
<Palette className="w-3.5 h-3.5 text-gray-400" />
|
||||||
type="button"
|
<div className="flex items-center gap-1">
|
||||||
disabled={isCodeMode}
|
{COLORS.map((c) => (
|
||||||
onClick={() => {
|
|
||||||
setShowColorPicker(!showColorPicker);
|
|
||||||
setShowBgColorPicker(false);
|
|
||||||
}}
|
|
||||||
className={`flex items-center gap-1.5 bg-white px-2.5 py-1.5 rounded-xl border border-gray-200 shadow-2xs text-xs font-bold cursor-pointer disabled:opacity-40 ${
|
|
||||||
showColorPicker ? 'ring-2 ring-purple-500 bg-purple-50' : 'hover:bg-gray-50'
|
|
||||||
}`}
|
|
||||||
title="انتخاب رنگ متن"
|
|
||||||
>
|
|
||||||
<Palette className="w-3.5 h-3.5 text-purple-600" />
|
|
||||||
<span>رنگ متن</span>
|
|
||||||
<span
|
|
||||||
className="w-3 h-3 rounded-full border border-black/20"
|
|
||||||
style={{ backgroundColor: customColor }}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showColorPicker && (
|
|
||||||
<div className="absolute top-full right-0 mt-1.5 p-3 bg-white rounded-2xl shadow-xl border border-gray-200 z-50 w-56 space-y-2.5 font-vazir text-right">
|
|
||||||
<span className="text-[11px] font-bold text-gray-500 block">رنگهای پیشنهادی:</span>
|
|
||||||
<div className="grid grid-cols-7 gap-1">
|
|
||||||
{PRESET_COLORS.map((c) => (
|
|
||||||
<button
|
<button
|
||||||
key={c.value}
|
key={c.value}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => exec('foreColor', c.value)}
|
||||||
exec('foreColor', c.value);
|
className="w-3.5 h-3.5 rounded-full border border-black/10 cursor-pointer hover:scale-125 transition-transform"
|
||||||
setCustomColor(c.value);
|
|
||||||
setShowColorPicker(false);
|
|
||||||
}}
|
|
||||||
className="w-6 h-6 rounded-lg border border-black/10 cursor-pointer hover:scale-110 transition-transform"
|
|
||||||
style={{ backgroundColor: c.value }}
|
style={{ backgroundColor: c.value }}
|
||||||
title={c.label}
|
title={c.label}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="pt-2 border-t border-gray-100 flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={customColor}
|
|
||||||
onChange={(e) => {
|
|
||||||
setCustomColor(e.target.value);
|
|
||||||
exec('foreColor', e.target.value);
|
|
||||||
}}
|
|
||||||
className="w-8 h-8 rounded-lg cursor-pointer border-none bg-transparent"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={customColor}
|
|
||||||
onChange={(e) => {
|
|
||||||
setCustomColor(e.target.value);
|
|
||||||
exec('foreColor', e.target.value);
|
|
||||||
}}
|
|
||||||
placeholder="#111827"
|
|
||||||
className="w-full text-xs font-mono font-bold px-2 py-1 border border-gray-200 rounded-lg outline-none uppercase"
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Background Color Picker & Custom HEX */}
|
|
||||||
<div className="relative">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={() => {
|
|
||||||
setShowBgColorPicker(!showBgColorPicker);
|
|
||||||
setShowColorPicker(false);
|
|
||||||
}}
|
|
||||||
className={`flex items-center gap-1.5 bg-white px-2.5 py-1.5 rounded-xl border border-gray-200 shadow-2xs text-xs font-bold cursor-pointer disabled:opacity-40 ${
|
|
||||||
showBgColorPicker ? 'ring-2 ring-purple-500 bg-purple-50' : 'hover:bg-gray-50'
|
|
||||||
}`}
|
|
||||||
title="هایلایت و رنگ پسزمینه متن"
|
|
||||||
>
|
|
||||||
<PaintBucket className="w-3.5 h-3.5 text-amber-500" />
|
|
||||||
<span>هایلایت</span>
|
|
||||||
<span
|
|
||||||
className="w-3 h-3 rounded-full border border-black/20"
|
|
||||||
style={{ backgroundColor: customBgColor }}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
{showBgColorPicker && (
|
|
||||||
<div className="absolute top-full right-0 mt-1.5 p-3 bg-white rounded-2xl shadow-xl border border-gray-200 z-50 w-56 space-y-2.5 font-vazir text-right">
|
|
||||||
<span className="text-[11px] font-bold text-gray-500 block">رنگهای هایلایت:</span>
|
|
||||||
<div className="grid grid-cols-7 gap-1">
|
|
||||||
{PRESET_BG_COLORS.map((c) => (
|
|
||||||
<button
|
|
||||||
key={c.value}
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
exec('hiliteColor', c.value);
|
|
||||||
setCustomBgColor(c.value);
|
|
||||||
setShowBgColorPicker(false);
|
|
||||||
}}
|
|
||||||
className="w-6 h-6 rounded-lg border border-black/10 cursor-pointer hover:scale-110 transition-transform"
|
|
||||||
style={{ backgroundColor: c.value }}
|
|
||||||
title={c.label}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="pt-2 border-t border-gray-100 flex items-center gap-2">
|
|
||||||
<input
|
|
||||||
type="color"
|
|
||||||
value={customBgColor === 'transparent' ? '#ffffff' : customBgColor}
|
|
||||||
onChange={(e) => {
|
|
||||||
setCustomBgColor(e.target.value);
|
|
||||||
exec('hiliteColor', e.target.value);
|
|
||||||
}}
|
|
||||||
className="w-8 h-8 rounded-lg cursor-pointer border-none bg-transparent"
|
|
||||||
/>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={customBgColor}
|
|
||||||
onChange={(e) => {
|
|
||||||
setCustomBgColor(e.target.value);
|
|
||||||
exec('hiliteColor', e.target.value);
|
|
||||||
}}
|
|
||||||
placeholder="#fef08a"
|
|
||||||
className="w-full text-xs font-mono font-bold px-2 py-1 border border-gray-200 rounded-lg outline-none uppercase"
|
|
||||||
dir="ltr"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* HTML Code View vs Visual */}
|
|
||||||
<div className="flex items-center bg-white p-0.5 rounded-xl border border-gray-200 shadow-2xs">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setIsCodeMode(!isCodeMode);
|
|
||||||
setShowColorPicker(false);
|
|
||||||
setShowBgColorPicker(false);
|
|
||||||
}}
|
|
||||||
className={`px-2.5 py-1.5 rounded-lg text-xs font-bold transition-all flex items-center gap-1.5 cursor-pointer ${
|
|
||||||
isCodeMode
|
|
||||||
? 'bg-purple-600 text-white shadow-xs'
|
|
||||||
: 'text-gray-700 hover:bg-gray-100'
|
|
||||||
}`}
|
|
||||||
title="نمایش و ویرایش مستقیم کد HTML"
|
|
||||||
>
|
|
||||||
<Code className="w-3.5 h-3.5" />
|
|
||||||
<span>{isCodeMode ? 'نمای دیداری' : 'کد HTML'}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Undo / Redo */}
|
{/* Undo / Redo */}
|
||||||
<div className="flex items-center gap-0.5 ms-auto">
|
<div className="flex items-center gap-0.5 ms-auto">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={() => exec('undo')}
|
onClick={() => exec('undo')}
|
||||||
className="p-1.5 hover:bg-gray-200 disabled:opacity-40 rounded-lg transition-colors cursor-pointer"
|
className="p-1.5 hover:bg-gray-200 rounded-lg transition-colors cursor-pointer"
|
||||||
title="بازگشت به عقب (Undo)"
|
title="Undo"
|
||||||
>
|
>
|
||||||
<Undo className="w-3.5 h-3.5 text-gray-500" />
|
<Undo className="w-3.5 h-3.5 text-gray-500" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={isCodeMode}
|
|
||||||
onClick={() => exec('redo')}
|
onClick={() => exec('redo')}
|
||||||
className="p-1.5 hover:bg-gray-200 disabled:opacity-40 rounded-lg transition-colors cursor-pointer"
|
className="p-1.5 hover:bg-gray-200 rounded-lg transition-colors cursor-pointer"
|
||||||
title="حرکت به جلو (Redo)"
|
title="Redo"
|
||||||
>
|
>
|
||||||
<Redo className="w-3.5 h-3.5 text-gray-500" />
|
<Redo className="w-3.5 h-3.5 text-gray-500" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Editor Content Area */}
|
{/* Editable Area */}
|
||||||
{isCodeMode ? (
|
|
||||||
<textarea
|
|
||||||
value={value || ''}
|
|
||||||
onChange={(e) => onChange(e.target.value)}
|
|
||||||
placeholder="کد HTML را اینجا وارد یا ویرایش کنید..."
|
|
||||||
className="w-full p-4 min-h-[140px] max-h-[350px] font-mono text-xs text-emerald-400 bg-slate-950 outline-none resize-y leading-relaxed"
|
|
||||||
dir="ltr"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<div
|
<div
|
||||||
ref={editorRef}
|
ref={editorRef}
|
||||||
contentEditable
|
contentEditable
|
||||||
suppressContentEditableWarning
|
suppressContentEditableWarning
|
||||||
onInput={handleInput}
|
onInput={handleInput}
|
||||||
onKeyUp={updateActiveStates}
|
|
||||||
onMouseUp={updateActiveStates}
|
|
||||||
onClick={updateActiveStates}
|
|
||||||
onFocus={updateActiveStates}
|
|
||||||
data-placeholder={placeholder}
|
data-placeholder={placeholder}
|
||||||
className="p-4 min-h-[140px] max-h-[350px] overflow-y-auto outline-none text-sm leading-relaxed text-gray-800 font-vazir empty:before:content-[attr(data-placeholder)] empty:before:text-gray-400 [&_h1]:text-2xl [&_h1]:font-black [&_h1]:my-2.5 [&_h1]:text-gray-900 [&_h2]:text-xl [&_h2]:font-bold [&_h2]:my-2 [&_h2]:text-gray-900 [&_h3]:text-lg [&_h3]:font-bold [&_h3]:my-1.5 [&_h3]:text-gray-900 [&_p]:my-1 [&_p]:leading-relaxed [&_ul]:list-disc [&_ul]:ms-6 [&_ul]:my-2 [&_ol]:list-decimal [&_ol]:ms-6 [&_ol]:my-2 [&_a]:text-blue-600 [&_a]:underline"
|
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"
|
dir="rtl"
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -138,8 +138,8 @@ export default function Hero({ banners = [], onShopNavigate }: { banners?: Banne
|
|||||||
{titleParts[1] && <span className="text-canina-blue ">{titleParts[1]}</span>}
|
{titleParts[1] && <span className="text-canina-blue ">{titleParts[1]}</span>}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<div
|
<p
|
||||||
className="text-sm lg:text-xl text-medical-gray-600 mb-3 sm:mb-5 max-w-2xl mx-auto lg:mx-0 leading-relaxed font-vazir animate-fade-in [&_p]:text-sm [&_p]:lg:text-xl [&_p]:leading-relaxed [&_p]:my-0 [&_span]:text-inherit"
|
className="text-sm lg:text-xl text-medical-gray-600 mb-3 sm:mb-5 max-w-2xl mx-auto lg:mx-0 leading-relaxed font-vazir animate-fade-in"
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: subtitle
|
__html: subtitle
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user