import { useEffect, useRef, useState } from 'react' import { EditorContent, useEditor } from '@tiptap/react' import StarterKit from '@tiptap/starter-kit' import Link from '@tiptap/extension-link' import { Mark, mergeAttributes } from '@tiptap/core' import { AlignLeftIcon, AlignRightIcon, BoldIcon, Heading1Icon, Heading2Icon, Heading3Icon, ImagePlusIcon, ItalicIcon, Link2Icon, ListIcon, ListOrderedIcon, Loader2Icon, PilcrowIcon, QuoteIcon, Redo2Icon, StrikethroughIcon, UnderlineIcon, Undo2Icon, } from 'lucide-react' import { useAdmin } from '../../admin/AdminContext' import { adminApi } from '../../admin/adminClient' import { DEFAULT_IMAGE_ALT } from '../../data/constants' import { buildLinkRel, isAllowedHref, isUrlLike, normalizeHref, parseLinkRel } from '../../utils/link' import { ArticleImage } from './ArticleImage' import { BidiTypography, bidiPluginKey } from './BidiTypography' import { EnglishText } from './EnglishText' import { LinkModal } from './LinkModal' import { applyBlockFormat, handleHeadingEnter } from './blockFormat' import type { LinkModalValue } from './LinkModal' /** * مارک‌های قدیمی (سایز ۱۲ و متن انگلیسی) — کنترل آن‌ها از نوار ابزار برداشته شده و * فقط برای اینکه محتوای قبلی از دست نرود در شِما نگه داشته شده‌اند. */ const LegacySmallSizeMark = Mark.create({ name: 'smallSize', parseHTML() { return [{ tag: 'span[data-small]' }] }, renderHTML({ HTMLAttributes }) { return ['span', mergeAttributes(HTMLAttributes, { 'data-small': 'true' }), 0] }, }) /** اکستنشن‌های مشترک ویرایشگر و رندر مقاله — هر دو باید یکسان باشند */ export const ARTICLE_EXTENSIONS = [ StarterKit.configure({ link: false, heading: { levels: [1, 2, 3] }, }), ArticleImage.configure({ allowBase64: false }), Link.configure({ openOnClick: false, HTMLAttributes: { rel: 'noopener noreferrer', target: '_blank' }, isAllowedUri: (uri: string) => isAllowedHref(uri), }), LegacySmallSizeMark, EnglishText, BidiTypography, ] type ArticleEditorProps = { doc: Record onChange: (json: Record) => void /** * شناسه محتوا — با هر تغییر آن، بدنه ویرایشگر از `doc` جاری دوباره بارگذاری می‌شود * (مثلاً وقتی مقاله از سرور می‌رسد). بدون این prop محتوا فقط یک‌بار مقداردهی می‌شود. */ contentKey?: string | number } /** نمونه ویرایشگر TipTap — همان چیزی که useEditor برمی‌گرداند */ type EditorInstance = NonNullable> type TextDirection = 'rtl' | 'ltr' /** نحوه اعمال لینک: روی انتخاب فعلی، درج متن تازه یا تبدیل محدوده تایپ‌شده */ type LinkApplyMode = 'selection' | 'insert' | 'range' type LinkModalState = { open: boolean editing: boolean mode: LinkApplyMode range: { from: number; to: number } | null initial: LinkModalValue } const CLOSED_LINK_MODAL: LinkModalState = { open: false, editing: false, mode: 'selection', range: null, initial: { href: '', newTab: true, nofollow: false }, } /** چند نویسه پیش از نشانگر برای تشخیص نشانیِ تایپ‌شده بررسی می‌شود */ const URL_LOOKBACK = 300 function ToolButton({ onClick, active = false, label, children, }: { onClick: () => void active?: boolean label: string children: React.ReactNode }) { return ( ) } const Divider = () =>