Commit 0b426860 authored by Яков's avatar Яков
Browse files

update image

parent a123742c
{
"name": "react-ag-qeditor",
"version": "1.1.35",
"version": "1.1.36",
"description": "WYSIWYG html editor",
"author": "atma",
"license": "MIT",
......
......@@ -8,17 +8,18 @@ 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 ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, selected }) => {
const imgRef = useRef(null);
const wrapperRef = useRef(null);
const [showAlignMenu, setShowAlignMenu] = useState(false);
const isInitialized = useRef(false);
const [isResizing, setIsResizing] = useState(false);
const [altModalVisible, setAltModalVisible] = useState(false);
const [tempAlt, setTempAlt] = useState(node.attrs.alt || '');
const [tempFrontAlt, setTempFrontAlt] = useState(node.attrs.frontAlt || '');
// wrap=false + left/right: outer wrapper is full-width block, inner div holds image+handles
const isNoWrap = !node.attrs.wrap && (node.attrs.align === 'left' || node.attrs.align === 'right');
// Добавляем прозрачный нулевой пробел после изображения
......@@ -370,57 +371,70 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
console.warn('getPos() failed:', e)
}
}, 50);
setShowAlignMenu(false);
};
// Стили для обертки изображения
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',
};
// Внешняя обёртка (NodeViewWrapper): управляет float/block-layout и отступами
const getOuterStyle = () => {
const { align, wrap, width } = node.attrs;
const w = width ? `${width}px` : 'auto';
const sharedMargin = { marginTop: '0.5rem', marginBottom: '0.5rem' };
if (node.attrs.align === 'center') {
if (align === 'center') {
return { ...sharedMargin, display: 'block', lineHeight: 0 };
}
if (!wrap) {
// no-wrap: float:left + width:100% — занимает всю строку, текст не может встать рядом.
// Используем float (а не display:block) чтобы layout-алгоритм был одинаковым
// с wrap=true и не было прыжка при переключении обтекания.
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' }),
};
}
// wrap: true — узкий float с шириной картинки, текст обтекает
return {
...baseStyle,
...(node.attrs.align === 'left' && {
float: 'left',
marginRight: '1rem',
width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
maxWidth: '100%',
}),
...(node.attrs.align === 'right' && {
float: 'right',
marginLeft: '1rem',
width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
maxWidth: '100%',
}),
...(node.attrs.align === 'text' && {
display: 'inline-block',
float: 'none',
margin: '0 0.2rem',
verticalAlign: 'middle',
width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
maxWidth: '100%',
}),
...sharedMargin, lineHeight: 0,
display: 'inline-block',
float: align === 'left' ? 'left' : 'right',
...(align === 'left' ? { marginRight: '1rem' } : { marginLeft: '1rem' }),
width: w, maxWidth: '100%',
};
};
// Внутренний контейнер: всегда inline-block — надёжно получает высоту от дочернего img
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 handleNodeClick = (e) => {
e.stopPropagation();
try {
const pos = getPos?.();
if (typeof pos === 'number') editor.commands.setNodeSelection(pos);
} catch (err) {
console.warn('getPos() failed:', err);
}
};
// Стили для самого изображения
const getImageStyle = () => ({
width: node.attrs.width ? `${node.attrs.width}px` : 'auto',
......@@ -434,49 +448,16 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
objectFit: 'contain'
});
console.log(node.attrs.frontAlt);
return (
<NodeViewWrapper
as="div"
style={getWrapperStyle()}
ref={wrapperRef}
onClick={(e) => {
e.stopPropagation();
try {
const pos = getPos?.()
if (typeof pos === 'number') {
editor.commands.setNodeSelection(pos)
}
} catch (e) {
console.warn('getPos() failed:', e)
}
// editor.commands.setNodeSelection(getPos());
}}
contentEditable={false}
data-image-wrapper
>
// Inner content shared between both rendering paths
const imageContent = (
<>
<img
{...node.attrs}
ref={imgRef}
draggable={true}
style={getImageStyle()}
// onLoad={() => {
// if (imgRef.current && !isInitialized.current && !node.attrs.width && !node.attrs.height) {
// const { width: editorWidth } = getEditorDimensions();
// const naturalWidth = imgRef.current.naturalWidth;
// const naturalHeight = imgRef.current.naturalHeight;
//
// safeUpdateAttributes({
// width: naturalWidth,
// height: naturalHeight,
// 'data-node-id': node.attrs['data-node-id'] || Math.random().toString(36).substr(2, 9)
// });
// isInitialized.current = true;
// }
// }}
/>
{
node.attrs.frontAlt?.length > 0 &&
{node.attrs.frontAlt?.length > 0 && (
<div
style={{
backgroundColor: '#FDE674',
......@@ -495,7 +476,7 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
whiteSpace: 'pre-line'
}}
>{node.attrs.frontAlt}</div>
}
)}
<Button
size="default"
shape={'circle'}
......@@ -506,12 +487,7 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
setTempFrontAlt(node.attrs.frontAlt || '');
setAltModalVisible(true);
}}
style={{
position: 'absolute',
top: 4,
right: '30px',
zIndex: 15,
}}
style={{ position: 'absolute', top: 4, right: '30px', zIndex: 15 }}
>
<FontSizeOutlined />
</Button>
......@@ -521,33 +497,21 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
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)
)
);
}
}}
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>
......@@ -558,103 +522,126 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos, select
onTouchStart={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%',
transform: 'translateX(-50%)',
backgroundColor: 'white',
boxShadow: '0 2px 8px rgba(0,0,0,0.15)',
borderRadius: 4,
padding: 4,
zIndex: 20,
display: 'flex'
}}>
{ALIGN_OPTIONS.map(align => (
<div style={{
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', alignItems: 'center', gap: 2, whiteSpace: 'nowrap',
}}>
{ALIGN_OPTIONS.map(a => (
<button
type="button"
key={a}
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
type="button"
key={align}
onClick={() => handleAlign(align)}
title={node.attrs.wrap ? 'Обтекание включено' : 'Обтекание выключено'}
onClick={(e) => {
e.stopPropagation();
safeUpdateAttributes({ wrap: !node.attrs.wrap });
requestAnimationFrame(() => {
try {
const pos = getPos?.();
if (typeof pos === 'number') editor.commands.setNodeSelection(pos);
} catch {}
});
}}
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.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>
)}
<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>
</>
)}
</div>
</Fragment>
)}
<Modal
title="Текст на картинке"
open={altModalVisible}
onOk={() => {
updateAttributes({ alt: tempAlt, frontAlt: tempFrontAlt });
setAltModalVisible(false);
}}
onOk={() => { updateAttributes({ alt: tempAlt, frontAlt: tempFrontAlt }); setAltModalVisible(false); }}
onCancel={() => setAltModalVisible(false)}
okText="Применить"
cancelText="Отмена"
>
<div style={{marginBottom: '5px'}}><Text>Лицевая сторона</Text></div>
<TextArea
value={tempFrontAlt}
onChange={(e) => setTempFrontAlt(e.target.value)}
rows={4}
placeholder="Введите текст"
/>
<TextArea value={tempFrontAlt} onChange={(e) => setTempFrontAlt(e.target.value)} rows={4} placeholder="Введите текст" />
<div style={{marginTop: '15px', marginBottom: '5px'}}><Text>Обратная сторона</Text></div>
<TextArea
value={tempAlt}
onChange={(e) => setTempAlt(e.target.value)}
rows={4}
placeholder="Введите текст"
/>
<TextArea value={tempAlt} onChange={(e) => setTempAlt(e.target.value)} rows={4} placeholder="Введите текст" />
</Modal>
</>
);
// Единая структура NodeViewWrapper > div > content — img всегда на одной глубине,
// поэтому при смене выравнивания React не размонтирует img и не перезагружает его.
return (
<NodeViewWrapper as="div" style={getOuterStyle()} contentEditable={false} data-image-wrapper>
<div ref={wrapperRef} style={getInnerStyle()} onClick={handleNodeClick}>
{imageContent}
</div>
</NodeViewWrapper>
);
};
......@@ -704,6 +691,11 @@ const ResizableImageExtension = TipTapImage.extend({
parseHTML: element => element.getAttribute('data-align') || 'left',
renderHTML: attributes => ({ 'data-align': attributes.align })
},
wrap: {
default: false,
parseHTML: element => element.getAttribute('data-wrap') === 'true',
renderHTML: attributes => attributes.wrap ? { 'data-wrap': 'true' } : {}
},
'data-node-id': {
default: null,
parseHTML: element => element.getAttribute('data-node-id'),
......@@ -722,18 +714,20 @@ const ResizableImageExtension = TipTapImage.extend({
} = HTMLAttributes;
const align = node.attrs.align || 'left';
const wrap = node.attrs.wrap || false;
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`);
......
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