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
c8be1c9b
Commit
c8be1c9b
authored
Jul 04, 2025
by
Яков
Browse files
fix
parent
5b6b2868
Changes
4
Hide whitespace changes
Inline
Side-by-side
package.json
View file @
c8be1c9b
{
{
"name"
:
"react-ag-qeditor"
,
"name"
:
"react-ag-qeditor"
,
"version"
:
"1.0.9
4
"
,
"version"
:
"1.0.9
5
"
,
"description"
:
"WYSIWYG html editor"
,
"description"
:
"WYSIWYG html editor"
,
"author"
:
"atma"
,
"author"
:
"atma"
,
"license"
:
"
MIT
"
,
"license"
:
"
MIT
"
,
...
...
src/QEditor.jsx
View file @
c8be1c9b
...
@@ -524,7 +524,9 @@ const QEditor = ({
...
@@ -524,7 +524,9 @@ const QEditor = ({
},
},
onUploadError
:
(
error
)
=>
{
onUploadError
:
(
error
)
=>
{
console
.
error
(
'
Upload error:
'
,
error
);
console
.
error
(
'
Upload error:
'
,
error
);
}
},
minDragDistance
:
10
,
// Можно настроить под свои нужды
dragPreviewOpacity
:
0.3
// Настройка прозрачности
})
})
],
],
content
:
value
,
content
:
value
,
...
...
src/extensions/DragAndDrop.js
View file @
c8be1c9b
// DragAndDrop.js
import
{
Extension
}
from
'
@tiptap/core
'
;
import
{
Extension
}
from
'
@tiptap/core
'
;
import
{
Plugin
,
PluginKey
}
from
'
prosemirror-state
'
;
import
{
Plugin
,
PluginKey
}
from
'
prosemirror-state
'
;
import
axios
from
'
axios
'
;
import
axios
from
'
axios
'
;
import
{
NodeSelection
}
from
'
prosemirror-state
'
;
export
const
DragAndDrop
=
Extension
.
create
({
export
const
DragAndDrop
=
Extension
.
create
({
name
:
'
dragAndDrop
'
,
name
:
'
dragAndDrop
'
,
addOptions
()
{
addOptions
()
{
return
{
return
{
uploadUrl
:
''
,
// URL для загрузки файлов
uploadUrl
:
''
,
uploadHandler
:
null
,
// Кастомный обработчик загрузки
uploadHandler
:
null
,
allowedFileTypes
:
[
// Разрешенные MIME-типы
allowedFileTypes
:
[
'
image/jpeg
'
,
'
image/jpeg
'
,
'
image/png
'
,
'
image/gif
'
,
'
image/webp
'
,
'
image/png
'
,
'
video/mp4
'
,
'
video/webm
'
,
'
audio/mpeg
'
'
image/gif
'
,
'
image/webp
'
,
'
video/mp4
'
,
'
video/webm
'
,
'
audio/mpeg
'
],
],
headers
:
{},
// Дополнительные заголовки
headers
:
{},
onUploadError
:
(
error
)
=>
console
.
error
(
'
Upload failed:
'
,
error
),
onUploadError
:
(
error
)
=>
console
.
error
(
'
Upload failed:
'
,
error
),
onUploadSuccess
:
()
=>
{}
// Колбек при успешной загрузке
onUploadSuccess
:
()
=>
{},
minDragDistance
:
10
,
dragPreviewOpacity
:
0.3
};
};
},
},
addProseMirrorPlugins
()
{
addProseMirrorPlugins
()
{
const
extension
=
this
;
const
extension
=
this
;
// Проверяем, является ли файл реальным (не из Word)
const
dragState
=
{
active
:
false
,
sourceNode
:
null
,
sourcePos
:
null
,
nodeId
:
null
};
const
isRealFile
=
(
file
)
=>
{
const
isRealFile
=
(
file
)
=>
{
if
(
!
file
||
!
file
.
type
)
return
false
;
if
(
!
file
||
!
file
.
type
)
return
false
;
// Игнорируем специфичные для Word типы
const
wordTypes
=
[
const
wordTypes
=
[
'
application/x-mso
'
,
'
application/x-mso
'
,
'
ms-office
'
,
'
wordprocessingml
'
,
'
ms-office
'
,
'
application/rtf
'
,
'
text/rtf
'
,
'
text/html
'
'
wordprocessingml
'
,
'
application/rtf
'
,
'
text/rtf
'
,
'
text/html
'
];
];
return
!
wordTypes
.
some
(
type
=>
file
.
type
.
includes
(
type
))
&&
if
(
wordTypes
.
some
(
type
=>
file
.
type
.
includes
(
type
)))
{
extension
.
options
.
allowedFileTypes
.
includes
(
file
.
type
);
return
false
;
}
// Проверяем разрешенные типы
return
extension
.
options
.
allowedFileTypes
.
includes
(
file
.
type
);
};
};
// Определяем тип ноды для вставки
const
getNodeType
=
(
mimeType
)
=>
{
const
getNodeType
=
(
mimeType
)
=>
{
if
(
mimeType
.
startsWith
(
'
image/
'
))
return
'
image
'
;
if
(
mimeType
.
startsWith
(
'
image/
'
))
return
'
image
'
;
if
(
mimeType
.
startsWith
(
'
video/
'
))
return
'
video
'
;
if
(
mimeType
.
startsWith
(
'
video/
'
))
return
'
video
'
;
...
@@ -57,97 +50,167 @@ export const DragAndDrop = Extension.create({
...
@@ -57,97 +50,167 @@ export const DragAndDrop = Extension.create({
return
null
;
return
null
;
};
};
// Обработчик загрузки файла
const
handleFileUpload
=
async
(
file
,
view
,
position
)
=>
{
const
handleFileUpload
=
async
(
file
,
view
,
position
)
=>
{
try
{
if
(
!
extension
.
options
.
uploadUrl
&&
!
extension
.
options
.
uploadHandler
)
{
let
fileUrl
;
console
.
error
(
'
No upload URL or handler provided
'
);
return
;
}
const
nodeType
=
getNodeType
(
file
.
type
);
if
(
!
nodeType
)
return
;
try
{
let
result
;
if
(
extension
.
options
.
uploadHandler
)
{
if
(
extension
.
options
.
uploadHandler
)
{
fileUrl
=
await
extension
.
options
.
uploadHandler
(
file
);
result
=
await
extension
.
options
.
uploadHandler
(
file
);
}
else
{
}
else
{
const
formData
=
new
FormData
();
const
formData
=
new
FormData
();
formData
.
append
(
'
file
'
,
file
);
formData
.
append
(
'
file
'
,
file
);
const
response
=
await
axios
.
post
(
extension
.
options
.
uploadUrl
,
formData
,
{
const
response
=
await
axios
.
post
(
headers
:
extension
.
options
.
headers
global
.
uploadUrl
,
});
formData
,
result
=
response
.
data
;
{
headers
:
{
'
Content-Type
'
:
'
multipart/form-data
'
,
...
extension
.
options
.
headers
,
},
}
);
if
(
!
response
.
data
?.
file_path
)
{
throw
new
Error
(
'
Invalid server response
'
);
}
fileUrl
=
response
.
data
.
file_path
;
}
}
if
(
!
fileUrl
)
return
;
if
(
!
result
?.
url
)
throw
new
Error
(
'
Invalid response from server
'
)
;
const
{
state
,
dispatch
}
=
view
;
const
node
=
view
.
state
.
schema
.
nodes
[
nodeType
].
create
({
const
type
=
getNodeType
(
file
.
type
);
src
:
result
.
url
,
if
(
!
type
)
return
;
alt
:
file
.
name
,
title
:
file
.
name
,
'
data-node-id
'
:
`img-
${
Date
.
now
()}
-
${
Math
.
random
().
toString
(
36
).
substr
(
2
,
9
)}
`
});
const
node
=
state
.
schema
.
nodes
[
type
].
create
({
src
:
fileUrl
}
);
const
tr
=
view
.
state
.
tr
.
insert
(
position
||
view
.
state
.
selection
.
from
,
node
);
dispatch
(
state
.
tr
.
insert
(
position
,
node
)
);
view
.
dispatch
(
tr
);
extension
.
options
.
onUploadSuccess
(
fileUrl
);
extension
.
options
.
onUploadSuccess
(
result
);
}
catch
(
error
)
{
}
catch
(
error
)
{
console
.
error
(
'
Upload failed:
'
,
error
);
extension
.
options
.
onUploadError
(
error
);
extension
.
options
.
onUploadError
(
error
);
}
}
};
};
// Обработчик вставки (paste)
const
handlePaste
=
(
view
,
event
)
=>
{
const
handlePaste
=
(
view
,
event
)
=>
{
const
items
=
Array
.
from
(
event
.
clipboardData
?.
items
||
[]);
const
items
=
Array
.
from
(
event
.
clipboardData
?.
items
||
[]);
const
htmlData
=
event
.
clipboardData
.
getData
(
'
text/html
'
);
if
(
event
.
clipboardData
.
getData
(
'
text/html
'
).
includes
(
'
urn:schemas-microsoft-com
'
))
{
// Если есть HTML и это контент из Word - пропускаем
if
(
htmlData
.
includes
(
'
urn:schemas-microsoft-com
'
))
{
return
false
;
return
false
;
}
}
// Фильтруем только реальные файлы
const
file
=
items
.
find
(
item
=>
isRealFile
(
item
.
getAsFile
()))?.
getAsFile
();
const
files
=
items
if
(
!
file
)
return
false
;
.
filter
(
item
=>
item
.
kind
===
'
file
'
)
.
map
(
item
=>
item
.
getAsFile
())
.
filter
(
file
=>
file
&&
isRealFile
(
file
));
if
(
files
.
length
===
0
)
return
false
;
event
.
preventDefault
();
event
.
preventDefault
();
const
pos
=
view
.
state
.
selection
.
from
;
handleFileUpload
(
file
,
view
);
files
.
forEach
(
file
=>
{
handleFileUpload
(
file
,
view
,
pos
);
});
return
true
;
return
true
;
};
};
// Обработчик перетаскивания (drop)
const
handleDragStart
=
(
event
)
=>
{
const
handleDrop
=
(
view
,
event
)
=>
{
const
target
=
event
.
target
;
const
files
=
Array
.
from
(
event
.
dataTransfer
?.
files
||
[])
if
(
!
target
.
matches
(
'
.ProseMirror img, .ProseMirror video, .ProseMirror audio
'
))
return
;
.
filter
(
file
=>
isRealFile
(
file
));
if
(
files
.
length
===
0
)
return
false
;
const
view
=
extension
.
editor
.
view
;
const
pos
=
view
.
posAtDOM
(
target
,
0
);
if
(
pos
===
undefined
||
pos
===
null
)
return
;
const
node
=
view
.
state
.
doc
.
nodeAt
(
pos
);
if
(
!
node
||
!
[
'
image
'
,
'
video
'
,
'
audio
'
].
includes
(
node
.
type
.
name
))
return
;
dragState
.
active
=
true
;
dragState
.
sourceNode
=
node
;
dragState
.
sourcePos
=
pos
;
dragState
.
nodeId
=
node
.
attrs
[
'
data-node-id
'
];
const
dragImage
=
new
Image
();
dragImage
.
src
=
'
data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7
'
;
event
.
dataTransfer
.
setData
(
'
text/plain
'
,
'
tiptap-drag
'
);
event
.
dataTransfer
.
effectAllowed
=
'
copyMove
'
;
event
.
dataTransfer
.
setDragImage
(
dragImage
,
0
,
0
);
target
.
style
.
opacity
=
extension
.
options
.
dragPreviewOpacity
;
};
const
handleDrop
=
(
event
)
=>
{
event
.
preventDefault
();
event
.
preventDefault
();
const
pos
=
view
.
posAtCoords
({
event
.
stopPropagation
();
left
:
event
.
clientX
,
top
:
event
.
clientY
,
})?.
pos
;
if
(
!
pos
)
return
false
;
const
view
=
extension
.
editor
.
view
;
const
coords
=
{
left
:
event
.
clientX
,
top
:
event
.
clientY
};
const
dropPos
=
view
.
posAtCoords
(
coords
)?.
pos
;
files
.
forEach
(
file
=>
{
if
(
event
.
dataTransfer
)
{
handleFileUpload
(
file
,
view
,
pos
);
event
.
dataTransfer
.
clearData
?.();
}
if
(
!
dragState
.
active
)
{
const
files
=
Array
.
from
(
event
.
dataTransfer
?.
files
||
[])
.
filter
(
file
=>
isRealFile
(
file
));
if
(
files
.
length
===
0
)
return
;
handleFileUpload
(
files
[
0
],
view
,
dropPos
||
view
.
state
.
selection
.
from
);
return
;
}
if
(
dropPos
===
undefined
||
dropPos
===
null
)
{
resetDragState
();
return
;
}
if
(
Math
.
abs
(
dropPos
-
dragState
.
sourcePos
)
<
extension
.
options
.
minDragDistance
)
{
resetDragState
();
return
;
}
const
{
state
}
=
view
;
let
actualPos
=
null
;
let
actualNode
=
null
;
state
.
doc
.
descendants
((
node
,
pos
)
=>
{
if
(
node
.
attrs
[
'
data-node-id
'
]
===
dragState
.
nodeId
)
{
actualNode
=
node
;
actualPos
=
pos
;
return
false
;
}
});
});
return
true
;
if
(
!
actualNode
||
actualPos
===
null
)
{
resetDragState
();
return
;
}
const
tr
=
state
.
tr
.
delete
(
actualPos
,
actualPos
+
actualNode
.
nodeSize
);
const
insertPos
=
dropPos
>
actualPos
?
dropPos
-
actualNode
.
nodeSize
:
dropPos
;
tr
.
insert
(
insertPos
,
actualNode
);
view
.
dispatch
(
tr
);
// Обновим выделение на вставленный узел
const
resolvedPos
=
view
.
state
.
doc
.
resolve
(
insertPos
);
const
nodeSelection
=
NodeSelection
.
create
(
view
.
state
.
doc
,
resolvedPos
.
pos
);
view
.
dispatch
(
view
.
state
.
tr
.
setSelection
(
nodeSelection
));
resetDragState
();
};
const
resetDragState
=
()
=>
{
document
.
querySelectorAll
(
'
.ProseMirror img, .ProseMirror video, .ProseMirror audio
'
)
.
forEach
(
el
=>
el
.
style
.
opacity
=
'
1
'
);
dragState
.
active
=
false
;
dragState
.
sourceNode
=
null
;
dragState
.
sourcePos
=
null
;
dragState
.
nodeId
=
null
;
};
const
setupGlobalListeners
=
()
=>
{
document
.
addEventListener
(
'
dragstart
'
,
handleDragStart
,
true
);
document
.
addEventListener
(
'
drop
'
,
handleDrop
,
true
);
document
.
addEventListener
(
'
dragend
'
,
resetDragState
,
true
);
return
()
=>
{
document
.
removeEventListener
(
'
dragstart
'
,
handleDragStart
,
true
);
document
.
removeEventListener
(
'
drop
'
,
handleDrop
,
true
);
document
.
removeEventListener
(
'
dragend
'
,
resetDragState
,
true
);
};
};
};
return
[
return
[
...
@@ -155,9 +218,41 @@ export const DragAndDrop = Extension.create({
...
@@ -155,9 +218,41 @@ export const DragAndDrop = Extension.create({
key
:
new
PluginKey
(
'
dragAndDrop
'
),
key
:
new
PluginKey
(
'
dragAndDrop
'
),
props
:
{
props
:
{
handlePaste
,
handlePaste
,
handleDrop
,
handleDOMEvents
:
{
dragstart
:
()
=>
true
,
drop
:
()
=>
true
,
dragover
:
(
view
,
event
)
=>
{
if
(
dragState
.
active
)
{
event
.
preventDefault
();
event
.
dataTransfer
.
dropEffect
=
'
move
'
;
return
true
;
}
const
items
=
event
.
dataTransfer
?.
items
;
if
(
items
&&
Array
.
from
(
items
).
some
(
item
=>
isRealFile
(
item
.
getAsFile
())))
{
event
.
preventDefault
();
event
.
dataTransfer
.
dropEffect
=
'
copy
'
;
return
true
;
}
return
false
;
},
dragenter
:
(
view
,
event
)
=>
{
if
(
dragState
.
active
||
(
event
.
dataTransfer
?.
items
&&
Array
.
from
(
event
.
dataTransfer
.
items
).
some
(
item
=>
isRealFile
(
item
.
getAsFile
())))
)
{
event
.
preventDefault
();
return
true
;
}
return
false
;
}
}
},
},
view
:
()
=>
({
destroy
:
setupGlobalListeners
()
})
}),
}),
];
];
}
,
}
});
});
src/extensions/Image.jsx
View file @
c8be1c9b
...
@@ -6,51 +6,30 @@ const MIN_WIDTH = 60;
...
@@ -6,51 +6,30 @@ 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
'
,
'
text
'
];
const
ResizableImageTemplate
=
({
node
,
updateAttributes
,
editor
,
getPos
})
=>
{
const
ResizableImageTemplate
=
({
node
,
updateAttributes
,
editor
,
getPos
,
selected
})
=>
{
const
imgRef
=
useRef
(
null
);
const
imgRef
=
useRef
(
null
);
const
wrapperRef
=
useRef
(
null
);
const
wrapperRef
=
useRef
(
null
);
const
[
editing
,
setEditing
]
=
useState
(
false
);
const
[
showAlignMenu
,
setShowAlignMenu
]
=
useState
(
false
);
const
[
showAlignMenu
,
setShowAlignMenu
]
=
useState
(
false
);
const
isInitialized
=
useRef
(
false
);
const
isInitialized
=
useRef
(
false
);
const
resizeData
=
useRef
({
startWidth
:
0
,
startHeight
:
0
,
startX
:
0
,
startY
:
0
,
aspectRatio
:
1
});
// Генерация уникального ID при создании
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
!
editor
?.
isEditable
||
typeof
getPos
!==
'
function
'
)
return
;
if
(
!
node
.
attrs
[
'
data-node-id
'
])
{
updateAttributes
({
const
insertZeroWidthSpace
=
()
=>
{
'
data-node-id
'
:
`img-
${
Date
.
now
()}
-
${
Math
.
random
().
toString
(
36
).
substr
(
2
,
9
)}
`
try
{
});
const
pos
=
getPos
();
}
if
(
typeof
pos
!==
'
number
'
||
pos
<
0
)
return
;
},
[
node
.
attrs
[
'
data-node-id
'
],
updateAttributes
]);
const
doc
=
editor
.
state
.
doc
;
const
insertPos
=
pos
+
1
;
if
(
insertPos
>=
doc
.
content
.
size
)
return
;
const
nextNode
=
doc
.
nodeAt
(
insertPos
);
if
(
nextNode
?.
textContent
===
'
\
u200B
'
)
return
;
setTimeout
(()
=>
{
useEffect
(()
=>
{
if
(
editor
.
isDestroyed
)
return
;
const
handleClickOutside
=
(
event
)
=>
{
editor
.
commands
.
insertContentAt
(
insertPos
,
{
if
(
wrapperRef
.
current
&&
!
wrapperRef
.
current
.
contains
(
event
.
target
)
&&
selected
)
{
type
:
'
text
'
,
editor
.
commands
.
setNodeSelection
(
getPos
());
text
:
'
\
u200B
'
});
},
50
);
}
catch
(
error
)
{
console
.
warn
(
'
Error inserting zero-width space:
'
,
error
);
}
}
};
};
document
.
addEventListener
(
'
mousedown
'
,
handleClickOutside
);
const
timer
=
setTimeout
(
insertZeroWidthSpace
,
100
);
return
()
=>
document
.
removeEventListener
(
'
mousedown
'
,
handleClickOutside
);
return
()
=>
clearTimeout
(
timer
);
},
[
selected
,
editor
,
getPos
]);
},
[
editor
,
getPos
]);
useEffect
(()
=>
{
useEffect
(()
=>
{
if
(
!
imgRef
.
current
||
isInitialized
.
current
)
return
;
if
(
!
imgRef
.
current
||
isInitialized
.
current
)
return
;
...
@@ -59,11 +38,11 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
...
@@ -59,11 +38,11 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
try
{
try
{
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
),
height
:
Math
.
round
(
height
)
height
:
Math
.
round
(
height
),
'
data-node-id
'
:
node
.
attrs
[
'
data-node-id
'
]
||
Math
.
random
().
toString
(
36
).
substr
(
2
,
9
)
});
});
isInitialized
.
current
=
true
;
isInitialized
.
current
=
true
;
}
}
...
@@ -79,42 +58,21 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
...
@@ -79,42 +58,21 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
}
}
return
()
=>
{
return
()
=>
{
if
(
imgRef
.
current
)
{
if
(
imgRef
.
current
)
imgRef
.
current
.
onload
=
null
;
imgRef
.
current
.
onload
=
null
;
}
};
};
},
[
node
.
attrs
.
width
,
node
.
attrs
.
height
,
updateAttributes
]);
},
[
node
.
attrs
.
width
,
node
.
attrs
.
height
,
updateAttributes
,
node
.
attrs
[
'
data-node-id
'
]
]);
const
handleResizeStart
=
(
direction
)
=>
(
e
)
=>
{
const
handleResizeStart
=
(
direction
)
=>
(
e
)
=>
{
e
.
preventDefault
();
e
.
preventDefault
();
e
.
stopPropagation
();
e
.
stopPropagation
();
const
nodePos
=
typeof
getPos
===
'
function
'
?
getPos
()
:
null
;
const
startWidth
=
node
.
attrs
.
width
||
imgRef
.
current
.
naturalWidth
;
const
currentWidth
=
node
.
attrs
.
width
||
imgRef
.
current
.
naturalWidth
;
const
startHeight
=
node
.
attrs
.
height
||
imgRef
.
current
.
naturalHeight
;
const
currentHeight
=
node
.
attrs
.
height
||
imgRef
.
current
.
naturalHeight
;
const
aspectRatio
=
startWidth
/
startHeight
;
const
startX
=
e
.
clientX
;
resizeData
.
current
=
{
const
startY
=
e
.
clientY
;
startWidth
:
currentWidth
,
startHeight
:
currentHeight
,
startX
:
e
.
clientX
,
startY
:
e
.
clientY
,
aspectRatio
:
currentWidth
/
currentHeight
,
direction
,
nodePos
};
const
onMouseMove
=
(
e
)
=>
{
const
onMouseMove
=
(
e
)
=>
{
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
;
...
@@ -142,38 +100,13 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
...
@@ -142,38 +100,13 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
}
}
}
}
// Сохраняем новые размеры в resizeData для использования в onMouseUp
updateAttributes
({
width
:
newWidth
,
height
:
newHeight
});
resizeData
.
current
.
currentWidth
=
newWidth
;
resizeData
.
current
.
currentHeight
=
newHeight
;
updateAttributes
({
width
:
newWidth
,
height
:
newHeight
});
};
};
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
:
resizeData
.
current
.
currentWidth
,
height
:
resizeData
.
current
.
currentHeight
});
}
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
);
...
@@ -183,48 +116,31 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
...
@@ -183,48 +116,31 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
const
handleAlign
=
(
align
)
=>
{
const
handleAlign
=
(
align
)
=>
{
updateAttributes
({
align
});
updateAttributes
({
align
});
setShowAlignMenu
(
false
);
setShowAlignMenu
(
false
);
setTimeout
(()
=>
editor
.
commands
.
focus
()
,
100
)
;
editor
.
commands
.
focus
();
};
};
const
getWrapperStyle
=
()
=>
{
const
getWrapperStyle
=
()
=>
({
const
baseStyle
=
{
display
:
'
inline-block
'
,
lineHeight
:
0
,
margin
:
'
0.5rem 0
'
,
position
:
'
relative
'
,
outline
:
selected
?
`1px dashed
${
BORDER_COLOR
}
`
:
'
none
'
,
verticalAlign
:
'
top
'
,
...(
node
.
attrs
.
align
===
'
left
'
&&
{
float
:
'
left
'
,
marginRight
:
'
1rem
'
}),
...(
node
.
attrs
.
align
===
'
right
'
&&
{
float
:
'
right
'
,
marginLeft
:
'
1rem
'
}),
...(
node
.
attrs
.
align
===
'
center
'
&&
{
display
:
'
block
'
,
marginLeft
:
'
auto
'
,
marginRight
:
'
auto
'
,
textAlign
:
'
center
'
}),
...(
node
.
attrs
.
align
===
'
text
'
&&
{
display
:
'
inline-block
'
,
display
:
'
inline-block
'
,
lineHeight
:
0
,
float
:
'
none
'
,
margin
:
'
0.5rem 0
'
,
margin
:
'
0 0.2rem
'
,
position
:
'
relative
'
,
verticalAlign
:
'
middle
'
outline
:
editing
?
`1px dashed
${
BORDER_COLOR
}
`
:
'
none
'
,
})
verticalAlign
:
'
top
'
});
};
switch
(
node
.
attrs
.
align
)
{
case
'
left
'
:
return
{
...
baseStyle
,
float
:
'
left
'
,
marginRight
:
'
1rem
'
};
case
'
right
'
:
return
{
...
baseStyle
,
float
:
'
right
'
,
marginLeft
:
'
1rem
'
};
case
'
center
'
:
return
{
...
baseStyle
,
display
:
'
block
'
,
marginLeft
:
'
auto
'
,
marginRight
:
'
auto
'
,
textAlign
:
'
center
'
};
case
'
text
'
:
return
{
...
baseStyle
,
display
:
'
inline-block
'
,
float
:
'
none
'
,
margin
:
'
0 0.2rem
'
,
verticalAlign
:
'
middle
'
};
case
'
wrap-left
'
:
return
{
...
baseStyle
,
float
:
'
left
'
,
margin
:
'
0 1rem 1rem 0
'
,
shapeOutside
:
'
margin-box
'
};
case
'
wrap-right
'
:
return
{
...
baseStyle
,
float
:
'
right
'
,
margin
:
'
0 0 1rem 1rem
'
,
shapeOutside
:
'
margin-box
'
};
default
:
return
baseStyle
;
}
};
const
getImageStyle
=
()
=>
({
const
getImageStyle
=
()
=>
({
width
:
node
.
attrs
.
width
?
`
${
node
.
attrs
.
width
}
px`
:
'
auto
'
,
width
:
node
.
attrs
.
width
?
`
${
node
.
attrs
.
width
}
px`
:
'
auto
'
,
...
@@ -244,7 +160,7 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
...
@@ -244,7 +160,7 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
ref
=
{
wrapperRef
}
ref
=
{
wrapperRef
}
onClick
=
{
(
e
)
=>
{
onClick
=
{
(
e
)
=>
{
e
.
stopPropagation
();
e
.
stopPropagation
();
setEditing
(
true
);
editor
.
commands
.
setNodeSelection
(
getPos
()
);
}
}
}
}
contentEditable
=
{
false
}
contentEditable
=
{
false
}
data
-
image
-
wrapper
data
-
image
-
wrapper
...
@@ -252,6 +168,7 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
...
@@ -252,6 +168,7 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
<
img
<
img
{
...
node
.
attrs
}
{
...
node
.
attrs
}
ref
=
{
imgRef
}
ref
=
{
imgRef
}
draggable
=
{
true
}
// обязательно true для работы dragstart
style
=
{
getImageStyle
()
}
style
=
{
getImageStyle
()
}
onLoad
=
{
()
=>
{
onLoad
=
{
()
=>
{
if
(
imgRef
.
current
&&
!
isInitialized
.
current
)
{
if
(
imgRef
.
current
&&
!
isInitialized
.
current
)
{
...
@@ -259,14 +176,15 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
...
@@ -259,14 +176,15 @@ const ResizableImageTemplate = ({ node, updateAttributes, editor, getPos }) => {
const
height
=
imgRef
.
current
.
naturalHeight
;
const
height
=
imgRef
.
current
.
naturalHeight
;
updateAttributes
({
updateAttributes
({
width
:
Math
.
round
(
width
),
width
:
Math
.
round
(
width
),
height
:
Math
.
round
(
height
)
height
:
Math
.
round
(
height
),
'
data-node-id
'
:
node
.
attrs
[
'
data-node-id
'
]
||
Math
.
random
().
toString
(
36
).
substr
(
2
,
9
)
});
});
isInitialized
.
current
=
true
;
isInitialized
.
current
=
true
;
}
}
}
}
}
}
/>
/>
{
editing
&&
(
{
selected
&&
(
<
Fragment
>
<
Fragment
>
{
[
'
nw
'
,
'
ne
'
,
'
sw
'
,
'
se
'
].
map
(
dir
=>
(
{
[
'
nw
'
,
'
ne
'
,
'
sw
'
,
'
se
'
].
map
(
dir
=>
(
<
div
<
div
...
@@ -351,103 +269,48 @@ const ResizableImageExtension = TipTapImage.extend({
...
@@ -351,103 +269,48 @@ const ResizableImageExtension = TipTapImage.extend({
addAttributes
()
{
addAttributes
()
{
return
{
return
{
...
this
.
parent
?.(),
...
this
.
parent
?.(),
src
:
{
src
:
{
default
:
null
},
default
:
null
,
alt
:
{
default
:
null
},
},
title
:
{
default
:
null
},
alt
:
{
default
:
null
,
},
title
:
{
default
:
null
,
},
width
:
{
width
:
{
default
:
null
,
default
:
null
,
parseHTML
:
element
=>
{
parseHTML
:
element
=>
parseInt
(
element
.
getAttribute
(
'
width
'
),
10
)
||
null
,
const
width
=
element
.
getAttribute
(
'
width
'
);
return
width
?
parseInt
(
width
,
10
)
:
null
;
},
renderHTML
:
attributes
=>
attributes
.
width
?
{
width
:
attributes
.
width
}
:
{}
renderHTML
:
attributes
=>
attributes
.
width
?
{
width
:
attributes
.
width
}
:
{}
},
},
height
:
{
height
:
{
default
:
null
,
default
:
null
,
parseHTML
:
element
=>
{
parseHTML
:
element
=>
parseInt
(
element
.
getAttribute
(
'
height
'
),
10
)
||
null
,
const
height
=
element
.
getAttribute
(
'
height
'
);
return
height
?
parseInt
(
height
,
10
)
:
null
;
},
renderHTML
:
attributes
=>
attributes
.
height
?
{
height
:
attributes
.
height
}
:
{}
renderHTML
:
attributes
=>
attributes
.
height
?
{
height
:
attributes
.
height
}
:
{}
},
},
align
:
{
align
:
{
default
:
'
left
'
,
default
:
'
left
'
,
parseHTML
:
element
=>
element
.
getAttribute
(
'
data-align
'
)
||
'
left
'
,
parseHTML
:
element
=>
element
.
getAttribute
(
'
data-align
'
)
||
'
left
'
,
renderHTML
:
attributes
=>
({
'
data-align
'
:
attributes
.
align
})
renderHTML
:
attributes
=>
({
'
data-align
'
:
attributes
.
align
})
},
'
data-node-id
'
:
{
default
:
null
,
parseHTML
:
element
=>
element
.
getAttribute
(
'
data-node-id
'
),
renderHTML
:
attributes
=>
({
'
data-node-id
'
:
attributes
[
'
data-node-id
'
]
})
}
}
};
};
},
},
renderHTML
({
HTMLAttributes
})
{
const
align
=
HTMLAttributes
.
align
||
HTMLAttributes
[
'
data-align
'
]
||
'
left
'
;
const
floatValue
=
align
.
startsWith
(
'
wrap-
'
)
?
align
.
split
(
'
-
'
)[
1
]
:
[
'
left
'
,
'
right
'
].
includes
(
align
)
?
align
:
'
none
'
;
let
marginValue
;
switch
(
align
)
{
case
'
left
'
:
case
'
wrap-left
'
:
marginValue
=
'
0 1rem 1rem 0
'
;
break
;
case
'
right
'
:
case
'
wrap-right
'
:
marginValue
=
'
0 0 1rem 1rem
'
;
break
;
case
'
center
'
:
marginValue
=
'
0.5rem auto
'
;
break
;
case
'
text
'
:
marginValue
=
'
0 0.2rem
'
;
break
;
default
:
marginValue
=
'
0
'
;
}
return
[
'
span
'
,
{
'
data-type
'
:
'
resizable-image
'
,
'
data-image-wrapper
'
:
true
,
'
data-align
'
:
align
,
style
:
`
display:
${
align
===
'
center
'
?
'
block
'
:
'
inline-block
'
}
;
float:
${
floatValue
}
;
margin:
${
marginValue
}
;
shape-outside:
${
align
.
startsWith
(
'
wrap-
'
)
?
'
margin-box
'
:
'
none
'
}
;
vertical-align:
${
align
===
'
text
'
?
'
middle
'
:
'
top
'
}
;
position: relative;
${
align
===
'
center
'
?
'
width: 100%; text-align: center;
'
:
''
}
`
},
[
'
img
'
,
{
...
HTMLAttributes
,
style
:
`
${
HTMLAttributes
.
style
||
''
}
;
display:
${
align
===
'
center
'
?
'
inline-block
'
:
'
block
'
}
;
margin:
${
align
===
'
center
'
?
'
0 auto
'
:
'
0
'
}
;
max-width: 100%;
height: auto;
vertical-align:
${
align
===
'
text
'
?
'
middle
'
:
'
top
'
}
;
`
,
'
data-align
'
:
align
}]];
},
addNodeView
()
{
addNodeView
()
{
return
ReactNodeViewRenderer
(
ResizableImageTemplate
);
return
ReactNodeViewRenderer
(
ResizableImageTemplate
);
},
addKeyboardShortcuts
()
{
return
{
'
Mod-ArrowLeft
'
:
()
=>
this
.
editor
.
commands
.
updateAttributes
(
this
.
type
.
name
,
{
align
:
'
left
'
}),
'
Mod-ArrowRight
'
:
()
=>
this
.
editor
.
commands
.
updateAttributes
(
this
.
type
.
name
,
{
align
:
'
right
'
}),
'
Mod-ArrowDown
'
:
()
=>
this
.
editor
.
commands
.
updateAttributes
(
this
.
type
.
name
,
{
align
:
'
center
'
}),
};
}
}
}).
configure
({
}).
configure
({
inline
:
true
,
inline
:
true
,
group
:
'
inline
'
,
group
:
'
inline
'
,
draggable
:
true
,
draggable
:
true
,
selectable
:
fals
e
selectable
:
tru
e
});
});
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