Can a file be uploaded via drag & drop rather than prompt?
My first thought is to use a form object w/ the focus or mouse_enter event.
Can a file be uploaded via drag & drop rather than prompt?
My first thought is to use a form object w/ the focus or mouse_enter event.
FileLoaders don’t support this by default, so you need to use a bit of JavaScript. This uses some of the more advanced features of Anvil, so if you need any clarification, please ask.
I’ve written an example app you can clone and use. If you click the link below, you can get a copy of it and use it as a dependency in your app, which should give you a FileDragAndDrop
component in your Toolbox:
https://anvil.works/build#clone:KYSTNIFTZ5H2PYJC=ERKLASMYEHVPWBLXFE73HD5E
It’s based on this MDN entry.
It has a CustomHTML Form that wraps a FileLoader in a div
with functions to handle drag and drop events:
<div style="border: solid #2196f3;"
anvil-slot-repeat="default"
ondrop="dropHandler(event);"
ondragover="dragOverHandler(event);">
</div>
First, we prevent the default behaviour of the ‘drag’ event:
function dragOverHandler(ev) {
// Prevent default drag behavior (prevent file from being opened).
ev.preventDefault();
}
Then we define a function to load the file when it is dropped. Unfortunately, JavaScript has two interfaces for getting file objects, depending on your browser. So we need to do it both ways:
function dropHandler(ev) {
// Define what happens when a file is dropped.
ev.preventDefault();
var reader = new FileReader();
// Thanks to browser differences, we need an if-else...
if (ev.dataTransfer.items) {
// Use DataTransferItemList interface to access the file(s)
for (var i = 0; i < ev.dataTransfer.items.length; i++) {
// If dropped items aren't files, reject them
if (ev.dataTransfer.items[i].kind === 'file') {
var file = ev.dataTransfer.items[i].getAsFile();
// Call an Anvil method with the file contents
getFileContents(ev, reader, file);
}
}
ev.dataTransfer.items.clear();
} else {
// Use DataTransfer interface to access the file(s)
for (var i = 0; i < ev.dataTransfer.files.length; i++) {
var file = ev.dataTransfer.files[i].getAsFile();
// Call an Anvil method with the file contents
getFileContents(ev, reader, file);
}
ev.dataTransfer.clearData();
}
}
The getFileContents
function passes the bytes into an Anvil method called drag_drop_upload
. Yep, the JavaScript is calling a Python function! See the reference docs section on calling JS and back again.
function getFileContents(ev, reader, file) {
// Read a file and call an Anvil method.
reader.onloadend = function() {
anvil.call(ev.target, "drag_drop_upload", file.type, reader.result, file.name);
}
reader.readAsBinaryString(file);
}
There are a couple of limitations you might want to be aware of - both can be worked around if you need to:
As I’ve written it, it doesn’t work for multiple files, but I think it can be modified to make that possible (in the JavaScript, dataTransfer.items
and dataTransfer.files
can contain multiple files).
When a file is drag-and-dropped, it bypasses the inbuilt code that sets self.file_loader_1.files
and changes the FileLoader’s text to say ‘1 File Loaded’. But you can do these things manually in the Python: you have access to the files in the event handler, and you can set self.file_loader_1.text
to whatever you want as well.
Here it is in action:
Hope you can make use of it, and do ask if you have any follow-up questions!
Great work mate! Super useful.
Thank you for making this. Very useful. One question if you have time: When I use the app, I get the following error:
TypeError: content must be a byte-string, not str
at app/DragAndDrop/FileDragAndDrop.py, line 15 column 4
Probably an encoding issue (Maybe the file needs to be explicitly converted to a binary format).
Cheers,
Hans Olav
Wait a second. It works fine in your app, but when I use it as a dependency in my app, it does not work. Strange.
Here is my app: https://anvil.works/build#clone:JFMYIZ3BLVLXS66N=PHHUFP3NM3MS4OINJN3OPUK4
Hans Olav
Aha – looks like this app was broken by the transition from Python 2 to Python 3, which draws a distinction between (text) strings and (data) bytestrings. The JS is sending the (data) contents of the file as a (text) string, and the Python is getting confused. In the absence of a more pleasant way to handle Python bytes
objects in JS, here’s the Python code to translate. Put this at the top of @shaun’s drag_drop_upload()
function (in the custom component):
# JS sent this data over as a "byte string", ie a string whose codepoints match the bytes in the file
# Transfrom from a it into a byte string for Python 3
data = bytes([ord(c) for c in data])
Thank you very much! I’ll try that and report back.
Hans Olav
Sorry for being dense.
I find the javascript, but not the drag_and_drop() function being called. Perhaps it is an Anvil function (that I cannot change)?
BTW: Maybe it gets tiresome, but have I told you how awesome I think Anvil is. A game-changer.
Hans Olav
And yes, I was dense. Found it. Thanks.
For others who might come across this thread: Here is a copy of the updated version (No string/byte encoding problems):
https://anvil.works/build#clone:YXH5Q2PE6NPVE5CD=GC37SVR7O6ASLO6QQDWTWTK6
Note that this version had to make a call to a server function since bytes() is not implemented in Skulpt.
Hans Olav