File Upload w/ Drag & Drop

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:

  1. 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).

  2. 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!

5 Likes

Great work mate! Super useful.

1 Like

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])
1 Like

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