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

update fix issue

parent 5b8e6efe
{
"name": "react-ag-qeditor",
"version": "1.1.59",
"version": "1.1.60",
"description": "WYSIWYG html editor",
"author": "atma",
"license": "MIT",
......
......@@ -137,6 +137,13 @@ export const DragAndDrop = Extension.create({
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)}`;
if (nodeType === 'image') {
......@@ -272,6 +279,12 @@ export const DragAndDrop = Extension.create({
const ext = blob.type.split('/')[1]?.split('+')[0] || 'png';
const result = await uploadBlob(blob, `pasted-image.${ext}`);
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 };
} catch (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 { NodeViewWrapper } from '@tiptap/react'
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 BORDER_COLOR = '#0096fd';
const ALIGN_OPTIONS = ['left', 'center', 'right', 'text'];
const ALIGN_OPTIONS = ['left', 'center', 'right'];
const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected }) => {
const [modalVisible, setModalVisible] = useState(false)
......@@ -18,12 +17,11 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
const [newPointTitle, setNewPointTitle] = useState('')
const [editingIdx, setEditingIdx] = useState(null)
const [editingText, setEditingText] = useState('')
const [isResizing, setIsResizing] = useState(false);
const [showAlignMenu, setShowAlignMenu] = useState(false);
const [editingTitle, setEditingTitle] = useState('')
const imgRef = useRef(null);
const isInitialized = useRef(false);
const wrapperRef = useRef(null);
const [isResizing, setIsResizing] = useState(false)
const imgRef = useRef(null)
const isInitialized = useRef(false)
const wrapperRef = useRef(null)
// Обработка кликов вне изображения
useEffect(() => {
......@@ -31,37 +29,31 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
if (wrapperRef.current && !wrapperRef.current.contains(event.target) && selected) {
try {
const pos = getPos?.()
if (typeof pos === 'number') {
editor.commands.setNodeSelection(pos)
}
if (typeof pos === 'number') editor.commands.setNodeSelection(pos)
} catch (e) {
console.warn('getPos() failed:', e)
}
// editor.commands.setNodeSelection(getPos());
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [selected, editor, getPos]);
// Загрузка и инициализация изображения
// Инициализация размеров изображения
useEffect(() => {
if (!imgRef.current || isInitialized.current) return;
const initImageSize = () => {
try {
// Если размеры уже заданы в атрибутах - используем их сразу
if (node.attrs.width && node.attrs.height) {
isInitialized.current = true;
return;
}
const { width: editorWidth } = getEditorDimensions();
const naturalWidth = imgRef.current.naturalWidth;
const naturalHeight = imgRef.current.naturalHeight;
if (naturalWidth <= 0 || naturalHeight <= 0) {
console.warn('Image has invalid natural dimensions, retrying...');
setTimeout(initImageSize, 100);
return;
}
......@@ -87,7 +79,6 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
};
const handleLoad = () => {
// Если размеры уже заданы в атрибутах, пропускаем инициализацию
if (node.attrs.width && node.attrs.height) {
isInitialized.current = true;
return;
......@@ -108,46 +99,19 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
};
}, [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 rect = e.target.getBoundingClientRect()
const x = ((e.clientX - rect.left) / rect.width) * 100
const y = ((e.clientY - rect.top) / rect.height) * 100
setNewPoint({ x, y })
setNewPointText('')
setNewPointTitle('')
}
const confirmAddPoint = () => {
const newPoints = [...points, {
...newPoint,
text: newPointText,
title: newPointTitle,
}]
const newPoints = [...points, { ...newPoint, text: newPointText, title: newPointTitle }]
setPoints(newPoints)
updateAttributes({ points: newPoints })
setNewPoint(null)
......@@ -155,7 +119,6 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
setNewPointTitle('')
}
const cancelAddPoint = () => {
setNewPoint(null)
setNewPointText('')
......@@ -167,7 +130,8 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
updateAttributes({ points: newPoints })
}
// Обработка ресайза изображения
// ─── Ресайз ───────────────────────────────────────────────────────────────
const handleResizeStart = (direction) => (e) => {
e.preventDefault();
e.stopPropagation();
......@@ -175,13 +139,10 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
setIsResizing(true);
try {
const pos = getPos?.()
if (typeof pos === 'number') {
editor.commands.setNodeSelection(pos)
}
if (typeof pos === 'number') editor.commands.setNodeSelection(pos)
} catch (e) {
console.warn('getPos() failed:', e)
}
// editor.commands.setNodeSelection(getPos());
const startWidth = node.attrs.width || imgRef.current.naturalWidth;
const startHeight = node.attrs.height || imgRef.current.naturalHeight;
......@@ -193,10 +154,8 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
const onMouseMove = (e) => {
requestAnimationFrame(() => {
const maxWidth = node.attrs.align === 'center' ? initialEditorWidth : initialAvailableSpace;
const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY;
let newWidth, newHeight;
if (node.attrs.align === 'center') {
......@@ -207,27 +166,18 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
newHeight = Math.round(newWidth / aspectRatio);
} else {
const scale = direction.includes('e') ? 1 : -1;
newWidth = Math.min(
Math.max(startWidth + deltaX * scale, MIN_WIDTH),
maxWidth
);
newWidth = Math.min(Math.max(startWidth + deltaX * scale, MIN_WIDTH), maxWidth);
newHeight = Math.round(newWidth / aspectRatio);
}
} else {
if (direction.includes('e') || direction.includes('w')) {
const scale = direction.includes('e') ? 1 : -1;
newWidth = Math.min(
Math.max(startWidth + deltaX * scale, MIN_WIDTH),
maxWidth
);
newWidth = Math.min(Math.max(startWidth + deltaX * scale, MIN_WIDTH), maxWidth);
newHeight = Math.round(newWidth / aspectRatio);
} else {
const scale = direction.includes('s') ? 1 : -1;
newHeight = Math.max(startHeight + deltaY * scale, MIN_WIDTH);
newWidth = Math.min(
Math.round(newHeight * aspectRatio),
maxWidth
);
newWidth = Math.min(Math.round(newHeight * aspectRatio), maxWidth);
newHeight = Math.round(newWidth / aspectRatio);
}
}
......@@ -236,20 +186,16 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
});
};
const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
setIsResizing(false);
try {
const pos = getPos?.()
if (typeof pos === 'number') {
editor.commands.setNodeSelection(pos)
}
if (typeof pos === 'number') editor.commands.setNodeSelection(pos)
} catch (e) {
console.warn('getPos() failed:', e)
}
// editor.commands.setNodeSelection(getPos());
editor.commands.focus();
};
......@@ -257,37 +203,16 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
window.addEventListener('mouseup', onMouseUp);
};
const safeUpdateAttributes = (newAttrs) => {
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);
}
}
// Проверяем минимальный размер
if (width < MIN_WIDTH) {
const ratio = MIN_WIDTH / width;
width = MIN_WIDTH;
height = Math.round(height * ratio);
}
// ─── Helpers ──────────────────────────────────────────────────────────────
updateAttributes({ width, height, ...newAttrs });
const safeUpdateAttributes = (patch) => {
let { width, height, align } = { ...node.attrs, ...patch }
const minW = 80, maxW = 1600, minH = 40, maxH = 2000
if (width != null) width = Math.min(maxW, Math.max(minW, Number(width) || 0))
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({ ...node.attrs, ...patch, width, height, align })
};
const getEditorDimensions = () => {
......@@ -301,12 +226,9 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
const availableEditorWidth = fullEditorWidth - paddingLeft - paddingRight;
let container;
// при center — всегда редактор
if (node.attrs.align === 'center') {
container = editorContent;
} else {
// при других выравниваниях — ближайший блок
container = imgRef.current?.closest('li, blockquote, td, p, div') || editorContent;
}
......@@ -315,73 +237,69 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
const containerPaddingRight = parseFloat(containerStyles.paddingRight) || 0;
const containerWidth = container.clientWidth - containerPaddingLeft - containerPaddingRight;
return {
width: containerWidth, // текущая ширина контейнера
availableSpace: availableEditorWidth // фиксированная доступная ширина
};
return { width: containerWidth, availableSpace: availableEditorWidth };
};
// Изменение выравнивания с автоматическим масштабированием
const handleAlign = (align) => {
safeUpdateAttributes({ align }); // первый вызов
safeUpdateAttributes({ align });
setTimeout(() => {
safeUpdateAttributes({ align }); // повторный вызов с обновлёнными размерами
safeUpdateAttributes({ align });
try {
const pos = getPos?.()
if (typeof pos === 'number') editor.commands.setNodeSelection(pos)
} catch {}
}, 50);
setShowAlignMenu(false);
editor.commands.focus();
};
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>
)
// ─── Стили (идентично Image.jsx) ─────────────────────────────────────────
const getOuterStyle = () => {
const { align, wrap, width } = node.attrs;
const w = width ? `${width}px` : 'auto';
const sharedMargin = { marginTop: '0.5rem', marginBottom: '0.5rem' };
const getWrapperStyle = () => {
const baseStyle = {
display: 'inline-block',
lineHeight: 0,
position: 'relative',
outline: (selected || isResizing) ? `1px dashed ${BORDER_COLOR}` : 'none',
verticalAlign: 'top',
margin: '0.5rem 0',
if (align === 'center') {
return {
...sharedMargin, lineHeight: 0,
display: 'inline-block', float: 'left', clear: 'both',
width: '100%', textAlign: 'center',
};
if (node.attrs.align === 'center') {
}
if (!wrap) {
return {
...baseStyle,
display: 'block',
marginLeft: 'auto',
marginRight: 'auto',
width: node.attrs.width ? `${node.attrs.width}px` : 'fit-content',
maxWidth: '100%',
textAlign: 'center'
...sharedMargin, lineHeight: 0,
display: 'inline-block', float: 'left', clear: 'both', width: '100%',
...(align === 'right' ? { textAlign: 'right' } : { textAlign: 'left' }),
};
}
return {
...baseStyle,
...(node.attrs.align === 'left' && {
float: 'left',
marginRight: '1rem',
width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
}),
...(node.attrs.align === 'right' && {
float: 'right',
marginLeft: '1rem',
width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
}),
...(node.attrs.align === 'text' && {
...sharedMargin, lineHeight: 0,
display: 'inline-block',
float: 'none',
margin: '0 0.2rem',
verticalAlign: 'middle',
width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
}),
float: align === 'left' ? 'left' : 'right',
...(align === 'left' ? { marginRight: '1rem' } : { marginLeft: '1rem' }),
width: w, maxWidth: '100%',
};
};
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 = () => ({
width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
height: 'auto',
......@@ -390,21 +308,29 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
cursor: 'default',
userSelect: 'none',
margin: node.attrs.align === 'center' ? '0 auto' : '0',
verticalAlign: node.attrs.align === 'text' ? 'middle' : 'top',
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 (
<NodeViewWrapper ref={wrapperRef} as="div" className="interactive-image-wrapper" contentEditable={false}>
<NodeViewWrapper as="div" style={getOuterStyle()} contentEditable={false} className="interactive-image-wrapper">
<div
style={getWrapperStyle()}
ref={wrapperRef}
style={getInnerStyle()}
onClick={(e) => {
e.stopPropagation();
try {
const pos = getPos?.();
if (typeof pos === 'number') {
editor.commands.setNodeSelection(pos);
}
if (typeof pos === 'number') editor.commands.setNodeSelection(pos);
} catch {}
}}
>
......@@ -418,7 +344,6 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
const { width: editorWidth } = getEditorDimensions();
const naturalWidth = imgRef.current.naturalWidth;
const naturalHeight = imgRef.current.naturalHeight;
safeUpdateAttributes({
width: naturalWidth,
height: naturalHeight,
......@@ -428,14 +353,18 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
}
}}
/>
{/* Кнопка редактирования точек */}
<Button
size="default"
type="primary"
onClick={() => setModalVisible(true)}
style={{ position: 'absolute', top: '4px', right: '30px', zIndex: 10 }}
onClick={(e) => { e.stopPropagation(); setModalVisible(true); }}
style={{ position: 'absolute', top: 4, right: 30, zIndex: 10 }}
>
Редактировать
</Button>
{/* Маркеры точек */}
{points.map((point, idx) => (
<Button
key={idx}
......@@ -445,58 +374,43 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
position: 'absolute',
top: `${point.y}%`,
left: `${point.x}%`,
width: 24,
height: 24,
borderRadius: '50%',
padding: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
width: 24, height: 24,
borderRadius: '50%', padding: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
transform: 'translate(-50%, -50%)',
zIndex: 5,
backgroundColor: '#1677ff',
border: 'none',
pointerEvents: 'none', // чтобы не блокировала выбор или драг
backgroundColor: '#1677ff', border: 'none',
pointerEvents: 'none',
}}
title={point.title || point.text}
>
{pointIcon}
</Button>
))}
{/* Кнопка удаления */}
{selected && (
<Button
type="text"
danger
size="small"
onClick={(e) => {
e.stopPropagation()
const pos = getPos?.()
e.stopPropagation();
const pos = getPos?.();
if (typeof pos === 'number') {
editor.view.dispatch(
editor.view.state.tr.delete(pos, pos + node.nodeSize)
)
editor.view.dispatch(editor.view.state.tr.delete(pos, pos + node.nodeSize))
}
}}
style={{
position: 'absolute',
top: 4,
right: 4,
zIndex: 30,
backgroundColor: 'white',
border: '1px solid #d9d9d9',
borderRadius: '50%',
width: 20,
height: 20,
fontSize: 12,
lineHeight: 1,
padding: '0px 0px 2px 0px',
cursor: 'pointer'
position: 'absolute', top: 4, right: 4, zIndex: 30,
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) && (
<Fragment>
{['nw', 'ne', 'sw', 'se'].map(dir => (
......@@ -505,79 +419,108 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
onMouseDown={handleResizeStart(dir)}
style={{
position: 'absolute',
width: 12,
height: 12,
width: 12, height: 12,
backgroundColor: BORDER_COLOR,
border: '1px solid white',
[dir[0] === 'n' ? 'top' : 'bottom']: -6,
[dir[1] === 'w' ? 'left' : 'right']: node.attrs.align === 'center' ? '50%' : -6,
transform: node.attrs.align === 'center' ?
`translateX(${dir[1] === 'w' ? '-100%' : '0%'})` : 'none',
transform: node.attrs.align === 'center'
? `translateX(${dir[1] === 'w' ? '-100%' : '0%'})` : 'none',
cursor: `${dir}-resize`,
zIndex: 10
}}
/>
))}
{showAlignMenu && (
{/* Тулбар выравнивания + обтекание */}
<div style={{
position: 'absolute',
top: -40,
left: '50%',
position: 'absolute', top: -36, left: '50%',
transform: 'translateX(-50%)',
backgroundColor: 'white',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
borderRadius: 4,
padding: 4,
zIndex: 20,
display: 'flex'
borderRadius: 4, padding: 4, zIndex: 20,
display: 'flex', alignItems: 'center', gap: 2, whiteSpace: 'nowrap',
}}>
{ALIGN_OPTIONS.map(align => (
{ALIGN_OPTIONS.map(a => (
<button
type="button"
key={align}
onClick={() => handleAlign(align)}
key={a}
title={a === 'left' ? 'По левому краю' : a === 'center' ? 'По центру' : 'По правому краю'}
onClick={() => handleAlign(a)}
style={{
margin: '0 2px',
padding: '10px 8px',
background: node.attrs.align === align ? '#e6f7ff' : 'transparent',
border: '1px solid #d9d9d9',
borderRadius: 2,
cursor: 'pointer'
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',
}}
>
{align}
{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>
))}
</div>
)}
{node.attrs.align !== 'center' && (
<>
<div style={{ width: 1, background: '#d9d9d9', alignSelf: 'stretch', margin: '0 2px' }} />
<button
type="button"
title={node.attrs.wrap ? 'Обтекание включено' : 'Обтекание выключено'}
onClick={(e) => {
e.stopPropagation();
setShowAlignMenu(!showAlignMenu);
safeUpdateAttributes({ wrap: !node.attrs.wrap });
requestAnimationFrame(() => {
try {
const pos = getPos?.();
if (typeof pos === 'number') editor.commands.setNodeSelection(pos);
} catch {}
});
}}
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
padding: '4px 6px',
background: node.attrs.wrap ? '#e6f7ff' : 'transparent',
border: `1px solid ${node.attrs.wrap ? BORDER_COLOR : '#d9d9d9'}`,
borderRadius: 2, cursor: 'pointer', fontSize: 11,
display: 'flex', alignItems: 'center', gap: 3,
}}
>
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>
</>
)}
</div>
</Fragment>
)}
</div>
{/* Модальное окно редактирования точек */}
<Modal
open={modalVisible}
onCancel={() => setModalVisible(false)}
......@@ -587,7 +530,7 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
width={800}
>
<div><Text>Нажмите на изображение, чтобы добавить маркер</Text></div>
<div style={{marginBottom: '10px'}}><Text>Нажмите на маркер, чтобы удалить или изменить текст</Text></div>
<div style={{ marginBottom: 10 }}><Text>Нажмите на маркер, чтобы удалить или изменить текст</Text></div>
<div style={{ position: 'relative' }}>
<img
src={node.attrs.src}
......@@ -624,19 +567,12 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
size="small"
style={{
position: 'absolute',
top: `${newPoint.y}%`,
left: `${newPoint.x}%`,
width: 24,
height: 24,
borderRadius: '50%',
padding: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
top: `${newPoint.y}%`, left: `${newPoint.x}%`,
width: 24, height: 24,
borderRadius: '50%', padding: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
transform: 'translate(-50%, -50%)',
zIndex: 1000,
backgroundColor: '#52c41a',
border: 'none',
zIndex: 1000, backgroundColor: '#52c41a', border: 'none',
}}
>
{pointIcon}
......@@ -654,7 +590,7 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
placeholder="Заголовок"
value={editingTitle}
onChange={(e) => setEditingTitle(e.target.value)}
style={{marginBottom: '8px'}}
style={{ marginBottom: 8 }}
/>
<Input.TextArea
autoSize
......@@ -662,27 +598,14 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
onChange={(e) => setEditingText(e.target.value)}
/>
<div style={{ display: 'flex', justifyContent: 'flex-end', gap: 8, marginTop: 8 }}>
<Button
size={'small'}
onClick={() => {
setEditingIdx(null)
setEditingText('')
}}
>
<Button size="small" onClick={() => { setEditingIdx(null); setEditingText(''); }}>
Закрыть
</Button>
<Button
size={'small'}
danger
onClick={() => {
setEditingIdx(null)
removePoint(idx)
}}
>
<Button size="small" danger onClick={() => { setEditingIdx(null); removePoint(idx); }}>
Удалить
</Button>
<Button
size={'small'}
size="small"
type="primary"
onClick={() => {
const updated = [...points]
......@@ -699,7 +622,7 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
</div>
}
showCancel={false}
okButtonProps={{style:{ display: 'none' }}}
okButtonProps={{ style: { display: 'none' } }}
>
<Button
type="primary"
......@@ -711,19 +634,12 @@ const InteractiveImageView = ({ node, updateAttributes, editor, getPos, selected
}}
style={{
position: 'absolute',
top: `${point.y}%`,
left: `${point.x}%`,
width: 24,
height: 24,
borderRadius: '50%',
padding: 0,
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
top: `${point.y}%`, left: `${point.x}%`,
width: 24, height: 24,
borderRadius: '50%', padding: 0,
display: 'flex', alignItems: 'center', justifyContent: 'center',
transform: 'translate(-50%, -50%)',
zIndex: 10,
backgroundColor: '#1677ff',
border: 'none',
zIndex: 10, backgroundColor: '#1677ff', border: 'none',
}}
>
{pointIcon}
......@@ -763,11 +679,15 @@ export const InteractiveImage = Node.create({
parseHTML: el => el.getAttribute('data-align') || 'left',
renderHTML: attrs => ({ 'data-align': attrs.align }),
},
wrap: {
default: false,
parseHTML: el => el.getAttribute('data-wrap') === 'true',
renderHTML: attrs => attrs.wrap ? { 'data-wrap': 'true' } : {},
},
points: {
default: [],
parseHTML: el => JSON.parse(el.getAttribute('data-points') || '[]'),
renderHTML: attrs =>
attrs.points.length > 0
renderHTML: attrs => attrs.points?.length > 0
? { 'data-points': JSON.stringify(attrs.points) }
: {},
},
......@@ -779,23 +699,22 @@ export const InteractiveImage = Node.create({
},
renderHTML({ node, HTMLAttributes }) {
const {
src, width, height
} = HTMLAttributes;
const style = [];
const { src, width, height } = HTMLAttributes;
const align = node.attrs.align || 'left';
const wrap = node.attrs.wrap || false;
const points = node.attrs.points || [];
const style = [];
if (align === 'center') {
style.push('display: block', 'margin-left: auto', 'margin-right: auto');
} 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') {
style.push('float: right', 'margin-left: 1rem');
} else if (align === 'text') {
style.push('display: inline-block', 'vertical-align: middle', 'margin: 0 0.2rem');
wrap
? style.push('float: right', 'margin-left: 1rem')
: style.push('display: block', 'margin-left: auto');
}
if (width) style.push(`width: ${width}px`);
......@@ -809,6 +728,7 @@ export const InteractiveImage = Node.create({
height,
style: style.join('; '),
'data-align': align,
'data-wrap': wrap ? 'true' : undefined,
'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