Commit c25a5de8 by Яков

fix drag and drop

parent c79e85c3
...@@ -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={{
......
...@@ -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()),
......
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;
},
}, },
}), }),
]; ];
......
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