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
82b42d2a
Commit
82b42d2a
authored
Apr 17, 2026
by
Яков
Browse files
update
parent
c4ae2bf2
Pipeline
#9939
canceled with stages
Changes
2
Pipelines
1
Show whitespace changes
Inline
Side-by-side
package.json
View file @
82b42d2a
{
{
"name"
:
"react-ag-qeditor"
,
"name"
:
"react-ag-qeditor"
,
"version"
:
"1.1.3
3
"
,
"version"
:
"1.1.3
4
"
,
"description"
:
"WYSIWYG html editor"
,
"description"
:
"WYSIWYG html editor"
,
"author"
:
"atma"
,
"author"
:
"atma"
,
"license"
:
"
MIT
"
,
"license"
:
"
MIT
"
,
...
...
src/extensions/DragAndDrop.js
View file @
82b42d2a
...
@@ -219,64 +219,89 @@ export const DragAndDrop = Extension.create({
...
@@ -219,64 +219,89 @@ export const DragAndDrop = Extension.create({
// 3. HTML с внешними картинками (Google Docs, веб-страницы и т.д.)
// 3. HTML с внешними картинками (Google Docs, веб-страницы и т.д.)
if
(
html
)
{
if
(
html
)
{
console
.
log
(
'
[DragAndDrop] paste html types:
'
,
items
.
map
(
i
=>
`
${
i
.
kind
}
:
${
i
.
type
}
`
));
const
tmpDoc
=
new
DOMParser
().
parseFromString
(
html
,
'
text/html
'
);
const
tmpDoc
=
new
DOMParser
().
parseFromString
(
html
,
'
text/html
'
);
const
externalImgs
=
Array
.
from
(
tmpDoc
.
querySelectorAll
(
'
img[src]
'
)).
filter
(
img
=>
{
const
externalImgs
=
Array
.
from
(
tmpDoc
.
querySelectorAll
(
'
img[src]
'
)).
filter
(
img
=>
{
const
src
=
img
.
getAttribute
(
'
src
'
)
||
''
;
const
src
=
img
.
getAttribute
(
'
src
'
)
||
''
;
return
src
.
startsWith
(
'
http://
'
)
||
src
.
startsWith
(
'
https://
'
)
||
src
.
startsWith
(
'
data:
'
);
return
src
.
startsWith
(
'
http://
'
)
||
src
.
startsWith
(
'
https://
'
)
||
src
.
startsWith
(
'
data:
'
);
});
});
console
.
log
(
'
[DragAndDrop] external imgs found:
'
,
externalImgs
.
length
,
externalImgs
.
map
(
i
=>
i
.
getAttribute
(
'
src
'
)?.
slice
(
0
,
80
)));
if
(
externalImgs
.
length
>
0
)
{
if
(
externalImgs
.
length
>
0
)
{
const
imgData
=
externalImgs
.
map
(
img
=>
({
src
:
img
.
getAttribute
(
'
src
'
),
width
:
parseInt
(
img
.
getAttribute
(
'
width
'
))
||
null
,
height
:
parseInt
(
img
.
getAttribute
(
'
height
'
))
||
null
,
}));
// Убираем <img> из HTML — TipTap вставит только текст
externalImgs
.
forEach
(
img
=>
img
.
remove
());
event
.
preventDefault
();
event
.
preventDefault
();
const
cleanHtml
=
tmpDoc
.
body
.
innerHTML
;
// Загружаем из URL или data URI в blob
if
(
cleanHtml
.
trim
())
{
const
fetchBlob
=
async
(
src
)
=>
{
extension
.
editor
.
commands
.
insertContent
(
cleanHtml
);
}
// Асинхронно загружаем каждую картинку на сервер и вставляем
;(
async
()
=>
{
for
(
const
{
src
,
width
,
height
}
of
imgData
)
{
try
{
let
blob
;
if
(
src
.
startsWith
(
'
data:
'
))
{
if
(
src
.
startsWith
(
'
data:
'
))
{
// data URI — конвертируем сразу в blob
const
[
header
,
b64
]
=
src
.
split
(
'
,
'
);
const
[
header
,
b64
]
=
src
.
split
(
'
,
'
);
const
mime
=
(
header
.
match
(
/:
(
.*
?)
;/
)
||
[])[
1
]
||
'
image/png
'
;
const
mime
=
(
header
.
match
(
/:
(
.*
?)
;/
)
||
[])[
1
]
||
'
image/png
'
;
const
binary
=
atob
(
b64
);
const
binary
=
atob
(
b64
);
const
arr
=
new
Uint8Array
(
binary
.
length
);
const
arr
=
new
Uint8Array
(
binary
.
length
);
for
(
let
i
=
0
;
i
<
binary
.
length
;
i
++
)
arr
[
i
]
=
binary
.
charCodeAt
(
i
);
for
(
let
i
=
0
;
i
<
binary
.
length
;
i
++
)
arr
[
i
]
=
binary
.
charCodeAt
(
i
);
blob
=
new
Blob
([
arr
],
{
type
:
mime
});
return
new
Blob
([
arr
],
{
type
:
mime
});
}
else
{
}
// Внешний URL — без credentials (иначе CORS с Access-Control-Allow-Origin: * падает)
const
resp
=
await
fetch
(
src
);
const
resp
=
await
fetch
(
src
);
console
.
log
(
'
[DragAndDrop] fetch
'
,
src
.
slice
(
0
,
80
),
resp
.
status
,
resp
.
headers
.
get
(
'
content-type
'
));
if
(
!
resp
.
ok
)
throw
new
Error
(
`HTTP
${
resp
.
status
}
`
);
if
(
!
resp
.
ok
)
throw
new
Error
(
`HTTP
${
resp
.
status
}
`
);
blob
=
await
resp
.
blob
();
return
resp
.
blob
();
}
};
if
(
!
blob
.
type
.
startsWith
(
'
image/
'
))
{
console
.
warn
(
'
[DragAndDrop] unexpected blob type:
'
,
blob
.
type
);
// Загружаем файл на CDN и возвращаем file_path
continue
;
const
uploadBlob
=
async
(
blob
,
name
)
=>
{
const
file
=
new
File
([
blob
],
name
,
{
type
:
blob
.
type
});
if
(
extension
.
options
.
uploadHandler
)
{
return
extension
.
options
.
uploadHandler
(
file
);
}
}
const
formData
=
new
FormData
();
formData
.
append
(
'
file
'
,
file
);
const
response
=
await
axios
.
post
(
extension
.
options
.
uploadUrl
,
formData
,
{
headers
:
extension
.
options
.
headers
});
return
response
.
data
;
};
;(
async
()
=>
{
// Загружаем все изображения параллельно
const
uploads
=
await
Promise
.
all
(
externalImgs
.
map
(
async
(
img
)
=>
{
const
src
=
img
.
getAttribute
(
'
src
'
);
const
width
=
parseInt
(
img
.
getAttribute
(
'
width
'
))
||
null
;
const
height
=
parseInt
(
img
.
getAttribute
(
'
height
'
))
||
null
;
const
style
=
img
.
getAttribute
(
'
style
'
)
||
''
;
try
{
const
blob
=
await
fetchBlob
(
src
);
if
(
!
blob
.
type
.
startsWith
(
'
image/
'
))
return
null
;
const
ext
=
blob
.
type
.
split
(
'
/
'
)[
1
]?.
split
(
'
+
'
)[
0
]
||
'
png
'
;
const
ext
=
blob
.
type
.
split
(
'
/
'
)[
1
]?.
split
(
'
+
'
)[
0
]
||
'
png
'
;
const
file
=
new
File
([
blob
],
`pasted-image.
${
ext
}
`
,
{
type
:
blob
.
type
});
const
result
=
await
uploadBlob
(
blob
,
`pasted-image.
${
ext
}
`
);
await
handleFileUpload
(
file
,
view
,
null
,
{
width
,
height
,
align
:
'
left
'
});
if
(
!
result
?.
file_path
)
return
null
;
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
);
return
null
;
}
}
})
);
// Заменяем src на загруженные URL прямо в tmpDoc
externalImgs
.
forEach
((
img
,
i
)
=>
{
const
upload
=
uploads
[
i
];
if
(
upload
)
{
img
.
setAttribute
(
'
src
'
,
upload
.
filePath
);
// Сохраняем data-align для parseHTML Image.jsx
const
ml
=
(
upload
.
style
.
match
(
/margin-left
\s
*:
\s
*
([^
;
]
+
)
/i
)
||
[])[
1
]?.
trim
();
const
mr
=
(
upload
.
style
.
match
(
/margin-right
\s
*:
\s
*
([^
;
]
+
)
/i
)
||
[])[
1
]?.
trim
();
const
fl
=
(
upload
.
style
.
match
(
/float
\s
*:
\s
*
([^
;
]
+
)
/i
)
||
[])[
1
]?.
trim
();
const
align
=
(
ml
===
'
auto
'
&&
mr
===
'
auto
'
)
?
'
center
'
:
(
fl
===
'
right
'
||
ml
===
'
auto
'
)
?
'
right
'
:
'
left
'
;
img
.
setAttribute
(
'
data-align
'
,
align
);
if
(
upload
.
width
)
img
.
setAttribute
(
'
width
'
,
String
(
upload
.
width
));
if
(
upload
.
height
)
img
.
setAttribute
(
'
height
'
,
String
(
upload
.
height
));
img
.
removeAttribute
(
'
style
'
);
}
else
{
img
.
remove
();
}
}
});
// Вставляем HTML целиком — текст и картинки на своих местах
extension
.
editor
.
commands
.
insertContent
(
tmpDoc
.
body
.
innerHTML
);
extension
.
options
.
onUploadSuccess
?.();
})();
})();
return
true
;
return
true
;
...
...
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