117 lines
4.8 KiB
TypeScript
117 lines
4.8 KiB
TypeScript
import { useEffect, useRef, useState } from 'react'
|
|
import { NodeViewWrapper } from '@tiptap/react'
|
|
import type { NodeViewProps } from '@tiptap/react'
|
|
import { PlusIcon, XIcon } from 'lucide-react'
|
|
|
|
type ImageAttrs = {
|
|
src: string
|
|
alt: string | null
|
|
title: string | null
|
|
caption: string | null
|
|
}
|
|
|
|
const DELETE_BUTTON_CLASS =
|
|
'flex h-7 w-7 shrink-0 items-center justify-center border border-red-400/50 bg-ink/85 text-red-300 backdrop-blur-sm transition-colors duration-150 hover:bg-red-500/20'
|
|
|
|
const INLINE_FIELD_CLASS =
|
|
'w-full border border-ivory/15 bg-ink px-3 py-1.5 text-xs font-light text-ivory placeholder:text-ivory/30 focus:border-copper focus:outline-none'
|
|
|
|
const ADD_CAPTION_CLASS =
|
|
'mt-1.5 inline-flex items-center gap-1.5 border border-dashed border-ivory/25 px-3 py-1 text-[11px] font-light text-ivory/50 transition-colors duration-150 hover:border-copper hover:text-copper'
|
|
|
|
/**
|
|
* نمایش تصویر مقاله در ویرایشگر — دقیقاً با همان ساختار خروجی سایت:
|
|
* <figure><img><figcaption>کپشن</figcaption></figure>
|
|
*
|
|
* ویرایش کاملاً درونخطی است (بدون پاپاور/مودال):
|
|
* - هر تصویر یک «جعبه توضیح/کپشن» همراه خود دارد و متن آن در figcaption ذخیره میشود.
|
|
* - دکمه X کپشن را کامل حذف میکند: صفت کپشن به مقدار پیشفرض برمیگردد و عنصر
|
|
* figcaption با ورودیاش از DOM ویرایشگر (و از خروجی ذخیرهشده) حذف میشود؛
|
|
* فقط تصویر میماند و «افزودن کپشن» دوباره جعبه را باز میکند.
|
|
* - ویرایش Alt در ویرایشگر وجود ندارد؛ Alt هنگام درج از نام فایل ثبت میشود.
|
|
* - دکمه X روی تصویر کل تصویر را حذف میکند.
|
|
*/
|
|
export function ImageNodeView({ node, updateAttributes, deleteNode, selected }: NodeViewProps) {
|
|
const { src, alt, title, caption } = node.attrs as ImageAttrs
|
|
const hasCaption = Boolean((caption ?? '').trim())
|
|
const [captionOpen, setCaptionOpen] = useState(true)
|
|
const captionInputRef = useRef<HTMLInputElement>(null)
|
|
const focusOnOpen = useRef(false)
|
|
const showCaption = captionOpen || hasCaption
|
|
|
|
// فقط وقتی کاربر «افزودن کپشن» را میزند ورودی فوکوس میگیرد (نه هنگام بارگذاری مقاله)
|
|
useEffect(() => {
|
|
if (!focusOnOpen.current) return
|
|
focusOnOpen.current = false
|
|
captionInputRef.current?.focus()
|
|
}, [captionOpen])
|
|
|
|
/** حذف کامل کپشن: عنصر figcaption و ورودیاش از DOM ویرایشگر بیرون میرود */
|
|
const removeCaption = () => {
|
|
updateAttributes({ caption: '' })
|
|
setCaptionOpen(false)
|
|
}
|
|
|
|
const addCaption = () => {
|
|
focusOnOpen.current = true
|
|
setCaptionOpen(true)
|
|
}
|
|
|
|
return (
|
|
<NodeViewWrapper
|
|
className={`article-figure-node ${selected ? 'is-selected' : ''}`}
|
|
data-drag-handle
|
|
>
|
|
<figure className="article-figure relative">
|
|
<img src={src} alt={alt ?? ''} title={title ?? ''} draggable={false} />
|
|
|
|
{/* حذف کل تصویر */}
|
|
<button
|
|
type="button"
|
|
contentEditable={false}
|
|
onClick={() => deleteNode()}
|
|
aria-label="حذف تصویر"
|
|
title="حذف تصویر"
|
|
className={`absolute left-2 top-2 z-10 ${DELETE_BUTTON_CLASS}`}
|
|
>
|
|
<XIcon className="h-3.5 w-3.5" />
|
|
</button>
|
|
|
|
{showCaption ? (
|
|
<figcaption className="article-figure__caption" contentEditable={false}>
|
|
<input
|
|
ref={captionInputRef}
|
|
type="text"
|
|
value={caption ?? ''}
|
|
onChange={(event) => updateAttributes({ caption: event.target.value })}
|
|
placeholder="توضیح تصویر (کپشن)…"
|
|
aria-label="کپشن تصویر"
|
|
className={INLINE_FIELD_CLASS}
|
|
/>
|
|
{/* حذف کامل کپشن — تصویر دستنخورده میماند */}
|
|
<button
|
|
type="button"
|
|
onClick={removeCaption}
|
|
aria-label="حذف کپشن"
|
|
title="حذف کپشن (تصویر حذف نمیشود)"
|
|
className={DELETE_BUTTON_CLASS}
|
|
>
|
|
<XIcon className="h-3.5 w-3.5" />
|
|
</button>
|
|
</figcaption>
|
|
) : (
|
|
<button
|
|
type="button"
|
|
contentEditable={false}
|
|
onClick={addCaption}
|
|
className={ADD_CAPTION_CLASS}
|
|
>
|
|
<PlusIcon className="h-3 w-3" aria-hidden="true" />
|
|
افزودن کپشن
|
|
</button>
|
|
)}
|
|
</figure>
|
|
</NodeViewWrapper>
|
|
)
|
|
}
|