Drag and drop file uploading using JavaScript

With the recent announcement of the File API draft specification being published I’m sure a lot of people were confused as to what it could really do and why it is truly a powerful API. Firefox’s latest alpha release of their 3.6 browser, aka Namoroka, is the first to implement this new draft specification.

One of those powerful things we can do with it is create a file uploader where the user can drag & drop multiple files from their desktop straight into the browser avoiding the previous method of using the file input creating the ultimate killer feature that browsers so badly need.

The below demo only works in Firefox 3.6 Alpha 1 if you don’t want to install it you can watch the screencast below.

Update: File API has changed see new article on changes. The File API has changed

Drag & drop it like it’s hot

Now I’m sure a few people would have seen this functionality already if they watched the Google Wave presentation where they demonstrated drag and drop file uploading, but they used Google Gears to accomplish this. They did mention they will be working on a draft spec to get this functionality into HTML5. This hasn’t happened yet and instead Arun Ranganathan of Mozilla wrote the first draft spec for such functionality to be possible.

Video will automatically start at 15m22s, where the drag drop is shown

The File API

The File API is what makes this whole thing possible, on the dataTransfer object it has been extended with the file attribute so it can read and convert the files you are dropping which then sends the file information as binary using an xhr upload attribute creating a desktop like behaviour but in the browser, opening great possibilities and much needed functionality that a user will find more intuitive.

XMLHttpRequest 2

The second revision of the XMLHTTPRequest specification adds further functionality so we can actually send our dropped files to the server asynchronously. There are several additions which I use in this demo such as the upload attribute and the progress events like progress and load. With those events we can give the user some detailed feedback such as a percentage loader used in this example.

How it works

This example uses a few emerging technologies such as xhr2, local file access and the drag and drop API. The order in which the events happen are as follows:

  • The user drags images from their desktop to the drop area in the browser and fires the TCNDDU.handleDrop function.
  • The dataTransfer object passes through the local files dragged over through the files attribute
  • Using the getAsDataURL method we can convert the file to a base64 encoded string create an image and sets its source to that string.
  • The file is then passed into an xhr request where we use the new sendAsBinary method available since Firefox 3.0 and pass in the file as binary data using the getAsBinary method
  • We attach some progress events to the upload attribute so we can create a progress bar with percentage feedback and a load progress event so we can remove the progress bar once the image has uploaded successfully

I’ll go through some of the code in the demo to explain a few things in more detail.

for (var i = 0; i < count; i++) {
    domElements = [
        document.createElement('li'),
        document.createElement('a'),
        document.createElement('img')
    ];

    domElements[2].src = files[i].getAsDataURL(); // base64 encodes local file(s)
    domElements[2].width = 300;
    domElements[2].height = 200;
    domElements[1].appendChild(domElements[2]);
    domElements[0].id = "item"+i;
    domElements[0].appendChild(domElements[1]);
    
    imgPreviewFragment.appendChild(domElements[0]);
    
    dropListing.appendChild(imgPreviewFragment);
    
    TCNDDU.processXHR(files.item(i), i);
}

This for loop is inside the TCNDDU.handleDrop function which will loop through the files, the count var in the for loop is pointing to event.dataTransfer.files.length so we know how many files we are working with.

domElements[2].src = files[i].getAsDataURL();

This line sets the source of the image as a base64 encoded string so we can display the local file to the user straight away.

fileUpload.addEventListener("progress", TCNDDU.uploadProgressXHR, false);
fileUpload.addEventListener("load", TCNDDU.loadedXHR, false);
fileUpload.addEventListener("error", TCNDDU.uploadError, false);

These lines are in the TCNDDU.processXHR that gets fired for each file dragged into the window. The fileUpload points to a new XMLHttpRequest().upload which we attach a few event listeners for progress, load and error so we can give useful feedback to the user.

xhr.open("POST", "upload.php");
xhr.overrideMimeType('text/plain; charset=x-user-defined-binary');
xhr.sendAsBinary(file.getAsBinary());

Here we post the data to the PHP file for possible further processing etc. We also use the overrideMimeType method to user defined binary and finally use the new sendAsBinary method which has the local file passed in as binary.

