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. Ryan Seddon says:

    @Michael

    That article is out of date and you should look at the latest version. That demo works fine for me the previous one uses a deprecated fileSize property so that’s why that demo doesn’t work.

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