UNPKG

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
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 } } }, })