Commit 8d6f07f5 by Яков

add align text

parent f34c4c37
{ {
"name": "react-ag-qeditor", "name": "react-ag-qeditor",
"version": "1.0.92", "version": "1.0.93",
"description": "WYSIWYG html editor", "description": "WYSIWYG html editor",
"author": "atma", "author": "atma",
"license": "MIT", "license": "MIT",
......
...@@ -4,7 +4,7 @@ import TipTapImage from "@tiptap/extension-image"; ...@@ -4,7 +4,7 @@ import TipTapImage from "@tiptap/extension-image";
const MIN_WIDTH = 60; const MIN_WIDTH = 60;
const BORDER_COLOR = '#0096fd'; const BORDER_COLOR = '#0096fd';
const ALIGN_OPTIONS = ['left', 'center', 'right']; const ALIGN_OPTIONS = ['left', 'center', 'right', 'text'];
const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => { const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
const imgRef = useRef(null); const imgRef = useRef(null);
...@@ -20,7 +20,6 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => { ...@@ -20,7 +20,6 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
aspectRatio: 1 aspectRatio: 1
}); });
// Добавляем прозрачный нулевой пробел после изображения (исправленная версия)
useEffect(() => { useEffect(() => {
if (!editor?.isEditable || typeof getPos !== 'function') return; if (!editor?.isEditable || typeof getPos !== 'function') return;
...@@ -30,16 +29,13 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => { ...@@ -30,16 +29,13 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
if (typeof pos !== 'number' || pos < 0) return; if (typeof pos !== 'number' || pos < 0) return;
const doc = editor.state.doc; const doc = editor.state.doc;
const insertPos = pos + 1; // Позиция после изображения const insertPos = pos + 1;
// Проверяем, что позиция существует в документе
if (insertPos >= doc.content.size) return; if (insertPos >= doc.content.size) return;
// Проверяем, не добавлен ли уже нулевой пробел
const nextNode = doc.nodeAt(insertPos); const nextNode = doc.nodeAt(insertPos);
if (nextNode?.textContent === '\u200B') return; if (nextNode?.textContent === '\u200B') return;
// Вставляем пробел с небольшой задержкой для стабильности
setTimeout(() => { setTimeout(() => {
if (editor.isDestroyed) return; if (editor.isDestroyed) return;
editor.commands.insertContentAt(insertPos, { editor.commands.insertContentAt(insertPos, {
...@@ -56,7 +52,6 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => { ...@@ -56,7 +52,6 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
return () => clearTimeout(timer); return () => clearTimeout(timer);
}, [editor, getPos]); }, [editor, getPos]);
// Инициализация размеров (исправленная версия)
useEffect(() => { useEffect(() => {
if (!imgRef.current || isInitialized.current) return; if (!imgRef.current || isInitialized.current) return;
...@@ -65,7 +60,6 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => { ...@@ -65,7 +60,6 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
const width = node.attrs.width || imgRef.current.naturalWidth; const width = node.attrs.width || imgRef.current.naturalWidth;
const height = node.attrs.height || imgRef.current.naturalHeight; const height = node.attrs.height || imgRef.current.naturalHeight;
// Проверяем валидность размеров перед обновлением
if (width > 0 && height > 0) { if (width > 0 && height > 0) {
updateAttributes({ updateAttributes({
width: Math.round(width), width: Math.round(width),
...@@ -78,16 +72,13 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => { ...@@ -78,16 +72,13 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
} }
}; };
// Если изображение уже загружено
if (imgRef.current.complete) { if (imgRef.current.complete) {
initImageSize(); initImageSize();
} else { } else {
// Если еще загружается - ждем события onLoad
imgRef.current.onload = initImageSize; imgRef.current.onload = initImageSize;
} }
return () => { return () => {
// Очищаем обработчик при размонтировании
if (imgRef.current) { if (imgRef.current) {
imgRef.current.onload = null; imgRef.current.onload = null;
} }
...@@ -98,6 +89,7 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => { ...@@ -98,6 +89,7 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
const nodePos = typeof getPos === 'function' ? getPos() : null;
const currentWidth = node.attrs.width || imgRef.current.naturalWidth; const currentWidth = node.attrs.width || imgRef.current.naturalWidth;
const currentHeight = node.attrs.height || imgRef.current.naturalHeight; const currentHeight = node.attrs.height || imgRef.current.naturalHeight;
...@@ -107,11 +99,22 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => { ...@@ -107,11 +99,22 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
startX: e.clientX, startX: e.clientX,
startY: e.clientY, startY: e.clientY,
aspectRatio: currentWidth / currentHeight, aspectRatio: currentWidth / currentHeight,
direction direction,
nodePos
}; };
const onMouseMove = (e) => { const onMouseMove = (e) => {
const { startWidth, startHeight, startX, startY, aspectRatio, direction } = resizeData.current; const { startWidth, startHeight, startX, startY, aspectRatio, direction, nodePos } = resizeData.current;
// Проверяем, что узел все еще существует
if (typeof nodePos === 'number') {
try {
const resolvedPos = editor.view.state.doc.resolve(nodePos);
if (!resolvedPos?.nodeAfter) return;
} catch {
return;
}
}
const deltaX = e.clientX - startX; const deltaX = e.clientX - startX;
const deltaY = e.clientY - startY; const deltaY = e.clientY - startY;
...@@ -119,20 +122,16 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => { ...@@ -119,20 +122,16 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
let newWidth, newHeight; let newWidth, newHeight;
if (node.attrs.align === 'center') { if (node.attrs.align === 'center') {
// Особый случай для центрированного изображения
if (direction.includes('n') || direction.includes('s')) { if (direction.includes('n') || direction.includes('s')) {
// Только вертикальный ресайз с сохранением пропорций
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.round(newHeight * aspectRatio); newWidth = Math.round(newHeight * aspectRatio);
} else { } else {
// Горизонтальный ресайз с сохранением пропорций
const scale = direction.includes('e') ? 1 : -1; const scale = direction.includes('e') ? 1 : -1;
newWidth = Math.max(startWidth + deltaX * scale, MIN_WIDTH); newWidth = Math.max(startWidth + deltaX * scale, MIN_WIDTH);
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.max(startWidth + deltaX * scale, MIN_WIDTH); newWidth = Math.max(startWidth + deltaX * scale, MIN_WIDTH);
...@@ -144,16 +143,39 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => { ...@@ -144,16 +143,39 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
} }
} }
updateAttributes({ // Принудительное обновление с новым transaction
const { state, dispatch } = editor.view;
const tr = state.tr.setNodeMarkup(nodePos, undefined, {
...node.attrs,
width: newWidth, width: newWidth,
height: newHeight height: newHeight
}); });
dispatch(tr);
}; };
const onMouseUp = () => { const onMouseUp = () => {
window.removeEventListener('mousemove', onMouseMove); window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp); window.removeEventListener('mouseup', onMouseUp);
editor.commands.focus();
// Обновляем атрибуты в конце ресайза для синхронизации
if (typeof resizeData.current.nodePos === 'number') {
updateAttributes({
width: node.attrs.width,
height: node.attrs.height
});
}
if (!editor.isDestroyed && editor.view) {
setTimeout(() => {
try {
editor.commands.focus();
// Принудительное обновление представления
editor.view.dispatch(editor.view.state.tr.setMeta('forceUpdate', true));
} catch (error) {
console.warn('Focus error after resize:', error);
}
}, 50);
}
}; };
window.addEventListener('mousemove', onMouseMove); window.addEventListener('mousemove', onMouseMove);
...@@ -189,6 +211,14 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => { ...@@ -189,6 +211,14 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
marginRight: 'auto', marginRight: 'auto',
textAlign: 'center' textAlign: 'center'
}; };
case 'text':
return {
...baseStyle,
display: 'inline-block',
float: 'none',
margin: '0 0.2rem',
verticalAlign: 'middle'
};
case 'wrap-left': case 'wrap-left':
return { ...baseStyle, float: 'left', margin: '0 1rem 1rem 0', shapeOutside: 'margin-box' }; return { ...baseStyle, float: 'left', margin: '0 1rem 1rem 0', shapeOutside: 'margin-box' };
case 'wrap-right': case 'wrap-right':
...@@ -205,7 +235,8 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => { ...@@ -205,7 +235,8 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
display: 'block', display: 'block',
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'
}); });
return ( return (
...@@ -356,17 +387,14 @@ const ResizableImageExtension = TipTapImage.extend({ ...@@ -356,17 +387,14 @@ const ResizableImageExtension = TipTapImage.extend({
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
// Получаем align из атрибутов, учитывая data-align как fallback
const align = HTMLAttributes.align || const align = HTMLAttributes.align ||
HTMLAttributes['data-align'] || HTMLAttributes['data-align'] ||
'left'; 'left';
// Определяем, нужно ли применять float
const floatValue = align.startsWith('wrap-') ? align.split('-')[1] : const floatValue = align.startsWith('wrap-') ? align.split('-')[1] :
['left', 'right'].includes(align) ? align : ['left', 'right'].includes(align) ? align :
'none'; 'none';
// Определяем margin в зависимости от выравнивания
let marginValue; let marginValue;
switch(align) { switch(align) {
case 'left': case 'left':
...@@ -380,6 +408,9 @@ const ResizableImageExtension = TipTapImage.extend({ ...@@ -380,6 +408,9 @@ const ResizableImageExtension = TipTapImage.extend({
case 'center': case 'center':
marginValue = '0.5rem auto'; marginValue = '0.5rem auto';
break; break;
case 'text':
marginValue = '0 0.2rem';
break;
default: default:
marginValue = '0'; marginValue = '0';
} }
...@@ -387,13 +418,13 @@ const ResizableImageExtension = TipTapImage.extend({ ...@@ -387,13 +418,13 @@ const ResizableImageExtension = TipTapImage.extend({
return ['span', { return ['span', {
'data-type': 'resizable-image', 'data-type': 'resizable-image',
'data-image-wrapper': true, 'data-image-wrapper': true,
'data-align': align, // Сохраняем значение align в data-атрибуте 'data-align': align,
style: ` style: `
display: ${align === 'center' ? 'block' : 'inline-block'}; display: ${align === 'center' ? 'block' : 'inline-block'};
float: ${floatValue}; float: ${floatValue};
margin: ${marginValue}; margin: ${marginValue};
shape-outside: ${align.startsWith('wrap-') ? 'margin-box' : 'none'}; shape-outside: ${align.startsWith('wrap-') ? 'margin-box' : 'none'};
vertical-align: top; vertical-align: ${align === 'text' ? 'middle' : 'top'};
position: relative; position: relative;
${align === 'center' ? 'width: 100%; text-align: center;' : ''} ${align === 'center' ? 'width: 100%; text-align: center;' : ''}
` `
...@@ -405,8 +436,8 @@ const ResizableImageExtension = TipTapImage.extend({ ...@@ -405,8 +436,8 @@ const ResizableImageExtension = TipTapImage.extend({
margin: ${align === 'center' ? '0 auto' : '0'}; margin: ${align === 'center' ? '0 auto' : '0'};
max-width: 100%; max-width: 100%;
height: auto; height: auto;
vertical-align: ${align === 'text' ? 'middle' : 'top'};
`, `,
// Убедимся, что align также передаётся в img
'data-align': align 'data-align': align
}]]; }]];
}, },
...@@ -418,7 +449,7 @@ const ResizableImageExtension = TipTapImage.extend({ ...@@ -418,7 +449,7 @@ const ResizableImageExtension = TipTapImage.extend({
inline: true, inline: true,
group: 'inline', group: 'inline',
draggable: true, draggable: true,
selectable: false // Важно отключить выделение изображения selectable: false
}); });
export default ResizableImageExtension; export default ResizableImageExtension;
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