Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
R
react-ag-qeditor
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
lib
react-ag-qeditor
Commits
c25a5de8
Commit
c25a5de8
authored
Jun 27, 2025
by
Яков
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fix drag and drop
parent
c79e85c3
Hide whitespace changes
Inline
Side-by-side
Showing
3 changed files
with
137 additions
and
106 deletions
+137
-106
App.js
example/src/App.js
+1
-1
QEditor.jsx
src/QEditor.jsx
+13
-2
DragAndDrop.js
src/extensions/DragAndDrop.js
+123
-103
No files found.
example/src/App.js
View file @
c25a5de8
...
@@ -12,7 +12,7 @@ const App = () => {
...
@@ -12,7 +12,7 @@ const App = () => {
// console.log(value);
// console.log(value);
}}
}}
uploadOptions
=
{{
uploadOptions
=
{{
url
:
'
https://cdn.atmaguru.online/upload/?sid=atmacompany&md5=0cETbV4BquHkqAdG9cK9MA&expires=1742192970
'
,
url
:
'
/upload
'
,
errorMessage
:
'Загрузка временно невозможна'
errorMessage
:
'Загрузка временно невозможна'
}}
}}
style
=
{{
style
=
{{
...
...
src/QEditor.jsx
View file @
c25a5de8
...
@@ -512,8 +512,19 @@ const QEditor = ({
...
@@ -512,8 +512,19 @@ const QEditor = ({
Superscript
,
Superscript
,
Subscript
,
Subscript
,
DragAndDrop
.
configure
({
DragAndDrop
.
configure
({
linkUpload
:
uploadOptions
.
url
uploadUrl
:
uploadOptions
.
url
,
}),
allowedFileTypes
:
[
'image/jpeg'
,
'image/png'
,
'video/mp4'
],
onUploadSuccess
:
(
fileUrl
)
=>
{
console
.
log
(
'File uploaded:'
,
fileUrl
);
},
onUploadError
:
(
error
)
=>
{
console
.
error
(
'Upload error:'
,
error
);
}
})
],
],
content
:
value
,
content
:
value
,
onUpdate
:
({
editor
})
=>
onChange
(
editor
.
getHTML
()),
onUpdate
:
({
editor
})
=>
onChange
(
editor
.
getHTML
()),
...
...
src/extensions/DragAndDrop.js
View file @
c25a5de8
import
{
Extension
}
from
'@tiptap/core'
;
import
{
Extension
}
from
'@tiptap/core'
;
import
{
Plugin
,
PluginKey
}
from
'prosemirror-state'
;
import
{
Plugin
,
PluginKey
}
from
'prosemirror-state'
;
import
{
EditorView
}
from
'prosemirror-view'
;
import
axios
from
'axios'
;
import
axios
from
'axios'
;
export
const
DragAndDrop
=
Extension
.
create
({
export
const
DragAndDrop
=
Extension
.
create
({
...
@@ -9,132 +8,153 @@ export const DragAndDrop = Extension.create({
...
@@ -9,132 +8,153 @@ export const DragAndDrop = Extension.create({
addOptions
()
{
addOptions
()
{
return
{
return
{
uploadUrl
:
''
,
// URL для загрузки файлов
uploadUrl
:
''
,
// URL для загрузки файлов
uploadHandler
:
null
,
// Альтернативный обработчик загрузки
uploadHandler
:
null
,
// Кастомный обработчик загрузки
types
:
[
'image'
],
// Поддерживаемые типы файлов
allowedFileTypes
:
[
// Разрешенные MIME-типы
'image/jpeg'
,
'image/png'
,
'image/gif'
,
'image/webp'
,
'video/mp4'
,
'video/webm'
,
'audio/mpeg'
],
headers
:
{},
// Дополнительные заголовки
headers
:
{},
// Дополнительные заголовки
onUploadError
:
(
error
)
=>
console
.
error
(
'Upload failed:'
,
error
),
onUploadSuccess
:
()
=>
{}
// Колбек при успешной загрузке
};
};
},
},
addProseMirrorPlugins
()
{
addProseMirrorPlugins
()
{
if
(
!
this
.
options
.
uploadUrl
&&
!
this
.
options
.
uploadHandler
)
{
const
extension
=
this
;
return
[];
}
// Проверяем, является ли файл реальным (не из Word)
const
isRealFile
=
(
file
)
=>
{
const
isRealFile
=
(
item
)
=>
{
if
(
!
file
||
!
file
.
type
)
return
false
;
// Игнорируем текстовые форматы и специфичные для Word
if
(
item
.
type
.
startsWith
(
'text/'
)
||
// Игнорируем специфичные для Word типы
item
.
type
.
startsWith
(
'application/x-mso'
)
||
const
wordTypes
=
[
item
.
type
===
'text/html'
||
'application/x-mso'
,
item
.
type
===
'text/rtf'
)
{
'ms-office'
,
'wordprocessingml'
,
'application/rtf'
,
'text/rtf'
,
'text/html'
];
if
(
wordTypes
.
some
(
type
=>
file
.
type
.
includes
(
type
)))
{
return
false
;
return
false
;
}
}
// Разрешенные файловые типы
// Проверяем разрешенные типы
return
[
return
extension
.
options
.
allowedFileTypes
.
includes
(
file
.
type
);
'image/'
,
'video/'
,
'audio/'
,
'application/octet-stream'
,
'application/pdf'
,
'application/zip'
].
some
(
type
=>
item
.
type
.
startsWith
(
type
));
};
};
const
uploadFile
=
async
(
file
)
=>
{
// Определяем тип ноды для вставки
if
(
this
.
options
.
uploadHandler
)
{
const
getNodeType
=
(
mimeType
)
=>
{
return
await
this
.
options
.
uploadHandler
(
file
);
if
(
mimeType
.
startsWith
(
'image/'
))
return
'image'
;
}
if
(
mimeType
.
startsWith
(
'video/'
))
return
'video'
;
if
(
mimeType
.
startsWith
(
'audio/'
))
return
'audio'
;
return
null
;
};
const
formData
=
new
FormData
();
// Обработчик загрузки файла
formData
.
append
(
'file'
,
file
);
const
handleFileUpload
=
async
(
file
,
view
,
position
)
=>
{
try
{
let
fileUrl
;
if
(
extension
.
options
.
uploadHandler
)
{
fileUrl
=
await
extension
.
options
.
uploadHandler
(
file
);
}
else
{
const
formData
=
new
FormData
();
formData
.
append
(
'file'
,
file
);
const
response
=
await
axios
.
post
(
extension
.
options
.
uploadUrl
,
formData
,
{
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
;
}
const
headers
=
{
if
(
!
fileUrl
)
return
;
'Content-Type'
:
'multipart/form-data'
,
...
this
.
options
.
headers
};
const
response
=
await
axios
.
post
(
const
{
state
,
dispatch
}
=
view
;
this
.
options
.
uploadUrl
,
const
type
=
getNodeType
(
file
.
type
);
formData
,
if
(
!
type
)
return
;
{
headers
}
);
if
(
!
response
.
data
)
throw
new
Error
(
'Upload failed'
);
const
node
=
state
.
schema
.
nodes
[
type
].
create
({
src
:
fileUrl
});
return
response
.
data
.
file_path
;
dispatch
(
state
.
tr
.
insert
(
position
,
node
));
extension
.
options
.
onUploadSuccess
(
fileUrl
);
}
catch
(
error
)
{
extension
.
options
.
onUploadError
(
error
);
}
};
};
const
handleUpload
=
async
(
file
,
view
,
pos
)
=>
{
// Обработчик вставки (paste)
try
{
const
handlePaste
=
(
view
,
event
)
=>
{
const
filePath
=
await
uploadFile
(
file
);
const
items
=
Array
.
from
(
event
.
clipboardData
?.
items
||
[]);
if
(
!
filePath
)
return
;
const
htmlData
=
event
.
clipboardData
.
getData
(
'text/html'
);
const
{
state
}
=
view
;
const
{
tr
}
=
state
;
let
node
;
if
(
file
.
type
.
startsWith
(
'image/'
)
&&
this
.
options
.
types
.
includes
(
'image'
))
{
node
=
state
.
schema
.
nodes
.
image
?.
create
({
src
:
filePath
});
}
else
if
(
file
.
type
.
startsWith
(
'video/'
)
&&
this
.
options
.
types
.
includes
(
'video'
))
{
node
=
state
.
schema
.
nodes
.
video
?.
create
({
src
:
filePath
});
}
else
if
(
file
.
type
.
startsWith
(
'audio/'
)
&&
this
.
options
.
types
.
includes
(
'audio'
))
{
node
=
state
.
schema
.
nodes
.
audio
?.
create
({
src
:
filePath
});
}
if
(
node
)
{
// Если есть HTML и это контент из Word - пропускаем
view
.
dispatch
(
tr
.
insert
(
pos
,
node
));
if
(
htmlData
.
includes
(
'urn:schemas-microsoft-com'
))
{
}
return
false
;
}
catch
(
error
)
{
console
.
error
(
'Upload error:'
,
error
);
}
}
// Фильтруем только реальные файлы
const
files
=
items
.
filter
(
item
=>
item
.
kind
===
'file'
)
.
map
(
item
=>
item
.
getAsFile
())
.
filter
(
file
=>
file
&&
isRealFile
(
file
));
if
(
files
.
length
===
0
)
return
false
;
event
.
preventDefault
();
const
pos
=
view
.
state
.
selection
.
from
;
files
.
forEach
(
file
=>
{
handleFileUpload
(
file
,
view
,
pos
);
});
return
true
;
};
// Обработчик перетаскивания (drop)
const
handleDrop
=
(
view
,
event
)
=>
{
const
files
=
Array
.
from
(
event
.
dataTransfer
?.
files
||
[])
.
filter
(
file
=>
isRealFile
(
file
));
if
(
files
.
length
===
0
)
return
false
;
event
.
preventDefault
();
const
pos
=
view
.
posAtCoords
({
left
:
event
.
clientX
,
top
:
event
.
clientY
,
})?.
pos
;
if
(
!
pos
)
return
false
;
files
.
forEach
(
file
=>
{
handleFileUpload
(
file
,
view
,
pos
);
});
return
true
;
};
};
return
[
return
[
new
Plugin
({
new
Plugin
({
key
:
new
PluginKey
(
'dragAndDrop'
),
key
:
new
PluginKey
(
'dragAndDrop'
),
props
:
{
props
:
{
handleDOMEvents
:
{
handlePaste
,
drop
(
view
,
event
)
{
handleDrop
,
const
files
=
event
.
dataTransfer
?.
files
;
if
(
!
files
||
files
.
length
===
0
)
return
false
;
const
coordinates
=
view
.
posAtCoords
({
left
:
event
.
clientX
,
top
:
event
.
clientY
,
});
if
(
!
coordinates
)
return
false
;
event
.
preventDefault
();
Array
.
from
(
files
).
forEach
(
file
=>
{
if
(
isRealFile
({
kind
:
'file'
,
type
:
file
.
type
}))
{
handleUpload
(
file
,
view
,
coordinates
.
pos
);
}
});
return
true
;
},
},
handlePaste
(
view
,
event
)
{
const
items
=
event
.
clipboardData
?.
items
;
if
(
!
items
)
return
false
;
// Проверяем наличие реальных файлов
const
files
=
Array
.
from
(
items
)
.
filter
(
item
=>
item
.
kind
===
'file'
&&
isRealFile
(
item
))
.
map
(
item
=>
item
.
getAsFile
())
.
filter
(
Boolean
);
if
(
files
.
length
===
0
)
return
false
;
event
.
preventDefault
();
const
{
state
}
=
view
;
const
pos
=
state
.
selection
.
$from
.
pos
;
files
.
forEach
(
file
=>
{
handleUpload
(
file
,
view
,
pos
);
});
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