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
88e590d1
Commit
88e590d1
authored
May 22, 2026
by
Яков
Browse files
update fix issue
parent
5b8e6efe
Changes
3
Show whitespace changes
Inline
Side-by-side
package.json
View file @
88e590d1
{
{
"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
"
,
...
...
src/extensions/DragAndDrop.js
View file @
88e590d1
...
@@ -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
);
...
...
src/extensions/InteractiveImage.js
View file @
88e590d1
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
);
}
}
// Проверяем минимальный размер
if
(
width
<
MIN_WIDTH
)
{
const
ratio
=
MIN_WIDTH
/
width
;
width
=
MIN_WIDTH
;
height
=
Math
.
round
(
height
*
ratio
);
}
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
=
()
=>
{
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
getOuterStyle
=
()
=>
{
const
{
align
,
wrap
,
width
}
=
node
.
attrs
;
const
w
=
width
?
`
${
width
}
px`
:
'
auto
'
;
const
sharedMargin
=
{
marginTop
:
'
0.5rem
'
,
marginBottom
:
'
0.5rem
'
};
const
getWrapperStyle
=
()
=>
{
if
(
align
===
'
center
'
)
{
const
baseStyle
=
{
return
{
display
:
'
inline-block
'
,
...
sharedMargin
,
lineHeight
:
0
,
lineHeight
:
0
,
display
:
'
inline-block
'
,
float
:
'
left
'
,
clear
:
'
both
'
,
position
:
'
relative
'
,
width
:
'
100%
'
,
textAlign
:
'
center
'
,
outline
:
(
selected
||
isResizing
)
?
`1px dashed
${
BORDER_COLOR
}
`
:
'
none
'
,
verticalAlign
:
'
top
'
,
margin
:
'
0.5rem 0
'
,
};
};
}
if
(
node
.
attrs
.
align
===
'
center
'
)
{
if
(
!
wrap
)
{
return
{
return
{
...
baseStyle
,
...
sharedMargin
,
lineHeight
:
0
,
display
:
'
block
'
,
display
:
'
inline-block
'
,
float
:
'
left
'
,
clear
:
'
both
'
,
width
:
'
100%
'
,
marginLeft
:
'
auto
'
,
...(
align
===
'
right
'
?
{
textAlign
:
'
right
'
}
:
{
textAlign
:
'
left
'
}),
marginRight
:
'
auto
'
,
width
:
node
.
attrs
.
width
?
`
${
node
.
attrs
.
width
}
px`
:
'
fit-content
'
,
maxWidth
:
'
100%
'
,
textAlign
:
'
center
'
};
};
}
}
return
{
return
{
...
baseStyle
,
...
sharedMargin
,
lineHeight
:
0
,
...(
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
'
&&
{
display
:
'
inline-block
'
,
display
:
'
inline-block
'
,
float
:
'
none
'
,
float
:
align
===
'
left
'
?
'
left
'
:
'
right
'
,
margin
:
'
0 0.2rem
'
,
...(
align
===
'
left
'
?
{
marginRight
:
'
1rem
'
}
:
{
marginLeft
:
'
1rem
'
}),
verticalAlign
:
'
middle
'
,
width
:
w
,
maxWidth
:
'
100%
'
,
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
:
'
30
px
'
,
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
,
left
:
'
50%
'
,
transform
:
'
translateX(-50%)
'
,
transform
:
'
translateX(-50%)
'
,
backgroundColor
:
'
white
'
,
backgroundColor
:
'
white
'
,
boxShadow
:
'
0 2px 8px rgba(0,0,0,0.15)
'
,
boxShadow
:
'
0 2px 8px rgba(0,0,0,0.15)
'
,
borderRadius
:
4
,
borderRadius
:
4
,
padding
:
4
,
zIndex
:
20
,
padding
:
4
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
2
,
whiteSpace
:
'
nowrap
'
,
zIndex
:
20
,
display
:
'
flex
'
}}
>
}}
>
{
ALIGN_OPTIONS
.
map
(
a
lign
=>
(
{
ALIGN_OPTIONS
.
map
(
a
=>
(
<
button
<
button
type
=
"
button
"
type
=
"
button
"
key
=
{
align
}
key
=
{
a
}
onClick
=
{()
=>
handleAlign
(
align
)}
title
=
{
a
===
'
left
'
?
'
По левому краю
'
:
a
===
'
center
'
?
'
По центру
'
:
'
По правому краю
'
}
onClick
=
{()
=>
handleAlign
(
a
)}
style
=
{{
style
=
{{
margin
:
'
0 2px
'
,
padding
:
'
4px 6px
'
,
padding
:
'
10px 8px
'
,
background
:
node
.
attrs
.
align
===
a
?
'
#e6f7ff
'
:
'
transparent
'
,
background
:
node
.
attrs
.
align
===
align
?
'
#e6f7ff
'
:
'
transparent
'
,
border
:
`1px solid
${
node
.
attrs
.
align
===
a
?
BORDER_COLOR
:
'
#d9d9d9
'
}
`
,
border
:
'
1px solid #d9d9d9
'
,
borderRadius
:
2
,
cursor
:
'
pointer
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
borderRadius
:
2
,
cursor
:
'
pointer
'
}}
}}
>
>
{
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
>
<
/button
>
))}
))}
<
/div
>
{
node
.
attrs
.
align
!==
'
center
'
&&
(
)}
<>
<
div
style
=
{{
width
:
1
,
background
:
'
#d9d9d9
'
,
alignSelf
:
'
stretch
'
,
margin
:
'
0 2px
'
}}
/
>
<
button
<
button
type
=
"
button
"
type
=
"
button
"
title
=
{
node
.
attrs
.
wrap
?
'
Обтекание включено
'
:
'
Обтекание выключено
'
}
onClick
=
{(
e
)
=>
{
onClick
=
{(
e
)
=>
{
e
.
stopPropagation
();
e
.
stopPropagation
();
setShowAlignMenu
(
!
showAlignMenu
);
safeUpdateAttributes
({
wrap
:
!
node
.
attrs
.
wrap
});
requestAnimationFrame
(()
=>
{
try
{
const
pos
=
getPos
?.();
if
(
typeof
pos
===
'
number
'
)
editor
.
commands
.
setNodeSelection
(
pos
);
}
catch
{}
});
}}
}}
style
=
{{
style
=
{{
position
:
'
absolute
'
,
padding
:
'
4px 6px
'
,
top
:
-
30
,
background
:
node
.
attrs
.
wrap
?
'
#e6f7ff
'
:
'
transparent
'
,
left
:
'
calc(50% - 6px)
'
,
border
:
`1px solid
${
node
.
attrs
.
wrap
?
BORDER_COLOR
:
'
#d9d9d9
'
}
`
,
transform
:
'
translateX(-50%)
'
,
borderRadius
:
2
,
cursor
:
'
pointer
'
,
fontSize
:
11
,
backgroundColor
:
'
white
'
,
display
:
'
flex
'
,
alignItems
:
'
center
'
,
gap
:
3
,
border
:
`1px solid
${
BORDER_COLOR
}
`
,
borderRadius
:
4
,
padding
:
'
8px 8px
'
,
cursor
:
'
pointer
'
,
fontSize
:
12
,
zIndex
:
10
}}
}}
>
>
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
>
<
/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
:
'
10
px
'
}}
><
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,11 +679,15 @@ export const InteractiveImage = Node.create({
...
@@ -763,11 +679,15 @@ 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
),
}
}
]
]
...
...
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