Commit c25a5de8 by Яков

fix drag and drop

parent c79e85c3
......@@ -12,7 +12,7 @@ const App = () => {
// console.log(value);
}}
uploadOptions={{
url: 'https://cdn.atmaguru.online/upload/?sid=atmacompany&md5=0cETbV4BquHkqAdG9cK9MA&expires=1742192970',
url: '/upload',
errorMessage: 'Загрузка временно невозможна'
}}
style={{
......
......@@ -512,8 +512,19 @@ const QEditor = ({
Superscript,
Subscript,
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,
onUpdate: ({editor}) => onChange(editor.getHTML()),
......
import { Extension } from '@tiptap/core';
import { Plugin, PluginKey } from 'prosemirror-state';
import { EditorView } from 'prosemirror-view';
import axios from 'axios';
export const DragAndDrop = Extension.create({
......@@ -9,132 +8,153 @@ export const DragAndDrop = Extension.create({
addOptions() {
return {
uploadUrl: '', // URL для загрузки файлов
uploadHandler: null, // Альтернативный обработчик загрузки
types: ['image'], // Поддерживаемые типы файлов
uploadHandler: null, // Кастомный обработчик загрузки
allowedFileTypes: [ // Разрешенные MIME-типы
'image/jpeg',
'image/png',
'image/gif',
'image/webp',
'video/mp4',
'video/webm',
'audio/mpeg'
],
headers: {}, // Дополнительные заголовки
onUploadError: (error) => console.error('Upload failed:', error),
onUploadSuccess: () => {} // Колбек при успешной загрузке
};
},
addProseMirrorPlugins() {
if (!this.options.uploadUrl && !this.options.uploadHandler) {
return [];
}
const extension = this;
// Проверяем, является ли файл реальным (не из Word)
const isRealFile = (file) => {
if (!file || !file.type) return false;
// Игнорируем специфичные для Word типы
const wordTypes = [
'application/x-mso',
'ms-office',
'wordprocessingml',
'application/rtf',
'text/rtf',
'text/html'
];
const isRealFile = (item) => {
// Игнорируем текстовые форматы и специфичные для Word
if (item.type.startsWith('text/') ||
item.type.startsWith('application/x-mso') ||
item.type === 'text/html' ||
item.type === 'text/rtf') {
if (wordTypes.some(type => file.type.includes(type))) {
return false;
}
// Разрешенные файловые типы
return [
'image/', 'video/', 'audio/',
'application/octet-stream',
'application/pdf',
'application/zip'
].some(type => item.type.startsWith(type));
// Проверяем разрешенные типы
return extension.options.allowedFileTypes.includes(file.type);
};
const uploadFile = async (file) => {
if (this.options.uploadHandler) {
return await this.options.uploadHandler(file);
}
// Определяем тип ноды для вставки
const getNodeType = (mimeType) => {
if (mimeType.startsWith('image/')) return 'image';
if (mimeType.startsWith('video/')) return 'video';
if (mimeType.startsWith('audio/')) return 'audio';
return null;
};
// Обработчик загрузки файла
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 headers = {
'Content-Type': 'multipart/form-data',
...this.options.headers
};
const response = await axios.post(
this.options.uploadUrl,
extension.options.uploadUrl,
formData,
{ headers }
{
headers: {
'Content-Type': 'multipart/form-data',
...extension.options.headers,
},
}
);
if (!response.data) throw new Error('Upload failed');
return response.data.file_path;
};
const handleUpload = async (file, view, pos) => {
try {
const filePath = await uploadFile(file);
if (!filePath) return;
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 (!response.data?.file_path) {
throw new Error('Invalid server response');
}
if (node) {
view.dispatch(tr.insert(pos, node));
fileUrl = response.data.file_path;
}
if (!fileUrl) return;
const { state, dispatch } = view;
const type = getNodeType(file.type);
if (!type) return;
const node = state.schema.nodes[type].create({ src: fileUrl });
dispatch(state.tr.insert(position, node));
extension.options.onUploadSuccess(fileUrl);
} catch (error) {
console.error('Upload error:', error);
extension.options.onUploadError(error);
}
};
return [
new Plugin({
key: new PluginKey('dragAndDrop'),
props: {
handleDOMEvents: {
drop(view, event) {
const files = event.dataTransfer?.files;
if (!files || files.length === 0) return false;
// Обработчик вставки (paste)
const handlePaste = (view, event) => {
const items = Array.from(event.clipboardData?.items || []);
const htmlData = event.clipboardData.getData('text/html');
const coordinates = view.posAtCoords({
left: event.clientX,
top: event.clientY,
});
if (!coordinates) return false;
// Если есть HTML и это контент из Word - пропускаем
if (htmlData.includes('urn:schemas-microsoft-com')) {
return false;
}
// Фильтруем только реальные файлы
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;
Array.from(files).forEach(file => {
if (isRealFile({ kind: 'file', type: file.type })) {
handleUpload(file, view, coordinates.pos);
}
files.forEach(file => {
handleFileUpload(file, view, 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);
// Обработчик перетаскивания (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;
const { state } = view;
const pos = state.selection.$from.pos;
if (!pos) return false;
files.forEach(file => {
handleUpload(file, view, pos);
handleFileUpload(file, view, pos);
});
return true;
},
};
return [
new Plugin({
key: new PluginKey('dragAndDrop'),
props: {
handlePaste,
handleDrop,
},
}),
];
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment