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.
211 lines (189 loc) • 8.53 kB
JSX
import { Node, mergeAttributes } from '@tiptap/core'
export default Node.create({
name: 'audio',
group: 'block',
atom: true,
draggable: true,
selectable: true,
/** ویژگیها (Attributes) */
addAttributes() {
return {
src: { default: null },
controls: { default: true },
alt: { default: null },
align: { default: 'left' }, // چپ | وسط | راست
style: { default: 'width:560px;height:auto;cursor:pointer;' },
}
},
/** تعریف Schema */
parseHTML() {
return [
{
tag: 'audio[src]',
getAttrs: (dom) => {
const domStyle = dom.getAttribute('style') || '';
// استخراج align از استایل (برای حفظ بعد از لود)
let align = 'left';
if (domStyle.includes('margin-left:auto') && domStyle.includes('margin-right:auto')) {
align = 'center';
} else if (domStyle.includes('float:right')) {
align = 'right';
}
// تمیز کردن style: حذف قسمتهای ترازبندی، نگه داشتن width/height/cursor
const cleanStyle = domStyle
.replace(/float:\s*\w+;\s*/g, '')
.replace(/display:\s*block;\s*/g, '')
.replace(/margin-left:\s*auto;\s*/g, '')
.replace(/margin-right:\s*auto;\s*/g, '')
.trim();
return {
src: dom.getAttribute('src'),
alt: dom.getAttribute('alt'),
controls: dom.hasAttribute('controls'),
align,
style: cleanStyle || 'width:560px;height:auto;cursor:pointer;',
};
},
},
];
},
/** خروجی HTML */
renderHTML({ HTMLAttributes }) {
const { align = 'left', style = '' } = HTMLAttributes
// استایل برای ترازبندی
const extra =
align === 'center'
? 'display:block;margin-left:auto;margin-right:auto;'
: align === 'right'
? 'float:right;margin-left:auto;'
: 'float:left;'
return [
'audio',
mergeAttributes(
HTMLAttributes,
{
controls: true, // همیشه controls داشته باشه
style: style + extra, // استایل اصلی + ترازبندی
align: null, // حذف ویژگی منسوخ align
},
),
]
},
/** دستورات (Commands) */
addCommands() {
return {
insertAudio:
attrs =>
({ commands }) =>
commands.insertContent({ type: this.name, attrs }),
}
},
/** نمایش در ادیتور (Node-View) */
addNodeView() {
return ({ node, editor, getPos }) => {
const { view } = editor
const { style, align } = node.attrs
/** wrapper برای ترازبندی در ادیتور */
const wrap = document.createElement('div')
wrap.style.cssText =
'display:flex;width:100%;justify-content:' +
(align === 'center'
? 'center'
: align === 'right'
? 'flex-end'
: 'flex-start')
/** کانتینر اصلی */
const box = document.createElement('div')
box.style.cssText = `position:relative;display:inline-block;${style}`
/** عنصر صوتی */
const audio = document.createElement('audio')
audio.setAttribute('controls', 'true')
Object.entries(node.attrs).forEach(([k, v]) => v && audio.setAttribute(k, v))
/** نوار ابزار بالای فایل صوتی */
const bar = document.createElement('div')
bar.style.cssText =
'position:absolute;top:0;left:50%;transform:translate(-50%,-100%);' +
'display:flex;gap:4px;padding:2px 4px;background:#fff;border:1px solid #888;' +
'border-radius:4px;z-index:10;font-size:0;'
/** تابع کمکی برای ساخت دکمهها */
const mkBtn = (title, svg, fn) => {
const btn = document.createElement('span')
btn.title = title
btn.style.cssText =
'width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;' +
'cursor:pointer;border-radius:4px;'
btn.innerHTML = `<img src="${svg}" width="20" height="20">`
btn.onclick = fn
bar.appendChild(btn)
}
/** تابعهای کمکی برای ذخیره تغییرات */
const dispatch = attrs =>
view.dispatch(
view.state.tr.setNodeMarkup(getPos(), null, { ...node.attrs, ...attrs }),
)
const saveAlign = a => dispatch({ align: a })
/** دکمههای ترازبندی */
mkBtn(
'چپ',
'https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/format_align_left/default/20px.svg',
() => {
wrap.style.justifyContent = 'flex-start'
saveAlign('left')
},
)
mkBtn(
'وسط',
'https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/format_align_center/default/20px.svg',
() => {
wrap.style.justifyContent = 'center'
saveAlign('center')
},
)
mkBtn(
'راست',
'https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/format_align_right/default/20px.svg',
() => {
wrap.style.justifyContent = 'flex-end'
saveAlign('right')
},
)
/** ویرایش alt */
mkBtn(
'alt',
'https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/edit/default/20px.svg',
() => {
const cur = audio.getAttribute('alt') || ''
const txt = prompt('متن alt را وارد کنید:', cur)
if (txt !== null) {
audio.setAttribute('alt', txt)
dispatch({ alt: txt })
}
},
)
/** حذف فایل صوتی */
mkBtn(
'حذف',
'https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/delete/default/20px.svg',
() => {
const from = getPos()
view.dispatch(view.state.tr.delete(from, from + node.nodeSize))
},
)
/** دستگیره جابجایی (Drag Handle) */
const drag = document.createElement('span')
drag.title = 'جابجایی'
drag.setAttribute('draggable', 'true')
drag.classList.add('ProseMirror-drag-handle')
drag.style.cssText =
'width:24px;height:24px;display:inline-flex;align-items:center;justify-content:center;' +
'cursor:grab;border-radius:4px;'
drag.innerHTML =
'<img src="https://fonts.gstatic.com/s/i/short-term/release/materialsymbolsoutlined/open_with/default/20px.svg" width="20" height="20">'
bar.appendChild(drag)
/** مونتاژ نهایی */
wrap.appendChild(box)
box.append(audio, bar)
return { dom: wrap }
}
},
})