109 lines
2.6 KiB
JavaScript
109 lines
2.6 KiB
JavaScript
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;
|