File upload & drag and drop with HTML5

My use case: I want to drag a file from my local filesystem and drop it into a web page. Then I want to do stuff with the data in the file.

This wasn't possible until recently, but thanks to HTML5 we can do this natively, without using external plug-ins. Yay!

First thing is to make an HTML element draggable. In practice, this means adding two event listeners to it that, in turn, cancel the event. In my case, I want to make the entire web page the "target" of the dragging, so I'll add these listeners to the window object:


var cancel = function(event) {
    if(event.preventDefault) {
        event.preventDefault();
    }
    return false;
}
window.addEventListener('dragover', cancel, false);
window.addEventListener('dragenter', cancel, false);

As you see, first we declare a cancel function, then use it as the event handler for both dragover and dragenter events.

Now we need to add a listener for the drop event. Again, we add it to the window object:


window.addEventListener('drop', function(event) {
    var files = event.dataTransfer.files;
    if(files.length == 1) {
        var file = files[0];
        var reader = new FileReader();
        reader.onerror = function(e) {
            switch(e.target.error.code) {
                case e.target.error.NOT_FOUND_ERR:
                    console.log("file not found");
                    break;
                case e.target.error.NOT_READABLE_ERR:
                    console.log("file not readable");
                    break;
                case e.target.error.ABORT_ERR:
                    console.log("aborted");
                    break;
                default:
                    console.log('generic error?');
            }
        }
        reader.onload = (function(theFile) {
            return function(e) {
                var file_contents = e.target.result;
                do_stuff(file_contents);
            };
        })(file);

        reader.readAsText(file);
    }
}, false);

Let's go through the code.

First we look at event.dataTransfer.files. This will contain a representation of the files that have been dropped into the browser window. Since I want to accept exactly one file only, I check for the length of the files property. If it's not 1, nothing else happens.

And if it is, we create a FileReader, which is a nifty Javascript object that takes care of asynchronously loading the file. We need to define at least the onload event for the FileReader, since that gets called when our file is ready for consumption. We can also define the onerror event, just in case things go awry.

The onerror handler is quite simple conceptually --it simply dumps an error to the console.

The onload handler is a bit more elaborated: we use a function which returns a function which is finally the handler. This might seem overly complicated, but the explanation is that this code can be extended to handle more than one file, and in that case you definitely need this closure-style of function, or otherwise the function will get bound to the wrong argument. The way it is now, onload is finally this:


function(file) {
    var file_contents = file.target.result;
    do_stuff(file_contents);
}

... and that's because that "(file);" at the end of the "(function {} )" block makes it execute in place :)

If you're confused, don't worry. Just re-read everything carefully.

Finally, we just tell the reader to read the file contents plain text. As I said before, this is an asynchronous process, so we don't get the result instantly. Instead, we get it in the onload handler.

It's important to understand this, specially if you come from a background of traditional sequential programming, where you would have expected something like this instead:


$file_contents = file_get_contents('filename.txt');

and would have the results immediately in $file_contents, once the read is done. It would also be blocking, i.e. your script would get stuck in file_get_contents until everything is read.

THE Gotcha

You will never get the contents of the dragged file if you're working with a local page (i.e. the url starts with file:///). Never. This was driving me insane, until I decided to implement the onerror handler, and got a NOT_READABLE_ERR error. That helped me track the error to this bug, but still... I didn't find it mentioned anywhere else.

So when developing you'll need to use a local server, and access your code from within it (e.g. http://localhost/test/draganddrop.html). Not that it's complicated to get a working local web server nowadays, but it's something to take into account.

Note: there is a command line option for enabling local file access with Chrome:

--allow-file-access-from-files```


You need to modify your launcher or whatever you use to launch Chrome, so that it uses that flag. E.g. ```
chrome --allow-file-access-from-files```


<h3>An example?</h3>

I used this technique (and pretty much all the sample codes are taken from there without modification) in my <a href="lab.soledadpenades.com/android/kml/">KML to DDMS tool</a>. The code is not obfuscated, so you might want to take a look at it :-)

<h3>More information</h3>
<ul>
    <li><a href="http://www.html5rocks.com/features/file">HTML5 Rocks - File Access</a></li>
    <li><a href="http://www.w3.org/TR/file-upload">W3C's File API specs</a></li>
    <li><a href="https://developer.mozilla.org/en/JavaScript/Guide/Closures">Javascript closures</a></li>
</ul>