A few weeks ago, while attending an IT conference, I heard one of the developers saying that the next version of their webapp will allow users to upload the files from their desktop using simple drag&drop. I immediately thought it’s a great feature in some scenarios:
- you can simply drop the file you working with to the browser instead of using the upload button and going through all you directory hierarchy to point it
- you can upload many files at once
- you get rid of those ugly upload fields and buttons
So after all the “why haven’t I thought about it before” thing I decided my next task at work would be adding this feature to our system. I started with some…
…background research
This drag&drop thing using the browser is not a whole new . It was f.e. possible in Firefox before, after installing the “dragdropupload” extension. But the newest Firefox (3.6, which is still in beta version but I believe the final version will be out very soon) implements the whole File API, so the users don’t need to install any extension (well, they just need to use the best web browser ;)). So here I will show you how to use this API. Just remember – right now it will only work in Firefox 3.6. We will have to do two things: retrieve (in Javascript on the browser side) the dropped files and upload them to the server using Ajax.
Handling dropped files
When the file is dropped to the browser it is simply displayed instead of the current page. Which is not what we want here. I assume we have an element (a <div id=”our_fancy_div”> for example) that we will drop the files to. We will observe the drop event, which is fired when the files are dropped.
var el = document.getElementById('our_fancy_div'); el.addEventListener('drop', onDrop, false);
Of course this would be shorter if you use Prototype or something similar, but here I won’t be using it. The good news is that with those two lines of core we have almost everything working. Cool, huh? We just need to implement our onDrop function:
function onDrop(event) { // we prevent the default action from happening, cos the // browser would replace our page with the dropped file event.stopPropagation(); event.preventDefault(); alert('You just dropped ' + event.dataTransfer.files.length + ' file(s).'); for(var i = 0 ; i < event.dataTransfer.files.length ; i++) { var file = event.dataTransfer.files.item(i); // TBD: do something useful... } }
OK, it wasn’t pretty simple, wasn’t it? Now we just have to…
Upload the files
This is a little bit trickier than the first part. We will have to make an Ajax request in which we will send the file content. I read (in some article about this topic) that we just have to put the file.getAsBinary()
to the request body, which is not exactly true.
We have three possible solutions here:
- use separate POST request to upload each file
- use separate PUT request to upload each file
- upload all the files with a big POST request
To make things simple I will only show you how to upload a single file.
Uploading a file using a PUT request
This is very straightforward. The only problem is that not every webserver allows you to use PUT requests. If it does, the code will look like this:
function uploadUsingPUT(file) { // create the ajax request object var xhr = new XMLHttpRequest(); // set the request method and the address // change the 'handle_request.php' to point // to the page you would like to be called xhr.open('PUT', '/handle_request.php', true); xhr.setRequestHeader('content-type', 'application/octet-stream'); xhr.setRequestHeader('content-disposition', 'attachment; filename="' + encodeURIComponent(file.fileName) + '"'); xhr.sendAsBinary(file.getAsBinary()); }
That’s all – we just send the file name in the request header and the request body contains only the file content. Of course you should put your listener to xhr.onreadystatechange
to handle the upload success and failure.
The PUT request method is very simple to use, cos it was design exactly for this purpose – uploading some content (which is still used in REST services). But as I said it’s not always possible to use it, so sometimes you will end up…
…uploading a file using a multi-part POST request
POST requests have more complex syntax than PUT requests. POST request can have multiple parts (separated with a boundary marker you have to specify in request header). Here we will create a request with a single part containing our file:
function uploadUsingPOST(file) { var xhr = new XMLHttpRequest(); xhr.open('POST', '/handle_request.php', true); var boundary = '------multipartformboundary' + (new Date).getTime(); var dashdash = '--'; var crlf = '\r\n'; // start the first (and the only one) request // part with a boundary marker var requestBody = dashdash + boundary + crlf; // describe the content of this request part requestBody += 'content-disposition: form-data; name="file"'; requestBody += '; filename="' + encodeURIComponent(file.fileName) + '"'; requestBody += crlf + 'content-type: application/octet-stream' + crlf; // double crlf indicates the end of request part header requestBody += crlf; // append the file content requestBody += file.getAsBinary(); // two markers below - end of part and end of request requestBody += crlf + dashdash + boundary + dashdash + crlf; // set the request type to multipart // and specify the boundary used in request body xhr.setRequestHeader('content-type', 'multipart/form-data; boundary='+ boundary); // upload the whole thing xhr.sendAsBinary(requestBody); }
As you see the is a little more magic here, but it is necessary – without it the server couldn’t understand your request.
Final words
The code above is the simplest case scenario to make you familiar with the topic. As mentioned before, you should add some error handling to the Ajax request. And you should of course handle the request on the server side and do something with the uploaded files. I the user is allowed to drop multiple files – think about how you would handle this. You have two choices here – send them separately or in on big POST request. If the dropped files are somehow connected to each other, you should send them together which is similar to an atomic transaction – either all files will be uploaded or the request will fail. On the other hand if those are individual files, you could upload them in separate requests, so the user won’t have to upload all of them again if f.e. the upload will fail almost at the end (for any reason).
Speaking of uploading – this can take some time of course, especially if the files are quite big. Nice thing is that XMLHttpRequest
can inform you about the uploading progress. You just have to add your listener like this:
xhr.upload.addEventListener("progress", onProgress, false);
You can show a progress bar before starting the request and update it in the onProgress function, which is pretty simple:
function onProgress(event) { if (event.lengthComputable) { var percentage = Math.round((event.loaded * 100) / event.total); if (percentage <= 100) { // TBD: update the progress bar } } }
I think that’s it. As you see this whole drag&drop thing can be handled with just a few lines of code. And it can amaze your clients during the presentation.:). If you have any questions – don’t hesitate to contact me.
mech