import React from 'react'; import {mergeAttributes, Node, NodeViewProps } from '@tiptap/core'; import {NodeViewWrapper, ReactNodeViewRenderer} from '@tiptap/react'; import {Trash2, ZoomIn, ZoomOut} from 'lucide-react'; import {Node as ProseMirrorNode, DOMOutputSpec} from 'prosemirror-model'; // Определяем тип для атрибутов узла customImage interface CustomImageAttributes { src: string | undefined; alt: string | undefined; title: string | undefined; scale: number; caption: string; // Добавляем атрибут caption } const ImageNodeView: React.FC = ({ node, updateAttributes, editor, getPos, selected, deleteNode }) => { // Утверждаем, что node имеет нужный тип const typedNode = node as ProseMirrorNode & { attrs: CustomImageAttributes }; // Проверяем, что узел корректен if (node.type.name !== 'customImage') { console.error('Неподдерживаемый тип узла:', node.type.name); return Ошибка: неподдерживаемый тип узла; } // Проверяем, что src существует if (!typedNode.attrs.src) { console.error('Изображение не загружено, src отсутствует:', typedNode.attrs); return Ошибка: изображение не загружено; } // Логируем атрибуты узла при загрузке //console.log('Атрибуты узла customImage при загрузке:', typedNode.attrs); const handleZoomIn = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); try { if (!editor || editor.isDestroyed) { console.error('Редактор уничтожен или недоступен'); return; } const pos = getPos(); const currentNode = editor.state.doc.nodeAt(pos); if (!currentNode || currentNode.type.name !== 'customImage') { console.error('Узел больше не существует или изменён:', { pos, currentNode }); return; } let currentScale = typedNode.attrs.scale; if (currentScale <= 0 || isNaN(currentScale)) { console.warn('Некорректное значение scale, устанавливаем значение по умолчанию:', { currentScale }); currentScale = 1; updateAttributes({ scale: 1 }); } const newScale = Math.min(currentScale + 0.1, 2); const roundedScale = Number(newScale.toFixed(2)); updateAttributes({ scale: roundedScale }); console.log('Масштаб увеличен через updateAttributes:', { currentScale, newScale: roundedScale }); } catch (error) { console.error('Ошибка при увеличении масштаба:', error); } }; const handleZoomOut = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); try { if (!editor || editor.isDestroyed) { console.error('Редактор уничтожен или недоступен'); return; } const pos = getPos(); const currentNode = editor.state.doc.nodeAt(pos); if (!currentNode || currentNode.type.name !== 'customImage') { console.error('Узел больше не существует или изменён:', { pos, currentNode }); return; } let currentScale = typedNode.attrs.scale; if (currentScale <= 0 || isNaN(currentScale)) { console.warn('Некорректное значение scale, устанавливаем значение по умолчанию:', { currentScale }); currentScale = 1; updateAttributes({ scale: 1 }); } const newScale = Math.max(currentScale - 0.1, 0.5); const roundedScale = Number(newScale.toFixed(2)); updateAttributes({ scale: roundedScale }); console.log('Масштаб уменьшен через updateAttributes:', { currentScale, newScale: roundedScale }); } catch (error) { console.error('Ошибка при уменьшении масштаба:', error); } }; const handleDelete = (e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); try { deleteNode(); } catch (error) { console.error('Ошибка при удалении изображения:', error); } }; const scale = typedNode.attrs.scale || 1; const caption = typedNode.attrs.caption || ''; const isEditable = editor.isEditable; // Проверяем, находится ли редактор в режиме редактирования return ( // {/* Оборачиваем изображение и панель в контейнер, который масштабируется */}
{typedNode.attrs.alt} {selected && isEditable && ( // Показываем панель только в режиме редактирования
Масштаб: {scale}
)}
{caption && (

{caption}

)}
); }; export const CustomImage = Node.create({ name: 'customImage', group: 'block', inline: false, selectable: true, draggable: true, atom: true, addAttributes() { return { src: { default: undefined, }, alt: { default: undefined, }, title: { default: undefined, }, scale: { default: 1, parseHTML: (element) => { const scale = Number(element.getAttribute('scale')); return isNaN(scale) || scale <= 0 ? 1 : Math.max(0.5, Math.min(2, Number(scale.toFixed(2)))); }, renderHTML: (attributes) => { const scale = Number(attributes.scale); const renderedScale = isNaN(scale) || scale <= 0 ? 1 : Math.max(0.5, Math.min(2, Number(scale.toFixed(2)))); return { scale: renderedScale }; }, }, caption: { // Добавляем атрибут caption default: '', parseHTML: (element) => element.getAttribute('data-caption') || '', renderHTML: (attributes) => { if (!attributes.caption) { return {}; } return { 'data-caption': attributes.caption, }; }, }, }; }, parseHTML() { return [ { tag: 'div.image-container', getAttrs: (element: HTMLElement) => { const img = element.querySelector('img'); const caption = element.querySelector('p.image-caption')?.textContent || ''; return { src: img?.getAttribute('src'), alt: img?.getAttribute('alt'), title: img?.getAttribute('title'), scale: img ? Number(img.getAttribute('scale')) || 1 : 1, caption, }; }, }, { tag: 'img[src]', getAttrs: (element) => { const scale = element.getAttribute('scale'); const caption = element.getAttribute('data-caption') || ''; return { src: element.getAttribute('src'), alt: element.getAttribute('alt'), title: element.getAttribute('title'), scale: scale ? Number(scale) : 1, caption, }; }, }, ]; }, renderHTML({ HTMLAttributes }): DOMOutputSpec { const { scale, caption, ...rest } = HTMLAttributes; const imgElement: DOMOutputSpec = [ 'img', mergeAttributes(rest, { scale: scale, 'data-caption': caption, style: `transform: scale(${scale})`, }), ]; const children: DOMOutputSpec[] = [imgElement]; if (caption) { children.push(['p', { class: 'image-caption mt-2 text-sm text-gray-600 italic text-center' }, caption]); } return ['div', { class: 'image-container flex flex-col items-center my-4' }, ...children]; }, addNodeView() { return ReactNodeViewRenderer(ImageNodeView); }, });