139 lines
4.3 KiB
TypeScript
139 lines
4.3 KiB
TypeScript
import { useState, forwardRef, useImperativeHandle } from 'react';
|
||
import { useEditor, EditorContent, Editor } from '@tiptap/react';
|
||
import StarterKit from '@tiptap/starter-kit';
|
||
import TextAlign from '@tiptap/extension-text-align';
|
||
import Image from '@tiptap/extension-image';
|
||
import Highlight from '@tiptap/extension-highlight';
|
||
import Blockquote from "@tiptap/extension-blockquote";
|
||
|
||
export interface ContentEditorRef {
|
||
setContent: (content: string) => void;
|
||
getEditor: () => Editor | null;
|
||
}
|
||
|
||
const ContentEditor = forwardRef<ContentEditorRef>((_, ref) => {
|
||
const [selectedImage, setSelectedImage] = useState<string | null>(null);
|
||
|
||
const ResizableImage = Image.extend({
|
||
addAttributes() {
|
||
return {
|
||
...this.parent?.(),
|
||
src: {
|
||
default: null,
|
||
},
|
||
alt: {
|
||
default: null,
|
||
},
|
||
title: {
|
||
default: null,
|
||
},
|
||
class: {
|
||
default: 'max-w-full h-auto', // Добавлено для адаптивности
|
||
},
|
||
width: {
|
||
default: 'auto',
|
||
parseHTML: (element) => element.getAttribute('width') || 'auto',
|
||
renderHTML: (attributes) => ({
|
||
width: attributes.width,
|
||
}),
|
||
},
|
||
height: {
|
||
default: 'auto',
|
||
parseHTML: (element) => element.getAttribute('height') || 'auto',
|
||
renderHTML: (attributes) => ({
|
||
height: attributes.height,
|
||
}),
|
||
},
|
||
};
|
||
},
|
||
});
|
||
|
||
const editor = useEditor({
|
||
extensions: [
|
||
StarterKit.configure({
|
||
blockquote: false, // Отключаем дефолтный
|
||
}),
|
||
Blockquote.configure({
|
||
HTMLAttributes: {
|
||
class: 'border-l-4 border-gray-300 pl-4 italic',
|
||
},
|
||
}),
|
||
TextAlign.configure({
|
||
types: ['heading', 'paragraph'],
|
||
}),
|
||
ResizableImage,
|
||
Highlight,
|
||
],
|
||
content: '',
|
||
onUpdate: ({ editor }) => {
|
||
const pos = editor.state.selection.$anchor.pos;
|
||
const resolvedPos = editor.state.doc.resolve(pos);
|
||
const parentNode = resolvedPos.parent;
|
||
|
||
if (parentNode.type.name === 'image') {
|
||
setSelectedImage(parentNode.attrs.src); // Сохраняем URL изображения
|
||
} else {
|
||
setSelectedImage(null); // Очищаем выбор, если это не изображение
|
||
}
|
||
},
|
||
editorProps: {
|
||
attributes: {
|
||
class: 'prose prose-lg focus:outline-none min-h-[300px] max-w-none',
|
||
},
|
||
},
|
||
});
|
||
|
||
// Позволяем родительскому компоненту использовать методы редактора
|
||
useImperativeHandle(ref, () => ({
|
||
setContent: (content: string) => {
|
||
editor?.commands.setContent(content);
|
||
},
|
||
getEditor: () => editor,
|
||
}));
|
||
|
||
// Функция изменения размера изображения
|
||
const updateImageSize = (delta: number) => {
|
||
if (selectedImage) {
|
||
const attrs = editor?.getAttributes('image');
|
||
const newWidth = Math.max((parseInt(attrs?.width) || 100) + delta, 50);
|
||
editor?.chain().focus().updateAttributes('image', { width: `${newWidth}px` }).run();
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="relative">
|
||
<EditorContent editor={editor} />
|
||
|
||
{selectedImage && (
|
||
<div
|
||
className="absolute z-10 flex space-x-2 bg-white border p-1 rounded shadow-lg"
|
||
style={{
|
||
top: editor?.view?.dom
|
||
? `${editor.view.dom.getBoundingClientRect().top + window.scrollY + 50}px`
|
||
: '0px',
|
||
left: editor?.view?.dom
|
||
? `${editor.view.dom.getBoundingClientRect().left + window.scrollX + 200}px`
|
||
: '0px',
|
||
}}
|
||
>
|
||
<button
|
||
onClick={() => updateImageSize(-20)}
|
||
className="px-2 py-1 bg-gray-200 rounded hover:bg-gray-300"
|
||
>
|
||
Уменьшить
|
||
</button>
|
||
<button
|
||
onClick={() => updateImageSize(20)}
|
||
className="px-2 py-1 bg-gray-200 rounded hover:bg-gray-300"
|
||
>
|
||
Увеличить
|
||
</button>
|
||
</div>
|
||
)}
|
||
</div>
|
||
);
|
||
});
|
||
|
||
ContentEditor.displayName = 'ContentEditor';
|
||
export default ContentEditor;
|