Skip to content
GitLab
Menu
Projects
Groups
Snippets
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
lib
react-ag-qeditor
Commits
8d6f07f5
Commit
8d6f07f5
authored
Jul 01, 2025
by
Яков
Browse files
add align text
parent
f34c4c37
Changes
2
Hide whitespace changes
Inline
Side-by-side
package.json
View file @
8d6f07f5
{
{
"name"
:
"react-ag-qeditor"
,
"name"
:
"react-ag-qeditor"
,
"version"
:
"1.0.9
2
"
,
"version"
:
"1.0.9
3
"
,
"description"
:
"WYSIWYG html editor"
,
"description"
:
"WYSIWYG html editor"
,
"author"
:
"atma"
,
"author"
:
"atma"
,
"license"
:
"
MIT
"
,
"license"
:
"
MIT
"
,
...
...
src/extensions/Image.jsx
View file @
8d6f07f5
...
@@ -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
;
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment