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.
285 lines (248 loc) • 14.7 kB
JSX
import { Image } from '@tiptap/extension-image';
const ResizeImageExtension = Image.extend({
name: 'resizeImage',
addAttributes() {
return {
...this.parent?.(),
style: {
default: 'width: 100%; height: auto; cursor: pointer;',
parseHTML: element => {
const width = element.getAttribute('width');
return width
? `width: ${width}px; height: auto; cursor: pointer;`
: element.style.cssText;
},
},
};
},
addNodeView() {
return ({ node, editor, getPos }) => {
const { view } = editor;
const { style } = node.attrs;
// ریشهی اصلی گره
const $wrapper = document.createElement('div');
$wrapper.classList.add('flex');
// کانتینری که قرار است عکس و کنترلها در آن باشند
const $container = document.createElement('div');
$container.classList.add('relative', 'cursor-pointer');
// عنصری که عکس را نگه میدارد
const $img = document.createElement('img');
// بهروزرسانی نود در State تیپتپ
const dispatchNodeView = (extraAttrs = {}) => {
if (typeof getPos === 'function') {
const newAttrs = {
...node.attrs,
style: $img.style.cssText, // استایلهای جدید را حتماً ذخیره کنید
...extraAttrs,
};
view.dispatch(view.state.tr.setNodeMarkup(getPos(), null, newAttrs));
}
};
// تابع برای ساخت SVG از Icons.jsx
const createSvgElement = (svgContent) => {
const div = document.createElement('div');
div.innerHTML = svgContent;
return div.firstElementChild;
};
// کنترل موقعیت (چپ، وسط، راست) و دکمهی alt
const paintPositionController = () => {
const $positionController = document.createElement('div');
$positionController.classList.add(
'absolute',
'top-0',
'left-1/2',
'w-[170px]',
'h-[28px]',
'z-[999]',
'py-4',
'px-2',
'bg-white',
'rounded',
'border-1',
'border-gray-400',
'cursor-pointer',
'transform',
'-translate-x-1/2',
'-translate-y-1/2',
'flex',
'justify-between',
'items-center'
);
const iconClasses = ['w-6', 'h-6', 'cursor-pointer', 'hover:bg-gray-200', 'p-0.5'];
// دکمه چپچین
const $leftController = createSvgElement(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-align-left"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 6l16 0" /><path d="M4 12l10 0" /><path d="M4 18l14 0" /></svg>
`);
$leftController.classList.add(...iconClasses);
$leftController.addEventListener('click', () => {
$img.style.margin = '0 auto 0 0';
dispatchNodeView();
});
// دکمه وسطچین
const $centerController = createSvgElement(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-align-center"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 6l16 0" /><path d="M8 12l8 0" /><path d="M6 18l12 0" /></svg>
`);
$centerController.classList.add(...iconClasses);
$centerController.addEventListener('click', () => {
$img.style.margin = '0 auto';
dispatchNodeView();
});
// دکمه راستچین
const $rightController = createSvgElement(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-align-right"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M4 6l16 0" /><path d="M10 12l10 0" /><path d="M6 18l14 0" /></svg>
`);
$rightController.classList.add(...iconClasses);
$rightController.addEventListener('click', () => {
$img.style.margin = '0 0 0 auto';
dispatchNodeView();
});
// دکمه تغییر alt (با یک آیکون ساده edit)
const $altController = createSvgElement(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-photo-ai"><path stroke="none" d="M0 0h24v24H0z" fill="none"/><path d="M15 8h.01" /><path d="M10 21h-4a3 3 0 0 1 -3 -3v-12a3 3 0 0 1 3 -3h12a3 3 0 0 1 3 3v5" /><path d="M3 16l5 -5c.928 -.893 2.072 -.893 3 0l1 1" /><path d="M14 21v-4a2 2 0 1 1 4 0v4" /><path d="M14 19h4" /><path d="M21 15v6" /></svg>
`);
$altController.classList.add(...iconClasses);
$altController.addEventListener('click', () => {
const currentAlt = $img.getAttribute('alt') || '';
const newAlt = prompt('متن جایگزین (alt) را وارد کنید:', currentAlt);
if (newAlt !== null) {
$img.setAttribute('alt', newAlt);
dispatchNodeView({ alt: newAlt });
}
});
// دکمه بردر (با امکان وارد کردن مقدار)
const $borderController = createSvgElement(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-photo-circle-plus">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M15 8h.01" />
<path d="M20.964 12.806a9 9 0 0 0 -8.964 -9.806a9 9 0 0 0 -9 9a9 9 0 0 0 9.397 8.991" />
<path d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l4 4" />
<path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0" />
<path d="M16 19.33h6" />
<path d="M19 16.33v6" />
</svg>
`);
$borderController.classList.add(...iconClasses);
$borderController.addEventListener('click', () => {
const currentBorderRadius = $img.style.borderRadius || '0';
const newBorderRadius = prompt('مقدار گردی گوشهها (به px یا %) را وارد کنید:', currentBorderRadius);
if (newBorderRadius !== null) {
// بررسی و اضافه کردن واحد اگر کاربر وارد نکرده باشه
const value = /^\d+$/.test(newBorderRadius) ? `${newBorderRadius}px` : newBorderRadius;
$img.style.borderRadius = value;
dispatchNodeView();
}
});
// دکمه حذف بردر
const $borderNoController = createSvgElement(`
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="icon icon-tabler icons-tabler-outline icon-tabler-photo-circle-minus">
<path stroke="none" d="M0 0h24v24H0z" fill="none"/>
<path d="M15 8h.01" />
<path d="M20.475 15.035a9 9 0 0 0 -8.475 -12.035a9 9 0 0 0 -9 9a9 9 0 0 0 9.525 8.985" />
<path d="M4 15l4 -4c.928 -.893 2.072 -.893 3 0l4 4" />
<path d="M14 14l1 -1c.928 -.893 2.072 -.893 3 0l2 2" />
<path d="M16 19h6" />
</svg>
`);
$borderNoController.classList.add(...iconClasses);
$borderNoController.addEventListener('click', () => {
$img.style.borderRadius = '0';
dispatchNodeView();
});
// افزودن دکمهها به کنترلر
$positionController.appendChild($altController);
$positionController.appendChild($rightController);
$positionController.appendChild($centerController);
$positionController.appendChild($leftController);
$positionController.appendChild($borderController);
$positionController.appendChild($borderNoController);
$container.appendChild($positionController);
};
// الحاق عناصر
$wrapper.appendChild($container);
$container.setAttribute('style', style);
$container.appendChild($img);
// انتقال اتربیوتهای نود (مثل src, alt و ...)
Object.entries(node.attrs).forEach(([key, value]) => {
if (value !== undefined && value !== null) {
$img.setAttribute(key, value);
}
});
// ذخیره عرض و ارتفاع اولیه برای حفظ نسبت
const originalWidth = $img.width;
const originalHeight = $img.height;
const aspectRatio = originalWidth / originalHeight;
// متغیرهای تغییر اندازه
let isResizing = false;
let startX = 0;
let startWidth = 0;
const onMouseMove = e => {
if (!isResizing) return;
const deltaX = e.clientX - startX;
const newWidth = startWidth - deltaX;
const newHeight = newWidth / aspectRatio;
$img.style.width = `${Math.max(newWidth, 10)}px`;
$img.style.height = `${Math.max(newHeight, 10)}px`;
$container.style.width = `${Math.max(newWidth, 10)}px`;
$container.style.height = `${Math.max(newHeight, 10)}px`;
};
const onMouseUp = () => {
if (isResizing) isResizing = false;
dispatchNodeView();
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
};
// کلیک روی عکس: نمایش ابزار موقعیتدهی و نقاط تغییر سایز
$container.addEventListener('click', () => {
paintPositionController();
// افزودن کلاسها و استایل مخصوص حالت انتخاب
$container.setAttribute(
'style',
`position: relative; border: 1.5px dashed #6C6C6C; ${style}`
);
// نقاط تغییر اندازه
const dotPosition = '-6px';
const dotsPosition = [
`top: ${dotPosition}; left: ${dotPosition}; cursor: nwse-resize;`,
`top: ${dotPosition}; right: ${dotPosition}; cursor: nesw-resize;`,
`bottom: ${dotPosition}; left: ${dotPosition}; cursor: nesw-resize;`,
`bottom: ${dotPosition}; right: ${dotPosition}; cursor: nwse-resize;`,
];
Array.from({ length: 1 }, (_, index) => {
const $dot = document.createElement('div');
$dot.classList.add(
'absolute',
'w-[12px]',
'h-[12px]',
'border',
'border-gray-600',
'bg-gray-200',
'rounded-full'
);
$dot.setAttribute(
'style',
`${dotsPosition[index]} border-width:1.5px;`
);
$dot.addEventListener('mousedown', e => {
e.preventDefault();
isResizing = true;
startX = e.clientX;
startWidth = $img.offsetWidth;
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
});
$container.appendChild($dot);
});
});
// اگر بیرون از کانتینر کلیک شد، ابزارهای کمکی حذف شوند
document.addEventListener('click', e => {
if (!$container.contains(e.target)) {
$container.setAttribute('style', `${style}`);
}
});
return {
dom: $wrapper,
};
};
},
});
export default ResizeImageExtension;