Are you tired of dragging and dropping files one by one like it's 1995? Well, fear not! In this tutorial, we're gonna make drag and drop work for directories like a charm, using nothing but a little bit of JavaScript and a dash of magic.
Disclaimer: "Please don't take this seriously, I'm just assuming all of my readers are React wizards who could create a React app using just their minds. So, I'll spare you the boredom of explaining how to use create-react-app."
First things first, let's define a dropzone element in your HTML where your users can drop their directories. This is where the magic happens! We'll use a div
element and the ondrop
event to make this happen:
import "./App.css";
function App() {
const [className, setClassName] = useState("");
const [results, setResults] = useState([]);
return (
<div className="App">
<header className="App-header">
<p>
Drag and drop one or multiple files or directories into the rectangle
</p>
<div
id="dropzone"
className={className}
onDragOver={onDragOver}
onDragEnter={onDragEnter}
onDragLeave={onDragLeave}
onDrop={onDropItems}
>
Drop your directory here
</div>
{!!results.length &&
results.map((result) => <div key={result.path}>{result.path}</div>)}
</header>
</div>
);
}
export default App;
Pretty simple, right? The onDrop
event is triggered when your user drops a file or directory onto the dropzone and the onDragOver
event is triggered when your user drags a file or directory over the dropzone.
Now, let's add event listeners to the dropzone element to handle the drop and dragover events:
const supportsFileSystemAccessAPI =
"getAsFileSystemHandle" in DataTransferItem.prototype;
const supportsWebkitGetAsEntry =
"webkitGetAsEntry" in DataTransferItem.prototype;
const onDragOver = (e) => {
e.preventDefault();
};
const onDragEnter = () => {
setClassName("outline");
};
const onDragLeave = () => {
setClassName("");
};
const onDropItems = async (e) => {
// Prevent navigation.
e.preventDefault();
if (!supportsFileSystemAccessAPI && !supportsWebkitGetAsEntry) {
// Cannot handle directories.
return;
}
// Unhighlight the drop zone.
setClassName("");
const files = await getAllFileEntries(e.dataTransfer.items);
const flattenFiles = files.reduce((acc, val) => acc.concat(val), []);
console.log("Results here dude!!! : ", flattenFiles);
setResults(flattenFiles);
};
const getAllFileEntries = async (dataTransferItemList) => {
let fileEntries = [];
// Use BFS to traverse entire directory/file structure
let queue = [];
// Unfortunately dataTransferItemList is not iterable i.e. no forEach
for (let i = 0; i < dataTransferItemList.length; i++) {
queue.push(dataTransferItemList[i].webkitGetAsEntry());
}
while (queue.length > 0) {
let entry = queue.shift();
if (entry.isFile) {
fileEntries.push(entry);
} else if (entry.isDirectory) {
let reader = entry.createReader();
queue.push(...(await readAllDirectoryEntries(reader)));
}
}
// return fileEntries;
return Promise.all(
fileEntries.map((entry) => readEntryContentAsync(entry))
);
};
// Get all the entries (files or sub-directories) in a directory by calling readEntries until it returns empty array
const readAllDirectoryEntries = async (directoryReader) => {
let entries = [];
let readEntries = await readEntriesPromise(directoryReader);
while (readEntries.length > 0) {
entries.push(...readEntries);
readEntries = await readEntriesPromise(directoryReader);
}
return entries;
};
// Wrap readEntries in a promise to make working with readEntries easier
const readEntriesPromise = async (directoryReader) => {
try {
return await new Promise((resolve, reject) => {
directoryReader.readEntries(resolve, reject);
});
} catch (err) {
console.error(err);
}
};
const readEntryContentAsync = async (entry) => {
return new Promise((resolve, reject) => {
let reading = 0;
const contents = [];
reading++;
entry.file(async (file) => {
reading--;
const rawFile = file;
rawFile.path = entry.fullPath;
contents.push(rawFile);
if (reading === 0) {
resolve(contents);
}
});
});
};
And voila! You can now drag and drop directories like a boss. But wait, what's this FileSystemDirectoryHandle
API? It's a shiny new API that's still in the experimental stage, but it allows us to work with files and directories in a more modern and efficient way. However, not all browsers support it just yet, so it's always a good idea to include fallbacks for browsers that don't support it.
That's it for this tutorial. I hope you've found it useful and entertaining! Now go forth and drag and drop with reckless abandon!
The repository with all code is here: https://github.com/mike-yuen/multi-file-upload