TCNDDU.uploadProgressXHR = function (event) {
    if (event.lengthComputable) {
        var percentage = Math.round((event.loaded * 100) / event.total);
        if (percentage < 100) {
            event.target.log.firstChild.nextSibling.firstChild.style.width = 
            (percentage*2) + "px";

            event.target.log.firstChild.nextSibling.firstChild.textContent = 
            percentage + "%";
        }
    }
};

For the progress event we attached earlier we can check if it will return the right information so we can do a progress bar by checking for the lengthComputable property if that’s available we know the progress event will return two values of loaded and total from there we can work out the percentage that has loaded and adjust our visual cues, in our case a progress bar.

event.target.log.firstChild.nextSibling.firstChild

This line allows us to get access to the current container of the image that is being calculated. event.target will always point back to the xhr object in the TCNDDU.processXHR we added a link to the container by adding log to event.target and pointing it to container variable.

Not without issues

There are two issues I came across with this demo and most likely are something I have done wrong or it could possibly be a bug.

The first issue I came across was the event.target.log suddenly losing its reference to itself and no longer being able to update the progress bar as the link to the current container is suddenly undefined, this also causes the load event to never get fired and the progress bar never gets removed.

The other issue is the progress event won't fire if the file size is below around 140-150kb so no feedback will be given to the user. I'm not sure if this is intentional or a bug. I hope it's the latter as feedback on any sized file would be necessary. You can see this happening on the toucan image in the screencast above.

I took a sceencast of the issues you can watch below.

Heading in a great direction

This functionality is exactly what the web needs going forward and will hopefully see it in other browsers very soon, Google Wave could benefit from this greatly and would make one of their coolest features work without any need for external plugins.

Resources

This article was inspired from a few examples I have seen throughout my searches for such functionality, what sparked my imagination and got me developing a drag and drop uploader was this demo found on the Mozilla bug list.

A few people have already figured out and demonstrated multiple file uploading in safari 4 and a similar version that works in Firefox 3.5, although you can't select multiple files you can upload more than one at a time. That example I used a slightly modified version in this demo to send binary data to the server.

There is also another great article on uploading files and posting forms using Ajax. This articles demo works in Firefox 3+.

Post filed under: html5, javascript.

Skip to comment form.

  1. Great work Ryan, really nice. Let’s only hope the Mozilla guys won’t turn this feature off before the first stable release gets out.

  2. Hello,

    This sample is really interesting. My first thought is about security. The File API doesn’t really talk about security and it doesn’t look like your demo is signed or required the user to allow you access. My drag and drop applet needed to be signed and then the user had to allow it to access the file system. With the JavaScript example what prevents the code from reading random files from the file system?

    Thanks for the great demo,
    Zack

    • Thanks Zack.

      I believe the browser cannot interact with files unless the files are explicably referenced for it to use i.e. dragged to a special location in the browser. Someone with malicious intent could not look through someones local file system using the File API.

      On closer inspection in the draft spec under user cases they do talk about “Once a user has given permission” which leads me to believe that in the final release this may work the same way geolocation and offline storage do with a little prompt to allow such access. Though I hope it doesn’t as this will disrupt the natural flow of drag’n drop and there are no real concerns to me on how this works.

  3. arun says:

    Hi,

    I’ve followed your link and installed Firefox 3.6 Alpha 1. But drag and drop is not working in 3.6 browser also. I have tried with other browsers(IE,opera,firefox3.5) also. Please help me out in this issue.

    Thank you

  4. jesudian says:

    hi,

    I have tried the drag and drop functionality with all the browser including the suggested browser (firefox3.6). It is not working in any of the browser pls help

    • Are you testing my hosted version through 3.6? It’s working fine for me and I just re-tested it again.

      If you are using the source files it needs to be running under an apache install for it to work on your localhost.

      The only browser to support this functionality is Firefox 3.6a1 at the moment.

  5. strus_fr says:

    It does not seem that upload.php is called. How can I check that so I can move the uploaded file into a specific directory.
    Thanks!

    • The demo is only there to demonstrate that such functionality is beginning to become possible. It does post the binary data to the PHP file but all the PHP file does is print_r the binary data back to the user if they were to navigate to the file. It’s up to you to look into handling binary data in PHP, which I didn’t go into as it would be a whole other article in itself. Just do a search for handling binary data in PHP you’ll get plenty of articles to help you out.

  6. An awesome writeup, CSS ninja. You’ve inspired me to start kicking the tires on Namoroka.

    It’s worth mentioning that BrowserPlus provides this functionality today in browsers back to IE6 (http://browserplus.yahoo.com/). I do understand the appeal of browser native support, but it does seem like we’ve got a ways to go before we’re there.

    A couple comments:
    “The other issue is the progress event won’t fire if the file size is below around 140-150kb so no feedback will be given to the user.”

    Seems like the progress implementation should guarantee at least 0% and 100%..

    “xhr.sendAsBinary(file.getAsBinary());”
    The straightforward implementation would suggest we’re pulling the entire file contents into memory? seems fine for the average case but larger file uploads could be tricky.

    In any case, we’re definitely “Heading in a great direction”, here. Thanks again for the writeup.

    lloyd

    • Hi Lloyd thank you glad you found it useful.

      Yeah I have seen browserplus will definitely check it out drag and drop uploading in every browser sounds great. Native support for this functionality will definitely be a huge advantage. The less plugins needed to be installed the better.

      Yeah that sounds about right. I guess since they are smaller files it wouldn’t be such a huge deal, progress would be more useful for larger files etc.

      Yeah the memory usage spikes quite high when you drag in a few images. From a quick test it doesn’t seem to reclaim all the memory when the upload is done, which is concerning. Looking on the w3c mailing list there is discussing of just overloading the xhr.send() with an array, there was also suggestions for a new method such as sendFile(File[]). Will be interesting to see how this pans out.

      You’re very welcome thanks for reading and posting your great comments.

  7. patrick says:

    Is it possible to retrieve just the list of urls passed by the drop. I would be usefull for intranet file management.

    • @Patrick – That sort of information could be a security risk and potentially used in a malicious way. I don’t see that sort of information being passed to the File API.

  8. Matt says:

    Thanks for the write-up – this is a great addition and I can’t wait until it’s a standard.

    I’m wondering: do you know how you might handle downloading in a similar way? Does the File API provide for the possibility of drag and drop downloading without add-ons or other installation? I’ve been dragging and dropping uploads for a while now with FF 3.0 and the XPConnect bridge, but the best way I can see to bulk download the files is to automatically zip them and send that to the user.

    It seems like the File API should provide a solution – any idea if it does?

    • @Matt – You’re welcome, yes this will be a great addition to the HTML5 spec and some much needed functionality.

      This sort of functionality is already built into browsers, if you drag an image to your desktop it will download the image.

      I do have one idea, say for example the user did a drag selection in the browser over a bunch of images. On dragstart you could potentially load up the dataTransfer.mozSetDataAt(“application/x-moz-file”, file, 0), this is a mozilla only implementation. Only issue is I’m not sure how you detect a drop to the desktop or what gets fired when you do a drop if anything.

      You might want to check out SwellJS they have a video demonstrating dragging from the browser to a spreadsheet, they are using the dataTransfer.getData & setData methods to pass the information.

  9. FredO says:

    Lovely example, thanks. But……

    I tried this technique with a Ruby on Rails site.

    Rails passes an ‘authencity_token’ string to the client to be passed back on every access to the Rails server.

    Using this technique, I came up against the problem that Rails responds with an error saying something to the effect of “no authenticity token”.

    I looks like I may have to generate a form on the fly add the authenticity_token as a hidden variable, and the url-encoded contents of the file as the contents of a text area field then submit the form programmatically.

  10. steki says:

    demo seems to be broken with ff 3.6b3?

    • What’s the exact issue you are having? I just tried it myself in FF3.6b3 & b4 and it worked ok for me.

      Getting the timestamp I’m not entirely sure. If it’s possible to extract that information on the server from binary data that could be an option.

  11. steki says:

    when uploading files i want to keep the original file date and time. is it possible to transmit these informations to the server?

  12. Fred Obermann aka FredO says:

    Whoa! I got a version of your code working with Ruby on Rails. Given that we usually pass an ‘authenticity_token’ back and forth in a Rails session, the first problem you run in to is that when you create an XMLHttpRequest object and execute a ‘sendAsBinary(contents)’ against it, no ‘authenticity_token’ is sent to the server. And the server rejects your request.

    My solution was to make use of Rails’ restful routing feature to solve this problem.

    First, I changed the server code to generate a hexadecimal ‘authenticity_token’ in lieu of the normal ‘base64′ version. Then I included the ‘authenticity_token’ as a hidden field on the form

    Then on the server, I set up route that incorporates the ‘authenticity_token’ (along with other useful variables) in the path specification in the ‘routes.rb’ file.

    And in the JavaScript code, in ‘handleDrop()’, I use a path that incorporates the ‘authenticity_token’ and other interesting variables.

    On the server side, the variables encoded into the path are automatically extracted into the ‘params’ hash. And to get the file contents you just have to write something like:

    contents = request.body.read()

    So simple!

    There was, however another wrinkle I had to deal with.

    What I found (with firefox 3.6 beta at least) is that sometimes the width and height attributes are set on the image after invoking something like:

    img.src = dropped_file.getAsDataURL()

    And sometimes the are NOT. At different times the same file will result in an image with width and height set to values from the jpeg file, and sometimes the dimensions will both be zero.

    So after the user drags and drops files onto the browser…. in order to reliably know their dimensions, you have to upload them to the server (where you can gather the size information with image-magick or image-science) and then use another function (like jQuery’s ‘post()’ ) to send a request for the width and height.

    Thanks again for the heads-up on this technology!

  13. Franz says:

    Thanks for the exhaustive article… one question: you limit the maximum size for the processed files to 1 megabyte. Is this limit arbitrary or does firefox enforce this?

    Many JPEGs from digital cameras are larger than 1 MB, so the uploader wouldn’t work for these.

    Maybe you can update you example to show the resizing of these images with canvas techniques?

    Have you tested the uploader performance when dragging many image files at once?

    • The 1MB limit is purely arbitrary I put it in place so the demo couldn’t be abused with people trying to upload massive files.

      I’m not exactly sure what you mean by resizing with canvas techniques?

      In theory it should be able to handle any amount of images but the more images you drag in the more memory gets consumed. As it has to load the images into memory and process them.

      Mozilla has however, after some review of the spec, updated how it works and I’ll be doing a follow up article on those updates soon. The changes made will be able to handle larger files more seamlessly.

  14. Franz says:

    “resizing by canvas techniques” means that you load the original image into a html5 canvas with the .fromDataUrl()-method, then scale the canvas down and dump out the scaled-down-version of your image as a JPEG with the .toDataUrl()-method.

    you could then upload the scaled down version and save bandwith

  15. Your article and a little reading at the Mozilla dev site helped me to come up with a fairly full featured file manager I use for image uploads. It can be grabbed from the link above.

    Enjoy!

  16. @ninja

    http://www.sillywindows.com/fm/

    It requires a login to use..

    Username: admin
    Password: letmein

    (Yeah, I know, not original but there’s really no sensitive data there.)

    Its my live development version, so things may change as time progresses.

  17. qwazix says:

    I have used this and other fine examples on the subject to create an open source project which aims to be a complete solution for drag and drop uploading. Visit http://dragdropupload.sourceforge.net

  18. That’s great! It really helped me. I have worked on a mootools plugin that degrades nicely on the other browsers (except IE6 and IE7)

    see: http://html5stars.com/?p=62

  19. Thomas says:

    Is it possible to simply attach the file to the form and send it only when the user clicks the submit button?

    • @Thomas – You could create a button which fires the upload function and remove it from the drop event so only when the user clicks the button will the images be uploaded.

  20. Jmactacular says:

    This is amazing!!!!!!!! Thank you so much. Exactly what I was looking for. I got it rocking with C#. I was also seeing the lost reference to event.target.log and the progress bar stops updating, but I found by just adding a guard around it of “if (event.target.log)” it can recover okay and finish the progress bar as it uploads fine. Seems to work for me so far! Will try some more testing to make sure it’s solid. Also added a couple other similar guards around “currentImageItem.className”

  21. Hi! Nice work here. I’ve been working on something very similar for our site. Have it working in FF and WebKit browsers. Unfortunately, the file size issue is a major show stopper. How do you tell your users “use this to upload your files — unless they’re bigger than this size”? There has to be some other way to do this than reading all the files into memory… anyone?

    • @Philip S – If you’re talking about the size limit on my demo it’s just an if statement checking the size, you can remove that and the user can upload any sized file. Only issue is that, like you mentioned, it will load all of them into memory. Good news about that is the File API spec editors are aware of this and have added the url attribute which creates a random string that links back to the file rather than loading it into memory. The Bad news is it’s only currently in Firefox 4.

  22. Great work man! I sorta “ninja’d” your code and made it chrome compatible ;) . Posting a link when I finish some progress bar issues.

  23. Deepak karma says:

    Dear Friend!
    Good Job. But the problem is that this functionality work with Mozilla..when i m using chrome , “getAsDataURL();” this code not work.
    If u have some notes over this.pls share with me. i have an urgent reuirement for upload image in chrome via javascript.
    Thanks.

    • @Deepak – There is a big red update box at the start of the article pointing to an updated version of this demo and also going into the changes for the File API, getAsDataURL() doesn’t exist anymore and is replaced with the FileReader object which has readAsDataURL().

  24. lucke84 says:

    Great job with this article!

    A question for you: what if i’d like to check which is the extension of the file i’m going to upload? in your opinion, can i know it before actually doing the the post? I googled a lot but i can’t find how to ask xhr object the filename of the one i’m uploading.

    • @lucke84 –

      …what if i’d like to check which is the extension of the file i’m going to upload?

      You can check a files mime type from within the File object by using the type attribute e.g.

      event.dataTransfer.files[0].type
      

      If you drop an image it will populate the type with image/jpg or png or gif. With that information you can then do a check and force it to only accept certain files. If a files mime type isn’t known the type attribute is an empty string.

  25. I have download the code and run in our server. But in IE it’s does not Work. And the another problem I am facing to save the images.

  26. Troy says:

    This is great but I can’t make upload.php work. I can save the file but how do I get the original file name and is there a way I can pass in an extra parameter like a destination directory?

    • Ryan Seddon says:

      @Troy

      You can pass custom headers in the XHR request e.g.

      xhr.setRequestHeader("X-File-Name", file.name);
      xhr.setRequestHeader("X-File-Size", file.size);
      

      Then on the server side just look for those headers to get the file info.

  27. davis says:

    Hey, I built something similar using the new File API here: https://github.com/davisford/kickstarter — it is the whole thing (client/server with a Java/Jetty/CXF REST backend and jQuery / jQueryUI front end).

    I dev’d using Chrome, and it may have issues with FF and IE — I dunno…didn’t spend any time trying them, but feel free to use it as a starter project for drag-n-drop fileupload. It also includes optional md5 calculation in JavaScript — can send all the file metadata as additional post XML (deserialized with JAXB), and includes progress bar polling:

    step 1: $ git clone git://github.com/davisford/kickstarter.git
    step 2: $ mvn jetty:run
    step 3: Open chrome http://localhost:8080
    step 4: drag-n-drop files
    step 5: submit button
    step 6: …there is no step 6

  28. Gary Pennington says:

    Nice Demo. Any idea why it works when I drop an image from the filesystem but does not work when I drop an image from another browser window?

    Google image search handles both forms of drag and drop, which is where I first saw drag and drop done.

    When I do drop an image from another browser window I get a drop event but the files array is empty (event.dataTransfer.files.length == 0)

    Any idea how to handle this?

    Many thanks, Gary

    • Ryan Seddon says:

      @Gary –

      Good question, so dropping an image from another browser window doesn’t bring the image across as file, hence why the files property is empty. What Google images does is get the image src location from the drop event and then request that on the server and loads it in the results. I ripped out the code from google images so you can see what they’re doing on drop.

      Basically a simplified version they’re doing:

      document.body.ondrop = function(e) {
          var img = e.dataTransfer.getData("text/html");
      
          // This will pass along the html dropped which 
          // can have the image in it and they extract 
          // the source and do the magic on the server
      };
      
  29. Not sure when this started happening, but I can’t get drag n drop upload to work in Firefox 11 (even your example). I’ve used Firebug to track down the problem to the onDrop handler:

    var dt = event.dataTransfer,
    files = dt.files, …

    the dataTransfer property is there, the files property is there, but it is length = 0 and the function just exits. Any ideas why the “files” array is no longer populated when dropping files in Firefox 11? (On Ubuntu 11.10)

  30. Altaf Patel says:

    It shows message like’file size is too big. should be below 1 MB although dragging text file of 1 KB. (Browser: Chrome).