Update #3: Using the techniques outlined in this article I have rolled out and initial release of imgscalr.com.
Feel free to download the JS source (it is heavily commented) and take a look at how it was rolled out. Unfortunately the required File API support is only in Chrome and Firefox currently, Safari gets it in version 6, Opera possibly in version 11 and possibly IE 10 (9 has no support).
Over the last day I’ve been trying to learn how to use the new HTML5 Drag and Drop API as well as the new File API. I want to create a really nice drag-and-drop experience for users trying to upload files to a webapp; much like Google just did withDrag and Drop attachments in Gmail.
They always say that the best way to learn something is to try and teach someone else, so here is my attempt at distilling this relatively new and unsupported technology down into nice diagrams, gotchas and code so you can enjoy it too.
Why do we need HTML5 for This?
Good question, and it all boils down to the information and access that an HTML5-compatible browser gives you to local resources on the person’s computer.
As of June 29, 2011 this code and the two APIs required for it to function work on the following browsers:
The only two browsers still not supporting both these HTML5 APIs completely are:
<“Additional details” section removed and summarized in the list above>
Understanding the HTML5 DnD and File API
Let me layout a high level abstract picture of the HTML5 Drag and Drop sequence of events so you can grok it faster, it basically looks like this (yes, I totally drew this by hand in OpenOffice Impress):
(Click to enlarge or PDF Version)
I created the diagram above so you could visually walk yourself through the processing of using these new features — it also helped me understand it a bit better seeing it laid out like this as well and the execution flow of the DnD and File APIs.
Let’s take a tour of the diagram, node-by-node. It goes something like this:
- (DnD API) Starts with the drop event when the user releases the mouse and the mouse-up event occurs.
- (DnD API) Get the DataTransfer object from the drop event
- (File API) Call DataTransfer.files to get a FileList, representing the list of files that were dropped.
- (File API) Iterate over all the individual File instances and use a FileReaderobject to read their content.
- (File API) Using the FileReader.readAsDataURL(file) call, every time a file is completely read, a new “data URL” (RFC 2397) formatted object is created and an event wrapping it is fired to the onloadend handler on the FileReader object.
- FYI: The “data URL” object is Base64-encoded binary data, with a spec-defined header sequence of chars. Browsers understand them.
- (File API) Now that we have the “data URL” object from the event, we can do cool things with it like replace the src attribute of an <img> tag to immediately show the value on-screen OR upload the Base64-encoded portion of the data to a serverto process as binary data.
As crazy as this all looks, it works really nicely when your browser supports it.
Update #4: As of April 27, 2011 the following browsers support this: Chrome, Firefox, Safari 6+ with Opera 11.x soon-to-follow. IE likely won’t support it until version 10.
Let’s take a look at the interesting bits of code that come together to make all this happen. First you have your HTML file:
Nothing fancy here, so let’s move on.
Next we have to register handlers with all the different Drag and Drop events that can occur on our page. Let’s look at the code first:
Wow that looks really complicated! Don’t worry, the dragEnter, dragExit anddragOver handlers are all basically no-op handlers that just stop the event from propagating further. They all look like this dragEnter handler:
You can even define a single handler like this that you can re-use for all those no-op events:
and just register it with the “dragenter”, “dragexit” and “dragover” events.
TIP: Sam points out in the comments that if you are using jQuery selectors to try and bind the listeners, it may not work.
The only interesting handler is the drop handler, and it looks like this:
The first bit looks familiar; stop the propagation of the event again. Then the interesting bit starts:
- Get the DataTransfer object from the event.
- Get the list of File objects that were dropped (via a FileList) from theDataTransfer object.
- If we have 1 or more files that were dropped, process them with the handleFilesfunction.
If you scroll back up to the diagram you’ll notice these are right out of it and were already outlined above as the transition between the HTML5 Drag and Drop API right into the new File API when you ask the DataTransfer for its FileList.
Now that we have that under control, let’s take a peek at the handleFiles function:
Taking a look at this line-by-line, we have:
- Grab the first file from the FileList – in this tutorial we only use the first file dropped.
- Replace the text in the drop box with something indicating we got the drop.
- Create a new FileReader (new File API) to read the file contents off disk.
- Register an onloadend handler with the FileReader that will process the file after it’s read.
- Start the read operation; reading files in as “data URL” formatted elements. We do this because we can literally set our return value as the <img> tag’s srcattribute and see them render; it’s slick!
So that was all pretty straight forward, the only magic left is to see how we handle the loaded file. It’s got to be insanely complex to make up for all the other small snippets so far right?
Nope! handleReaderLoadEnd is deceivingly simlpe:
And again, line by line, we are doing:
- Get the <img> element from our DOM.
- Set its src attribute value to the “data URL” formatted value read from disk; luckily browsers know how to render these things natively. Their format is really simple though and the actual image bits come in as Base64-encoded, you can see an example here.
And that’s it… awesome right?
I am not sure why I had to call evt.target.result as a few API docs I read said that theProgressEvent‘s result would contain the object, but when I just called ‘evt.result’ I got null. I did some debugger inspection and saw that target had the result property and changed it to that.
Update #1: Scott clarified below why evt.target.result has to be called:
If you were curious, the reason you have to do evt.target.result is because the event object is essentially just a wrapper with some information about what type of event was fired, and what object raised the event. The target property returns the FileReader object “reader.” “reader” has a “result” property that gets file information when the “onload” event fires. The event class doesn’t have a result property, so “event.result” will return undefined.
For anyone wanting the exact CSS I used to make the upload box look so epic, here it is. Enjoy!
Download Sample Code
If you would like to download the entire project created for this tutorial, you can grab it here. Enjoy!
When I started this little adventure I didn’t expect it to go so smoothly. At first reading through the APIs it was a bit daunting, but as I started to see more and more examples things cleared up for me.
If you have read this far I hope it helped and you can hit the ground running with the HTML5 APIs. I have no idea when Google Chrome, Apple Safari or Internet Explorer 9 will support these APIs and we can start rolling out apps using them exclusively. I think for the next few years at least having multiple modes of uploading is going to be necessary until it become ubiquitous. I was even surprised to see a site as large as Facebook use a Java Applet as an upload mechanism the other day.