How Gmail’s drag and drop works and why it’s not supported in Safari

Recently Gmail pushed out an update that allowed users to drag and drop files from desktop to Gmail and have them automatically uploaded. Being the web geek I am I had to figure out how it functioned. Firefox was easy and I have covered drag and drop uploading already. They also mentioned in their post that Chrome was supported but I know Chrome is yet to implement the File API. Most intriguing was that it doesn’t work in Safari?

In the above demo I’ve recreated a mock Gmail UI that works the same as the actual Gmail with drag drop uploading. It feature detects the browsers capabilities and depending on it will use the File API or another method, which I will delve into. The beauty of feature detection is once Chrome and other browsers add support for the File API the alternative method will become defunct and would require no maintenance for the code.

I have intentionally left out the File API code that would allow Firefox to function as I wanted to focus on Chrome’s implementation.

So how does Chrome do it?

Using a bit of CSS trickery we can create the illusion of having File API support and still allow users to drag and drop files from the desktop into Gmail. I accomplish this by having the drop zone, which becomes visible on a dragenter event, contain an invisible file input with a 100% width and height of the drop zone area. The file input also has the attribute multiple on it allowing a user to drop multiple files.

Drop files here

To add them as attachments

The drop zone is hidden by default and only appears when the body detects the dragenter event.

#drop {
    position: relative;
}
#drop input {
    position: absolute;
    width: 100%;
    height: 100%;
    top: 0;
    left: 0;
    opacity: 0;
}

The file input inside the drop zone has position absolute, width and height of 100% and its opacity set to 0 so it can still be interacted with but not seen.

Handling the drop

Unlike Firefox 3.6 we don’t bother listening for the drop event in the drop zone as Chrome is using the file input. We instead attach a change event to the file input which will trigger when a file is selected through the browse screen or in our case when a file is dropped on the input, and since we added the multiple attribute we can drop more than one file at a time.

dropArea.addEventListener("change", TCNDDU.handleDrop, false);
 
TCNDDU.handleDrop = function (evt) {
    var files = evt.target.files;
    dropArea.style.display = "none";
    for(var i = 0, len = files.length; i < len; i++) {
        // iterate over file(s) and process them for uploading
    }
};

The function that handles the change event looks at the javascript event target to access the files array which contains the dropped file(s) name, size and type. From there we can iterate over the files and queue them to be uploaded.

Uploading the file

I've updated the demo to now upload the file. Using XHR2 and the upload attribute to attach progress events so we can do a real progress bar that indicates to the user how much the file has uploaded.

Drop the file or files into the drop zone in Chrome or Safari and the files will be uploaded.

Have a read of Andrea Giammarchi Safari 4 Multiple Upload With Progress Bar article. I used the PHP from that article to handle the upload.

Why doesn't it work in Safari?

Technically it does work in Safari but only if you drop one file at a time which isn't what users would expect. Dropping multiple files will populate the files array in the file input, but selecting say 3 files and dragging them into Safari will register 3 files have been dragged but if you iterate over the files array those 3 files will all be the same file...

To explain it better let's say you have 3 files each one called a.jpg, b.jpg and c.jpg selecting those 3 files and then clicking and dragging on the b.jpg file will add 3 files to the input. The file you did the drag operation on will be added 3 times to the input and it will ignore the other files, iterating over the files array will have b.jpg 3 times?!

To add to the strangeness if I have an input that is visible and I click the choose file/browse button and select multiple files in the dialog box upon clicking open the files array will correctly be populated with all the files selected and won't have the duplication issue.

That folks is why I believe Safari isn't supported in the new drag and drop upload feature in Gmail. I'm sure Google has tapped Apple on the shoulder to get that fixed.

Update: Paul has filed a ticket in webkits bug tracker.

A work around?

The only work around that Google could do is to break out of the loop if they detect multiple instances of the same file being referenced. This work around would limit Safari to only 1 file per drag and drop operation.

TCNDDU.handleDrop = function (evt) {
    var files = evt.target.files;
    
    for(var i = 0, len = files.length; i < len; i++) {
        if(i != 0 && files[0].fileName === files[i].fileName) continue;
    }
};

Checking the first file name against other file names and making sure they don't match, if they do skip that iteration and go to the next. If the file names are all the same only the first will be processed but you still get the benefit of drag and drop action in Safari. Not ideal and an error message could be shown to the user letting them know why only 1 file appears.

Still sometime to go

From my investigation it seems Safari isn't far off from getting some drag and drop love, they just need to push out a fix for the bug mentioned above or Google could limit drag and drop to first file only for Safari.

I don't expect to see this functionality in any version of IE until they support the File API. Dragging and dropping a file into a file input doesn't work nor does IE have a files array for the file input.

Opera is a long way from having this functionality they have yet to implement the Drag and Drop API and their file inputs work the same as IE.

Our best bet of getting cross browser drag and drop uploading is with the Drag and Drop and File API. Let's hope we see some other browser vendors add this soon.

[link href="http://cssn.in/ja/027"]