Commit 88e590d1 authored by Яков's avatar Яков
Browse files

update fix issue

parent 5b8e6efe
{ {
"name": "react-ag-qeditor", "name": "react-ag-qeditor",
"version": "1.1.59", "version": "1.1.60",
"description": "WYSIWYG html editor", "description": "WYSIWYG html editor",
"author": "atma", "author": "atma",
"license": "MIT", "license": "MIT",
......
...@@ -137,6 +137,13 @@ export const DragAndDrop = Extension.create({ ...@@ -137,6 +137,13 @@ export const DragAndDrop = Extension.create({
if (!result?.file_path) throw new Error('Invalid response from server'); if (!result?.file_path) throw new Error('Invalid response from server');
// Регистрируем файл так же, как это делает Uploader.js
axios.post('/api/web/ru/set-file-data/', {
...result,
pathname: window.location.pathname,
slug: 'redactor'
}, { withCredentials: true }).catch(() => {});
const id = `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const id = `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
if (nodeType === 'image') { if (nodeType === 'image') {
...@@ -272,6 +279,12 @@ export const DragAndDrop = Extension.create({ ...@@ -272,6 +279,12 @@ export const DragAndDrop = Extension.create({
const ext = blob.type.split('/')[1]?.split('+')[0] || 'png'; const ext = blob.type.split('/')[1]?.split('+')[0] || 'png';
const result = await uploadBlob(blob, `pasted-image.${ext}`); const result = await uploadBlob(blob, `pasted-image.${ext}`);
if (!result?.file_path) return null; if (!result?.file_path) return null;
// Регистрируем файл
axios.post('/api/web/ru/set-file-data/', {
...result,
pathname: window.location.pathname,
slug: 'redactor'
}, { withCredentials: true }).catch(() => {});
return { filePath: result.file_path, width, height, style }; return { filePath: result.file_path, width, height, style };
} catch (e) { } catch (e) {
console.warn('[DragAndDrop] не удалось загрузить:', src?.slice(0, 80), e); console.warn('[DragAndDrop] не удалось загрузить:', src?.slice(0, 80), e);
......
import { Node, mergeAttributes, ReactNodeViewRenderer } from '@tiptap/react' import { Node, ReactNodeViewRenderer } from '@tiptap/react'
import React, { Fragment, useEffect, useRef, useState } from 'react' import React, { Fragment, useEffect, useRef, useState } from 'react'
import { NodeViewWrapper } from '@tiptap/react' import { NodeViewWrapper } from '@tiptap/react'
import { Button, Modal, Popconfirm, Input, Typography } from 'antd' import { Button, Modal, Popconfirm, Input, Typography } from 'antd'
import { FontSizeOutlined } from "@ant-design/icons";
const {Text} = Typography;
const { Text } = Typography;
const MIN_WIDTH = 60; const MIN_WIDTH = 60;
const BORDER_COLOR = '#0096fd'; const BORDER_COLOR = '#0096fd';
const ALIGN_OPTIONS = ['left', 'center', 'right', 'text']; const ALIGN_OPTIONS = ['left', 'center', 'right'];
const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected }) => { const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected }) => {
const [modalVisible, setModalVisible] = useState(false) const [modalVisible, setModalVisible] = useState(false)
...@@ -18,12 +17,11 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -18,12 +17,11 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
const [newPointTitle, setNewPointTitle] = useState('') const [newPointTitle, setNewPointTitle] = useState('')
const [editingIdx, setEditingIdx] = useState(null) const [editingIdx, setEditingIdx] = useState(null)
const [editingText, setEditingText] = useState('') const [editingText, setEditingText] = useState('')
const [isResizing, setIsResizing] = useState(false);
const [showAlignMenu, setShowAlignMenu] = useState(false);
const [editingTitle, setEditingTitle] = useState('') const [editingTitle, setEditingTitle] = useState('')
const imgRef = useRef(null); const [isResizing, setIsResizing] = useState(false)
const isInitialized = useRef(false); const imgRef = useRef(null)
const wrapperRef = useRef(null); const isInitialized = useRef(false)
const wrapperRef = useRef(null)
// Обработка кликов вне изображения // Обработка кликов вне изображения
useEffect(() => { useEffect(() => {
...@@ -31,37 +29,31 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -31,37 +29,31 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
if (wrapperRef.current && !wrapperRef.current.contains(event.target) && selected) { if (wrapperRef.current && !wrapperRef.current.contains(event.target) && selected) {
try { try {
const pos = getPos?.() const pos = getPos?.()
if (typeof pos === 'number') { if (typeof pos === 'number') editor.commands.setNodeSelection(pos)
editor.commands.setNodeSelection(pos)
}
} catch (e) { } catch (e) {
console.warn('getPos() failed:', e) console.warn('getPos() failed:', e)
} }
// editor.commands.setNodeSelection(getPos());
} }
}; };
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside);
}, [selected, editor, getPos]); }, [selected, editor, getPos]);
// Загрузка и инициализация изображения // Инициализация размеров изображения
useEffect(() => { useEffect(() => {
if (!imgRef.current || isInitialized.current) return; if (!imgRef.current || isInitialized.current) return;
const initImageSize = () => { const initImageSize = () => {
try { try {
// Если размеры уже заданы в атрибутах - используем их сразу
if (node.attrs.width && node.attrs.height) { if (node.attrs.width && node.attrs.height) {
isInitialized.current = true; isInitialized.current = true;
return; return;
} }
const { width: editorWidth } = getEditorDimensions(); const { width: editorWidth } = getEditorDimensions();
const naturalWidth = imgRef.current.naturalWidth; const naturalWidth = imgRef.current.naturalWidth;
const naturalHeight = imgRef.current.naturalHeight; const naturalHeight = imgRef.current.naturalHeight;
if (naturalWidth <= 0 || naturalHeight <= 0) { if (naturalWidth <= 0 || naturalHeight <= 0) {
console.warn('Image has invalid natural dimensions, retrying...');
setTimeout(initImageSize, 100); setTimeout(initImageSize, 100);
return; return;
} }
...@@ -87,7 +79,6 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -87,7 +79,6 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
}; };
const handleLoad = () => { const handleLoad = () => {
// Если размеры уже заданы в атрибутах, пропускаем инициализацию
if (node.attrs.width && node.attrs.height) { if (node.attrs.width && node.attrs.height) {
isInitialized.current = true; isInitialized.current = true;
return; return;
...@@ -108,46 +99,19 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -108,46 +99,19 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
}; };
}, [node.attrs.width, node.attrs.height, node.attrs['data-node-id']]); }, [node.attrs.width, node.attrs.height, node.attrs['data-node-id']]);
// Добавляем прозрачный нулевой пробел после изображения // ─── Точки ───────────────────────────────────────────────────────────────
useEffect(() => {
if (!editor || !getPos) return;
let pos;
try {
pos = getPos();
if (typeof pos !== 'number') return;
} catch (e) {
console.warn('getPos() failed:', e);
return;
}
const doc = editor.state.doc;
if (doc.nodeSize > pos && doc.nodeAt(pos)?.textContent !== '\u200B') {
editor.commands.insertContentAt(pos + 1, {
type: 'text',
text: '\u200B'
});
}
}, [editor, getPos]);
const addPoint = (e) => { const addPoint = (e) => {
const rect = e.target.getBoundingClientRect() const rect = e.target.getBoundingClientRect()
const x = ((e.clientX - rect.left) / rect.width) * 100 const x = ((e.clientX - rect.left) / rect.width) * 100
const y = ((e.clientY - rect.top) / rect.height) * 100 const y = ((e.clientY - rect.top) / rect.height) * 100
setNewPoint({ x, y }) setNewPoint({ x, y })
setNewPointText('') setNewPointText('')
setNewPointTitle('') setNewPointTitle('')
} }
const confirmAddPoint = () => { const confirmAddPoint = () => {
const newPoints = [...points, { const newPoints = [...points, { ...newPoint, text: newPointText, title: newPointTitle }]
...newPoint,
text: newPointText,
title: newPointTitle,
}]
setPoints(newPoints) setPoints(newPoints)
updateAttributes({ points: newPoints }) updateAttributes({ points: newPoints })
setNewPoint(null) setNewPoint(null)
...@@ -155,7 +119,6 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -155,7 +119,6 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
setNewPointTitle('') setNewPointTitle('')
} }
const cancelAddPoint = () => { const cancelAddPoint = () => {
setNewPoint(null) setNewPoint(null)
setNewPointText('') setNewPointText('')
...@@ -167,7 +130,8 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -167,7 +130,8 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
updateAttributes({ points: newPoints }) updateAttributes({ points: newPoints })
} }
// Обработка ресайза изображения // ─── Ресайз ───────────────────────────────────────────────────────────────
const handleResizeStart = (direction) => (e) => { const handleResizeStart = (direction) => (e) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
...@@ -175,13 +139,10 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -175,13 +139,10 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
setIsResizing(true); setIsResizing(true);
try { try {
const pos = getPos?.() const pos = getPos?.()
if (typeof pos === 'number') { if (typeof pos === 'number') editor.commands.setNodeSelection(pos)
editor.commands.setNodeSelection(pos)
}
} catch (e) { } catch (e) {
console.warn('getPos() failed:', e) console.warn('getPos() failed:', e)
} }
// editor.commands.setNodeSelection(getPos());
const startWidth = node.attrs.width || imgRef.current.naturalWidth; const startWidth = node.attrs.width || imgRef.current.naturalWidth;
const startHeight = node.attrs.height || imgRef.current.naturalHeight; const startHeight = node.attrs.height || imgRef.current.naturalHeight;
...@@ -193,10 +154,8 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -193,10 +154,8 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
const onMouseMove = (e) => { const onMouseMove = (e) => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const maxWidth = node.attrs.align === 'center' ? initialEditorWidth : initialAvailableSpace; const maxWidth = node.attrs.align === 'center' ? initialEditorWidth : initialAvailableSpace;
const deltaX = e.clientX - startX; const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY; const deltaY = e.clientY - startY;
let newWidth, newHeight; let newWidth, newHeight;
if (node.attrs.align === 'center') { if (node.attrs.align === 'center') {
...@@ -207,27 +166,18 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -207,27 +166,18 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
newHeight = Math.round(newWidth / aspectRatio); newHeight = Math.round(newWidth / aspectRatio);
} else { } else {
const scale = direction.includes('e') ? 1 : -1; const scale = direction.includes('e') ? 1 : -1;
newWidth = Math.min( newWidth = Math.min(Math.max(startWidth + deltaX * scale, MIN_WIDTH), maxWidth);
Math.max(startWidth + deltaX * scale, MIN_WIDTH),
maxWidth
);
newHeight = Math.round(newWidth / aspectRatio); newHeight = Math.round(newWidth / aspectRatio);
} }
} else { } else {
if (direction.includes('e') || direction.includes('w')) { if (direction.includes('e') || direction.includes('w')) {
const scale = direction.includes('e') ? 1 : -1; const scale = direction.includes('e') ? 1 : -1;
newWidth = Math.min( newWidth = Math.min(Math.max(startWidth + deltaX * scale, MIN_WIDTH), maxWidth);
Math.max(startWidth + deltaX * scale, MIN_WIDTH),
maxWidth
);
newHeight = Math.round(newWidth / aspectRatio); newHeight = Math.round(newWidth / aspectRatio);
} else { } else {
const scale = direction.includes('s') ? 1 : -1; const scale = direction.includes('s') ? 1 : -1;
newHeight = Math.max(startHeight + deltaY * scale, MIN_WIDTH); newHeight = Math.max(startHeight + deltaY * scale, MIN_WIDTH);
newWidth = Math.min( newWidth = Math.min(Math.round(newHeight * aspectRatio), maxWidth);
Math.round(newHeight * aspectRatio),
maxWidth
);
newHeight = Math.round(newWidth / aspectRatio); newHeight = Math.round(newWidth / aspectRatio);
} }
} }
...@@ -236,20 +186,16 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -236,20 +186,16 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
}); });
}; };
const onMouseUp = () => { const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp); window.removeEventListener('mouseup', onMouseUp);
setIsResizing(false); setIsResizing(false);
try { try {
const pos = getPos?.() const pos = getPos?.()
if (typeof pos === 'number') { if (typeof pos === 'number') editor.commands.setNodeSelection(pos)
editor.commands.setNodeSelection(pos)
}
} catch (e) { } catch (e) {
console.warn('getPos() failed:', e) console.warn('getPos() failed:', e)
} }
// editor.commands.setNodeSelection(getPos());
editor.commands.focus(); editor.commands.focus();
}; };
...@@ -257,37 +203,16 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -257,37 +203,16 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
window.addEventListener('mouseup', onMouseUp); window.addEventListener('mouseup', onMouseUp);
}; };
const safeUpdateAttributes = (newAttrs) => { // ─── Helpers ──────────────────────────────────────────────────────────────
const { width: editorWidth, availableSpace } = getEditorDimensions();
let { width, height, align } = { ...node.attrs, ...newAttrs };
const newAlign = newAttrs.align || align;
// При изменении выравнивания проверяем доступное пространство
if (newAlign && newAlign !== align) {
const maxWidth = availableSpace;
if (width > maxWidth) {
const ratio = maxWidth / width;
width = maxWidth;
height = Math.round(height * ratio);
}
} else {
// Для обычного обновления размеров
const maxWidth = availableSpace;
if (width > maxWidth) {
const ratio = maxWidth / width;
width = maxWidth;
height = Math.round(height * ratio);
}
}
// Проверяем минимальный размер const safeUpdateAttributes = (patch) => {
if (width < MIN_WIDTH) { let { width, height, align } = { ...node.attrs, ...patch }
const ratio = MIN_WIDTH / width; const minW = 80, maxW = 1600, minH = 40, maxH = 2000
width = MIN_WIDTH; if (width != null) width = Math.min(maxW, Math.max(minW, Number(width) || 0))
height = Math.round(height * ratio); if (height != null) height = Math.min(maxH, Math.max(minH, Number(height) || 0))
} const allowedAlign = new Set(['left', 'center', 'right'])
if (align && !allowedAlign.has(align)) align = node.attrs.align || 'center'
updateAttributes({ width, height, ...newAttrs }); updateAttributes({ ...node.attrs, ...patch, width, height, align })
}; };
const getEditorDimensions = () => { const getEditorDimensions = () => {
...@@ -301,12 +226,9 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -301,12 +226,9 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
const availableEditorWidth = fullEditorWidth - paddingLeft - paddingRight; const availableEditorWidth = fullEditorWidth - paddingLeft - paddingRight;
let container; let container;
// при center — всегда редактор
if (node.attrs.align === 'center') { if (node.attrs.align === 'center') {
container = editorContent; container = editorContent;
} else { } else {
// при других выравниваниях — ближайший блок
container = imgRef.current?.closest('li, blockquote, td, p, div') || editorContent; container = imgRef.current?.closest('li, blockquote, td, p, div') || editorContent;
} }
...@@ -315,73 +237,69 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -315,73 +237,69 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
const containerPaddingRight = parseFloat(containerStyles.paddingRight) || 0; const containerPaddingRight = parseFloat(containerStyles.paddingRight) || 0;
const containerWidth = container.clientWidth - containerPaddingLeft - containerPaddingRight; const containerWidth = container.clientWidth - containerPaddingLeft - containerPaddingRight;
return { return { width: containerWidth, availableSpace: availableEditorWidth };
width: containerWidth, // текущая ширина контейнера
availableSpace: availableEditorWidth // фиксированная доступная ширина
};
}; };
// Изменение выравнивания с автоматическим масштабированием
const handleAlign = (align) => { const handleAlign = (align) => {
safeUpdateAttributes({ align }); // первый вызов safeUpdateAttributes({ align });
setTimeout(() => { setTimeout(() => {
safeUpdateAttributes({ align }); // повторный вызов с обновлёнными размерами safeUpdateAttributes({ align });
try {
const pos = getPos?.()
if (typeof pos === 'number') editor.commands.setNodeSelection(pos)
} catch {}
}, 50); }, 50);
setShowAlignMenu(false);
editor.commands.focus();
}; };
const pointIcon = ( // ─── Стили (идентично Image.jsx) ─────────────────────────────────────────
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 11.2969C5.48281 11.2969 5.875 11.6891 5.875 12.1719C5.87476 12.6545 5.48267 13.0469 5 13.0469C4.51742 13.0468 4.12524 12.6544 4.125 12.1719C4.125 11.6891 4.51727 11.297 5 11.2969ZM5 0.953125C6.1342 0.953125 7.20454 1.36506 8.01074 2.11328C8.40761 2.48046 8.71875 2.91056 8.9375 3.38867C9.16558 3.887 9.2812 4.41809 9.28125 4.96484C9.28125 5.43031 9.19735 5.88841 9.03027 6.32422C8.86938 6.74441 8.6362 7.13211 8.33789 7.48047C7.7457 8.16797 6.91738 8.66094 6.00488 8.86719C5.76899 8.92034 5.59863 9.13558 5.59863 9.38086V9.87109C5.59863 9.9679 5.52061 10.0468 5.42383 10.0469H4.58008C4.4832 10.0469 4.40527 9.96797 4.40527 9.87109V9.38086C4.40582 8.99176 4.53698 8.6138 4.77832 8.30859C5.02044 8.00253 5.3627 7.78908 5.74219 7.70312C6.40618 7.55314 7.00665 7.19836 7.43164 6.70312C7.86133 6.20312 8.08789 5.60234 8.08789 4.96484C8.08769 3.41031 6.703 2.14648 5 2.14648C3.29709 2.14658 1.91329 3.41037 1.91309 4.96484V5.38672C1.91309 5.48359 1.83418 5.5625 1.7373 5.5625H0.893555C0.796854 5.5623 0.71875 5.48347 0.71875 5.38672V4.96484C0.718799 4.41813 0.834455 3.88854 1.0625 3.38867C1.28123 2.90903 1.59244 2.48103 1.98926 2.1123C2.79546 1.36547 3.86569 0.95317 5 0.953125Z" fill="white"/>
</svg>
)
const getWrapperStyle = () => { const getOuterStyle = () => {
const baseStyle = { const { align, wrap, width } = node.attrs;
display: 'inline-block', const w = width ? `${width}px` : 'auto';
lineHeight: 0, const sharedMargin = { marginTop: '0.5rem', marginBottom: '0.5rem' };
position: 'relative',
outline: (selected || isResizing) ? `1px dashed ${BORDER_COLOR}` : 'none',
verticalAlign: 'top',
margin: '0.5rem 0',
};
if (node.attrs.align === 'center') { if (align === 'center') {
return { return {
...baseStyle, ...sharedMargin, lineHeight: 0,
display: 'block', display: 'inline-block', float: 'left', clear: 'both',
marginLeft: 'auto', width: '100%', textAlign: 'center',
marginRight: 'auto', };
width: node.attrs.width ? `${node.attrs.width}px` : 'fit-content', }
maxWidth: '100%', if (!wrap) {
textAlign: 'center' return {
...sharedMargin, lineHeight: 0,
display: 'inline-block', float: 'left', clear: 'both', width: '100%',
...(align === 'right' ? { textAlign: 'right' } : { textAlign: 'left' }),
}; };
} }
return { return {
...baseStyle, ...sharedMargin, lineHeight: 0,
...(node.attrs.align === 'left' && { display: 'inline-block',
float: 'left', float: align === 'left' ? 'left' : 'right',
marginRight: '1rem', ...(align === 'left' ? { marginRight: '1rem' } : { marginLeft: '1rem' }),
width: node.attrs.width ? `${node.attrs.width}px` : 'auto', width: w, maxWidth: '100%',
}),
...(node.attrs.align === 'right' && {
float: 'right',
marginLeft: '1rem',
width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
}),
...(node.attrs.align === 'text' && {
display: 'inline-block',
float: 'none',
margin: '0 0.2rem',
verticalAlign: 'middle',
width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
}),
}; };
}; };
const getInnerStyle = () => {
const { align, width } = node.attrs;
const w = width ? `${width}px` : 'auto';
const base = {
position: 'relative',
display: 'inline-block',
verticalAlign: 'top',
lineHeight: 0,
outline: (selected || isResizing) ? `1px dashed ${BORDER_COLOR}` : 'none',
width: w,
maxWidth: '100%',
};
if (align === 'center') {
return { ...base, display: 'block', marginLeft: 'auto', marginRight: 'auto',
width: width ? `${width}px` : 'fit-content' };
}
return base;
};
const getImageStyle = () => ({ const getImageStyle = () => ({
width: node.attrs.width ? `${node.attrs.width}px` : 'auto', width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
height: 'auto', height: 'auto',
...@@ -390,21 +308,29 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -390,21 +308,29 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
cursor: 'default', cursor: 'default',
userSelect: 'none', userSelect: 'none',
margin: node.attrs.align === 'center' ? '0 auto' : '0', margin: node.attrs.align === 'center' ? '0 auto' : '0',
verticalAlign: node.attrs.align === 'text' ? 'middle' : 'top',
objectFit: 'contain' objectFit: 'contain'
}); });
// ─── Иконка точки ─────────────────────────────────────────────────────────
const pointIcon = (
<svg width="10" height="14" viewBox="0 0 10 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M5 11.2969C5.48281 11.2969 5.875 11.6891 5.875 12.1719C5.87476 12.6545 5.48267 13.0469 5 13.0469C4.51742 13.0468 4.12524 12.6544 4.125 12.1719C4.125 11.6891 4.51727 11.297 5 11.2969ZM5 0.953125C6.1342 0.953125 7.20454 1.36506 8.01074 2.11328C8.40761 2.48046 8.71875 2.91056 8.9375 3.38867C9.16558 3.887 9.2812 4.41809 9.28125 4.96484C9.28125 5.43031 9.19735 5.88841 9.03027 6.32422C8.86938 6.74441 8.6362 7.13211 8.33789 7.48047C7.7457 8.16797 6.91738 8.66094 6.00488 8.86719C5.76899 8.92034 5.59863 9.13558 5.59863 9.38086V9.87109C5.59863 9.9679 5.52061 10.0468 5.42383 10.0469H4.58008C4.4832 10.0469 4.40527 9.96797 4.40527 9.87109V9.38086C4.40582 8.99176 4.53698 8.6138 4.77832 8.30859C5.02044 8.00253 5.3627 7.78908 5.74219 7.70312C6.40618 7.55314 7.00665 7.19836 7.43164 6.70312C7.86133 6.20312 8.08789 5.60234 8.08789 4.96484C8.08769 3.41031 6.703 2.14648 5 2.14648C3.29709 2.14658 1.91329 3.41037 1.91309 4.96484V5.38672C1.91309 5.48359 1.83418 5.5625 1.7373 5.5625H0.893555C0.796854 5.5623 0.71875 5.48347 0.71875 5.38672V4.96484C0.718799 4.41813 0.834455 3.88854 1.0625 3.38867C1.28123 2.90903 1.59244 2.48103 1.98926 2.1123C2.79546 1.36547 3.86569 0.95317 5 0.953125Z" fill="white"/>
</svg>
)
// ─── Render ───────────────────────────────────────────────────────────────
return ( return (
<NodeViewWrapper ref={wrapperRef} as="div" className="interactive-image-wrapper" contentEditable={false}> <NodeViewWrapper as="div" style={getOuterStyle()} contentEditable={false} className="interactive-image-wrapper">
<div <div
style={getWrapperStyle()} ref={wrapperRef}
style={getInnerStyle()}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
try { try {
const pos = getPos?.(); const pos = getPos?.();
if (typeof pos === 'number') { if (typeof pos === 'number') editor.commands.setNodeSelection(pos);
editor.commands.setNodeSelection(pos);
}
} catch {} } catch {}
}} }}
> >
...@@ -418,7 +344,6 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -418,7 +344,6 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
const { width: editorWidth } = getEditorDimensions(); const { width: editorWidth } = getEditorDimensions();
const naturalWidth = imgRef.current.naturalWidth; const naturalWidth = imgRef.current.naturalWidth;
const naturalHeight = imgRef.current.naturalHeight; const naturalHeight = imgRef.current.naturalHeight;
safeUpdateAttributes({ safeUpdateAttributes({
width: naturalWidth, width: naturalWidth,
height: naturalHeight, height: naturalHeight,
...@@ -428,14 +353,18 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -428,14 +353,18 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
} }
}} }}
/> />
{/* Кнопка редактирования точек */}
<Button <Button
size="default" size="default"
type="primary" type="primary"
onClick={() => setModalVisible(true)} onClick={(e) => { e.stopPropagation(); setModalVisible(true); }}
style={{ position: 'absolute', top: '4px', right: '30px', zIndex: 10 }} style={{ position: 'absolute', top: 4, right: 30, zIndex: 10 }}
> >
Редактировать Редактировать
</Button> </Button>
{/* Маркеры точек */}
{points.map((point, idx) => ( {points.map((point, idx) => (
<Button <Button
key={idx} key={idx}
...@@ -445,58 +374,43 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -445,58 +374,43 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
position: 'absolute', position: 'absolute',
top: `${point.y}%`, top: `${point.y}%`,
left: `${point.x}%`, left: `${point.x}%`,
width: 24, width: 24, height: 24,
height: 24, borderRadius: '50%', padding: 0,
borderRadius: '50%', display: 'flex', alignItems: 'center', justifyContent: 'center',
padding: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
zIndex: 5, zIndex: 5,
backgroundColor: '#1677ff', backgroundColor: '#1677ff', border: 'none',
border: 'none', pointerEvents: 'none',
pointerEvents: 'none', // чтобы не блокировала выбор или драг
}} }}
title={point.title || point.text} title={point.title || point.text}
> >
{pointIcon} {pointIcon}
</Button> </Button>
))} ))}
{/* Кнопка удаления */}
{selected && ( {selected && (
<Button <Button
type="text" type="text"
danger danger
size="small" size="small"
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation();
const pos = getPos?.();
const pos = getPos?.()
if (typeof pos === 'number') { if (typeof pos === 'number') {
editor.view.dispatch( editor.view.dispatch(editor.view.state.tr.delete(pos, pos + node.nodeSize))
editor.view.state.tr.delete(pos, pos + node.nodeSize)
)
} }
}} }}
style={{ style={{
position: 'absolute', position: 'absolute', top: 4, right: 4, zIndex: 30,
top: 4, backgroundColor: 'white', border: '1px solid #d9d9d9',
right: 4, borderRadius: '50%', width: 20, height: 20,
zIndex: 30, fontSize: 12, lineHeight: 1, padding: '0px 0px 2px 0px', cursor: 'pointer'
backgroundColor: 'white',
border: '1px solid #d9d9d9',
borderRadius: '50%',
width: 20,
height: 20,
fontSize: 12,
lineHeight: 1,
padding: '0px 0px 2px 0px',
cursor: 'pointer'
}} }}
> >×</Button>
×
</Button>
)} )}
{/* Ручки ресайза + тулбар выравнивания */}
{(selected || isResizing) && ( {(selected || isResizing) && (
<Fragment> <Fragment>
{['nw', 'ne', 'sw', 'se'].map(dir => ( {['nw', 'ne', 'sw', 'se'].map(dir => (
...@@ -505,79 +419,108 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -505,79 +419,108 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
onMouseDown={handleResizeStart(dir)} onMouseDown={handleResizeStart(dir)}
style={{ style={{
position: 'absolute', position: 'absolute',
width: 12, width: 12, height: 12,
height: 12,
backgroundColor: BORDER_COLOR, backgroundColor: BORDER_COLOR,
border: '1px solid white', border: '1px solid white',
[dir[0] === 'n' ? 'top' : 'bottom']: -6, [dir[0] === 'n' ? 'top' : 'bottom']: -6,
[dir[1] === 'w' ? 'left' : 'right']: node.attrs.align === 'center' ? '50%' : -6, [dir[1] === 'w' ? 'left' : 'right']: node.attrs.align === 'center' ? '50%' : -6,
transform: node.attrs.align === 'center' ? transform: node.attrs.align === 'center'
`translateX(${dir[1] === 'w' ? '-100%' : '0%'})` : 'none', ? `translateX(${dir[1] === 'w' ? '-100%' : '0%'})` : 'none',
cursor: `${dir}-resize`, cursor: `${dir}-resize`,
zIndex: 10 zIndex: 10
}} }}
/> />
))} ))}
{showAlignMenu && ( {/* Тулбар выравнивания + обтекание */}
<div style={{ <div style={{
position: 'absolute', position: 'absolute', top: -36, left: '50%',
top: -40, transform: 'translateX(-50%)',
left: '50%', backgroundColor: 'white',
transform: 'translateX(-50%)', boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
backgroundColor: 'white', borderRadius: 4, padding: 4, zIndex: 20,
boxShadow: '0 2px 8px rgba(0,0,0,0.15)', display: 'flex', alignItems: 'center', gap: 2, whiteSpace: 'nowrap',
borderRadius: 4, }}>
padding: 4, {ALIGN_OPTIONS.map(a => (
zIndex: 20, <button
display: 'flex' type="button"
}}> key={a}
{ALIGN_OPTIONS.map(align => ( title={a === 'left' ? 'По левому краю' : a === 'center' ? 'По центру' : 'По правому краю'}
onClick={() => handleAlign(a)}
style={{
padding: '4px 6px',
background: node.attrs.align === a ? '#e6f7ff' : 'transparent',
border: `1px solid ${node.attrs.align === a ? BORDER_COLOR : '#d9d9d9'}`,
borderRadius: 2, cursor: 'pointer', display: 'flex', alignItems: 'center',
}}
>
{a === 'left' && (
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="16" height="2" rx="1" fill="currentColor"/>
<rect x="0" y="4" width="10" height="2" rx="1" fill="currentColor"/>
<rect x="0" y="8" width="16" height="2" rx="1" fill="currentColor"/>
<rect x="0" y="12" width="10" height="2" rx="1" fill="currentColor"/>
</svg>
)}
{a === 'center' && (
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="16" height="2" rx="1" fill="currentColor"/>
<rect x="3" y="4" width="10" height="2" rx="1" fill="currentColor"/>
<rect x="0" y="8" width="16" height="2" rx="1" fill="currentColor"/>
<rect x="3" y="12" width="10" height="2" rx="1" fill="currentColor"/>
</svg>
)}
{a === 'right' && (
<svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="16" height="2" rx="1" fill="currentColor"/>
<rect x="6" y="4" width="10" height="2" rx="1" fill="currentColor"/>
<rect x="0" y="8" width="16" height="2" rx="1" fill="currentColor"/>
<rect x="6" y="12" width="10" height="2" rx="1" fill="currentColor"/>
</svg>
)}
</button>
))}
{node.attrs.align !== 'center' && (
<>
<div style={{ width: 1, background: '#d9d9d9', alignSelf: 'stretch', margin: '0 2px' }} />
<button <button
type="button" type="button"
key={align} title={node.attrs.wrap ? 'Обтекание включено' : 'Обтекание выключено'}
onClick={() => handleAlign(align)} onClick={(e) => {
e.stopPropagation();
safeUpdateAttributes({ wrap: !node.attrs.wrap });
requestAnimationFrame(() => {
try {
const pos = getPos?.();
if (typeof pos === 'number') editor.commands.setNodeSelection(pos);
} catch {}
});
}}
style={{ style={{
margin: '0 2px', padding: '4px 6px',
padding: '10px 8px', background: node.attrs.wrap ? '#e6f7ff' : 'transparent',
background: node.attrs.align === align ? '#e6f7ff' : 'transparent', border: `1px solid ${node.attrs.wrap ? BORDER_COLOR : '#d9d9d9'}`,
border: '1px solid #d9d9d9', borderRadius: 2, cursor: 'pointer', fontSize: 11,
borderRadius: 2, display: 'flex', alignItems: 'center', gap: 3,
cursor: 'pointer'
}} }}
> >
{align} <svg width="16" height="14" viewBox="0 0 16 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="0" y="0" width="7" height="7" rx="1" fill="currentColor" opacity="0.5"/>
<rect x="9" y="0" width="7" height="2" rx="1" fill="currentColor"/>
<rect x="9" y="4" width="5" height="2" rx="1" fill="currentColor"/>
<rect x="0" y="9" width="16" height="2" rx="1" fill="currentColor"/>
<rect x="0" y="12" width="12" height="2" rx="1" fill="currentColor"/>
</svg>
Обтекание
</button> </button>
))} </>
</div> )}
)} </div>
<button
type="button"
onClick={(e) => {
e.stopPropagation();
setShowAlignMenu(!showAlignMenu);
}}
style={{
position: 'absolute',
top: -30,
left: 'calc(50% - 6px)',
transform: 'translateX(-50%)',
backgroundColor: 'white',
border: `1px solid ${BORDER_COLOR}`,
borderRadius: 4,
padding: '8px 8px',
cursor: 'pointer',
fontSize: 12,
zIndex: 10
}}
>
Align
</button>
</Fragment> </Fragment>
)} )}
</div> </div>
{/* Модальное окно редактирования точек */}
<Modal <Modal
open={modalVisible} open={modalVisible}
onCancel={() => setModalVisible(false)} onCancel={() => setModalVisible(false)}
...@@ -587,7 +530,7 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -587,7 +530,7 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
width={800} width={800}
> >
<div><Text>Нажмите на изображение, чтобы добавить маркер</Text></div> <div><Text>Нажмите на изображение, чтобы добавить маркер</Text></div>
<div style={{marginBottom: '10px'}}><Text>Нажмите на маркер, чтобы удалить или изменить текст</Text></div> <div style={{ marginBottom: 10 }}><Text>Нажмите на маркер, чтобы удалить или изменить текст</Text></div>
<div style={{ position: 'relative' }}> <div style={{ position: 'relative' }}>
<img <img
src={node.attrs.src} src={node.attrs.src}
...@@ -624,19 +567,12 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -624,19 +567,12 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
size="small" size="small"
style={{ style={{
position: 'absolute', position: 'absolute',
top: `${newPoint.y}%`, top: `${newPoint.y}%`, left: `${newPoint.x}%`,
left: `${newPoint.x}%`, width: 24, height: 24,
width: 24, borderRadius: '50%', padding: 0,
height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center',
borderRadius: '50%',
padding: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
zIndex: 1000, zIndex: 1000, backgroundColor: '#52c41a', border: 'none',
backgroundColor: '#52c41a',
border: 'none',
}} }}
> >
{pointIcon} {pointIcon}
...@@ -654,7 +590,7 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -654,7 +590,7 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
placeholder="Заголовок" placeholder="Заголовок"
value={editingTitle} value={editingTitle}
onChange={(e) => setEditingTitle(e.target.value)} onChange={(e) => setEditingTitle(e.target.value)}
style={{marginBottom: '8px'}} style={{ marginBottom: 8 }}
/> />
<Input.TextArea <Input.TextArea
autoSize autoSize
...@@ -662,27 +598,14 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -662,27 +598,14 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
onChange={(e) => setEditingText(e.target.value)} onChange={(e) => setEditingText(e.target.value)}
/> />
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 8 }}> <div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 8 }}>
<Button <Button size="small" onClick={() => { setEditingIdx(null); setEditingText(''); }}>
size={'small'}
onClick={() => {
setEditingIdx(null)
setEditingText('')
}}
>
Закрыть Закрыть
</Button> </Button>
<Button <Button size="small" danger onClick={() => { setEditingIdx(null); removePoint(idx); }}>
size={'small'}
danger
onClick={() => {
setEditingIdx(null)
removePoint(idx)
}}
>
Удалить Удалить
</Button> </Button>
<Button <Button
size={'small'} size="small"
type="primary" type="primary"
onClick={() => { onClick={() => {
const updated = [...points] const updated = [...points]
...@@ -699,7 +622,7 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -699,7 +622,7 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
</div> </div>
} }
showCancel={false} showCancel={false}
okButtonProps={{style:{ display: 'none' }}} okButtonProps={{ style: { display: 'none' } }}
> >
<Button <Button
type="primary" type="primary"
...@@ -711,19 +634,12 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected ...@@ -711,19 +634,12 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
}} }}
style={{ style={{
position: 'absolute', position: 'absolute',
top: `${point.y}%`, top: `${point.y}%`, left: `${point.x}%`,
left: `${point.x}%`, width: 24, height: 24,
width: 24, borderRadius: '50%', padding: 0,
height: 24, display: 'flex', alignItems: 'center', justifyContent: 'center',
borderRadius: '50%',
padding: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
transform: 'translate(-50%, -50%)', transform: 'translate(-50%, -50%)',
zIndex: 10, zIndex: 10, backgroundColor: '#1677ff', border: 'none',
backgroundColor: '#1677ff',
border: 'none',
}} }}
> >
{pointIcon} {pointIcon}
...@@ -763,13 +679,17 @@ export const InteractiveImage = Node.create({ ...@@ -763,13 +679,17 @@ export const InteractiveImage = Node.create({
parseHTML: el => el.getAttribute('data-align') || 'left', parseHTML: el => el.getAttribute('data-align') || 'left',
renderHTML: attrs => ({ 'data-align': attrs.align }), renderHTML: attrs => ({ 'data-align': attrs.align }),
}, },
wrap: {
default: false,
parseHTML: el => el.getAttribute('data-wrap') === 'true',
renderHTML: attrs => attrs.wrap ? { 'data-wrap': 'true' } : {},
},
points: { points: {
default: [], default: [],
parseHTML: el => JSON.parse(el.getAttribute('data-points') || '[]'), parseHTML: el => JSON.parse(el.getAttribute('data-points') || '[]'),
renderHTML: attrs => renderHTML: attrs => attrs.points?.length > 0
attrs.points.length > 0 ? { 'data-points': JSON.stringify(attrs.points) }
? { 'data-points': JSON.stringify(attrs.points) } : {},
: {},
}, },
} }
}, },
...@@ -779,23 +699,22 @@ export const InteractiveImage = Node.create({ ...@@ -779,23 +699,22 @@ export const InteractiveImage = Node.create({
}, },
renderHTML({ node, HTMLAttributes }) { renderHTML({ node, HTMLAttributes }) {
const { const { src, width, height } = HTMLAttributes;
src, width, height
} = HTMLAttributes;
const style = [];
const align = node.attrs.align || 'left'; const align = node.attrs.align || 'left';
const wrap = node.attrs.wrap || false;
const points = node.attrs.points || []; const points = node.attrs.points || [];
const style = [];
if (align === 'center') { if (align === 'center') {
style.push('display: block', 'margin-left: auto', 'margin-right: auto'); style.push('display: block', 'margin-left: auto', 'margin-right: auto');
} else if (align === 'left') { } else if (align === 'left') {
style.push('float: left', 'margin-right: 1rem'); wrap
? style.push('float: left', 'margin-right: 1rem')
: style.push('display: block', 'margin-right: auto');
} else if (align === 'right') { } else if (align === 'right') {
style.push('float: right', 'margin-left: 1rem'); wrap
} else if (align === 'text') { ? style.push('float: right', 'margin-left: 1rem')
style.push('display: inline-block', 'vertical-align: middle', 'margin: 0 0.2rem'); : style.push('display: block', 'margin-left: auto');
} }
if (width) style.push(`width: ${width}px`); if (width) style.push(`width: ${width}px`);
...@@ -809,6 +728,7 @@ export const InteractiveImage = Node.create({ ...@@ -809,6 +728,7 @@ export const InteractiveImage = Node.create({
height, height,
style: style.join('; '), style: style.join('; '),
'data-align': align, 'data-align': align,
'data-wrap': wrap ? 'true' : undefined,
'data-points': JSON.stringify(points), 'data-points': JSON.stringify(points),
} }
] ]
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment