drop
This commit is contained in:
108
src/components/FileDropzone.jsx
Normal file
108
src/components/FileDropzone.jsx
Normal file
@@ -0,0 +1,108 @@
|
||||
import { useState, useRef } from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
|
||||
/**
|
||||
* An otherwise invisible dropzone wrapper.
|
||||
* Renders seamlessly around children without altering layout or style,
|
||||
* and highlights with an outline only while a file is dragged over it.
|
||||
*/
|
||||
export function FileDropzone({
|
||||
accept = 'application/pdf,.pdf',
|
||||
disabled = false,
|
||||
onFileDrop,
|
||||
children,
|
||||
sx,
|
||||
...props
|
||||
}) {
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const dragCounter = useRef(0);
|
||||
|
||||
const handleDragEnter = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (disabled) return;
|
||||
dragCounter.current += 1;
|
||||
if (e.dataTransfer.items && e.dataTransfer.items.length > 0) {
|
||||
setIsDragging(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragLeave = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragCounter.current -= 1;
|
||||
if (dragCounter.current <= 0) {
|
||||
dragCounter.current = 0;
|
||||
setIsDragging(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (disabled) {
|
||||
e.dataTransfer.dropEffect = 'none';
|
||||
return;
|
||||
}
|
||||
e.dataTransfer.dropEffect = 'copy';
|
||||
};
|
||||
|
||||
const handleDrop = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
dragCounter.current = 0;
|
||||
setIsDragging(false);
|
||||
if (disabled) return;
|
||||
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (!file) return;
|
||||
|
||||
if (accept) {
|
||||
const allowed = accept
|
||||
.split(',')
|
||||
.map((s) => s.trim().toLowerCase())
|
||||
.filter(Boolean);
|
||||
const name = (file.name || '').toLowerCase();
|
||||
const dotIndex = name.lastIndexOf('.');
|
||||
const ext = dotIndex !== -1 ? name.slice(dotIndex) : '';
|
||||
const mimeType = (file.type || '').toLowerCase();
|
||||
|
||||
const matches = allowed.some((item) => {
|
||||
if (item.startsWith('.')) {
|
||||
return ext === item;
|
||||
}
|
||||
return mimeType === item;
|
||||
});
|
||||
|
||||
if (!matches) return;
|
||||
}
|
||||
|
||||
onFileDrop?.(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box
|
||||
onDragEnter={handleDragEnter}
|
||||
onDragLeave={handleDragLeave}
|
||||
onDragOver={handleDragOver}
|
||||
onDrop={handleDrop}
|
||||
sx={{
|
||||
display: 'inline-flex',
|
||||
position: 'relative',
|
||||
borderRadius: 1,
|
||||
flexShrink: 0,
|
||||
// Keep outline inset so it never affects external geometry or causes scrollbars:
|
||||
outline: isDragging ? '2px dashed' : 'none',
|
||||
outlineColor: 'primary.main',
|
||||
outlineOffset: -2,
|
||||
transition: 'outline 0.15s ease-in-out',
|
||||
...sx,
|
||||
}}
|
||||
{...props}
|
||||
>
|
||||
{typeof children === 'function' ? children({ isDragging }) : children}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
export default FileDropzone;
|
||||
Reference in New Issue
Block a user