Drag and drop file uploading using JavaScript

Aug 26
 
 
 
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.

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.

Ionut G. Stan, September 1st, 2009

Thanks Ionut, yeah hope this makes it in the final release which is due pretty soon.

The Css Ninja, September 1st, 2009

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

Zack Grossbart, September 1st, 2009

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.

The Css Ninja, September 2nd, 2009

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

arun, September 8th, 2009

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

jesudian, September 8th, 2009

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.

The Css Ninja, September 8th, 2009

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!

strus_fr, September 18th, 2009

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.

The Css Ninja, September 18th, 2009

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

Lloyd Hilaiel, September 19th, 2009

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.

The Css Ninja, September 19th, 2009

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

patrick, October 1st, 2009

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, October 1st, 2009

@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.

The Css Ninja, October 1st, 2009

@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.

The Css Ninja, October 1st, 2009

[...] do this I used examples from here for the new File API and here for the AJAX upload. Obviously this could be expanded upon by [...]

Drag and Drop off the Desktop in Duke Webfiles, October 6th, 2009

[...] He’s come up with a demo that shows what’s possible when you’ve got downloadable fonts, drag and drop and editable content. (If you want to know more about drag and drop we suggest you read his excellent overview of using drag and drop to do file uploading.) [...]

font_dragr: a drag and drop preview tool for fonts at hacks.mozilla.org, October 22nd, 2009

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.

FredO, November 12th, 2009

@FredO – I never delved to deeply into handling the data on the server side, but you may want to check out this article http://www.appelsiini.net/2009/10/html5-drag-and-drop-multiple-file-upload which goes into handling file uploads on the server.

The Css Ninja, November 12th, 2009

[...] Drag and drop file uploading using Javascript – Link. [...]

Styling Comment Forms | WebDesignExpert.Me, November 18th, 2009

demo seems to be broken with ff 3.6b3?

steki, November 21st, 2009

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

steki, November 21st, 2009

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.

The Css Ninja, November 22nd, 2009

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!

Fred Obermann aka FredO, November 23rd, 2009

Thanks for the awesome follow up comment Fred! Good to know you found a solution, will be very useful for other readers.

The Css Ninja, November 23rd, 2009

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?

Franz, December 3rd, 2009

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.

The Css Ninja, December 3rd, 2009

“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

Franz, December 3rd, 2009

Sounds like a cool idea, would be pretty intensive work for the client to process even with web workers.

The Css Ninja, December 4th, 2009

[...] This post was Twitted by thek27 [...]

Twitted by thek27, December 10th, 2009

[...] This post was Twitted by IainDelaney [...]

Twitted by IainDelaney, January 10th, 2010

Leave a comment