nilfam-editor
Version:
A powerful, customizable rich-text editor built with TipTap, React, and Tailwind CSS. Supports RTL/LTR text, resizable media (images/videos), tables, code blocks, font styling, and more for an enhanced content creation experience.
99 lines (83 loc) • 4 kB
JavaScript
import { Plugin } from 'prosemirror-state'
import { Heading } from '@tiptap/extension-heading'
function createSlug(text) {
return text
.trim()
.replace(/\s+/g, '-')
.replace(/[؟?.,،؛:]/g, '')
.toLowerCase()
}
export const HeadingWithAutoId = Heading.extend({
/* ➊ تعریف اتریبیوتهای id و class */
addAttributes() {
return {
...this.parent?.(),
id: {
default : null,
parseHTML : el => el.getAttribute('id'),
renderHTML : attrs => attrs.id ? { id: attrs.id } : {},
},
class: {
default : 'anchor-target', // همیشه این کلاس را داریم
parseHTML : el => {
const classes = el.getAttribute('class')?.split(/\s+/) ?? []
return [...new Set([...classes, 'anchor-target'])].join(' ')
},
renderHTML : attrs => {
const classes = new Set((attrs.class ?? '').split(/\s+/).filter(Boolean))
classes.add('anchor-target')
return { class: Array.from(classes).join(' ') }
},
},
}
},
/* ➋ پلاگین ProseMirror برای مدیریت آیدیها */
addProseMirrorPlugins() {
return [
new Plugin({
appendTransaction: (transactions, oldState, newState) => {
// اگر هیچ تغییری در داکیومنت رخ نداده، کاری نکن
if (!transactions.some(tr => tr.docChanged)) return null
const { doc } = newState
const tr = newState.tr
let needUpdate = false
/* جدولی برای شمارش تکرار اسلاگها */
const slugCount = new Map() // key = slug, value = تعداد دیدهشده
doc.descendants((node, pos) => {
if (node.type.name !== 'heading') return
const text = node.textContent.trim() // محتوای هدینگ
let { id, class: cls = '' } = node.attrs
let changed = false
/* ① اگر متن خالی بود، آیدی را کاملاً پاک کن */
if (!text) {
if (id) {
tr.setNodeMarkup(pos, undefined, { ...node.attrs, id: null })
needUpdate = true
}
return
}
/* ② تولید اسلاگ + جلوگیری از تکرار */
const base = createSlug(text) || 'untitled'
const seen = slugCount.get(base) ?? 0
const unique = seen === 0 ? base : `${base}-${seen}`
slugCount.set(base, seen + 1)
if (id !== unique) {
id = unique
changed = true
}
/* ③ اطمینان از وجود کلاس anchor-target */
if (!cls.split(/\s+/).includes('anchor-target')) {
cls = `${cls} anchor-target`.trim()
changed = true
}
if (changed) {
tr.setNodeMarkup(pos, undefined, { ...node.attrs, id, class: cls })
needUpdate = true
}
})
return needUpdate ? tr : null
},
}),
]
},
})