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.

<div id="drop">
    <h1>Drop files here</h1>
    <p>To add them as attachments</p>
    <input type="file" multiple="true" id="filesUpload" />
</div>

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.

Short URL: http://cssn.in/ja/027

 

Post filed under: javascript.

Skip to comment form.

  1. anthonydpaul says:

    Using FF 3.6.3 MacOS, when I drag any file(s) to the demo, it opens the initial file in the tab/window just as it would if I were to open a JPG in FF. Chrome MacOS behaves as it should.

  2. Paul Irish says:

    Just in case they aren’t aware of this issue, I filed a ticket:
    https://bugs.webkit.org/show_bug.cgi?id=37957

    (hopefully its a dupe, but i couldnt find one)

    Awesome work btw.

  3. @anthonydpaul – I purposely left out handling Firefox as I wanted to focus on how Chrome works. Firefox won’t do anything in my demo as I check for support of the FileReader. Only if the browser doesn’t support FileReader will it fallback to using the file input method.

    @Paul – Thanks Paul, I did a quick search last night and couldn’t find anything either. I was going to do a more thorough search and then file a ticket if nothing was suitable. Looks like you beat me to it!

  4. m0rgen says:

    but how does this handle the fileupload?
    as far as i know, there is no way to manipulate a file input, so the only thing safari or chrome can do with the drops is reading their name and size, but NO UPLOAD of these files…

    or do i miss something?

    regards

  5. Simon says:

    Works fine for me in Safari 4.0.5. If I drag three independant files all at once I see all three filenames and their various sizes in K.

    But as I cannot upload anywhere my quick testing doesn’t allow me to verify if the file I dragged will get transferred 3 times or if it really does work.

  6. @m0rgen – I have been getting that question quite a bit so I’ve updated the article and demo to now actually upload the file and use xhr2 progress events to do the progress bar, give it a try.

    @Simon – I’m running Safari 4.0.5 also and I still get the duplicate file issue? Are you on a MAC or PC?

  7. m0rgen says:

    awesome, but doesn’t work for me, maybe there are some issues concerning my server doing xhr-uploads…?

    xhr uploads work for me if I encode the whole file, but therefore I need the getAsBinary-methode that I can only get working with firefox3.6 but not safari

    one question to IE? it does support dataTranfer on dropevent. I have no IE for testing, but isn’t it possible to do xhr with it?

  8. m0rgen says:

    finally made it workin with this php :)

    $filename = false;
    $filesize = 0;
    $uploaded = 0;
     
    if(function_exists('apache_response_headers')) {
    	$headers = apache_response_headers();
    	$filename = basename($headers['X-File-Name']);
    	$filesize+= $headers['X-File-Size'];
    } else {
    	if(isset($_SERVER['HTTP_X_FILE_NAME'])) { $filename = $_SERVER['HTTP_X_FILE_NAME']; } 
    	if(isset($_SERVER['HTTP_X_FILE_SIZE'])) { $filesize+= $_SERVER['HTTP_X_FILE_SIZE']; }
    }
    if(isset($_SERVER['CONTENT_LENGTH'])) { $uploaded+= $_SERVER['CONTENT_LENGTH']; }
     
    if($uploaded==$filesize && $filename) {
    	$content = file_get_contents("php://input");
    	$io = fopen($filename,'w');
    	fwrite($io,$content);
    	fclose($io);
    	chmod($filename, 0755);
    }
  9. m0rgen, good to hear you got it working. I would be interested in seeing what you’re using this for.

  10. Daniel says:

    OK, so it is beautiful… How about this…. drag an email straight out of Outlook onto the browser for uploading. Is it possible?

  11. pangratz says:

    does anyone know a JS library which handles all the browser specific stuff and abstracts the drag and drop handling, so i only have to specify a drop target and i will get informed with a list of dropped files, when a drop has been executed?

  12. Bardan says:

    I don’t want it to be uploaded via ajax. I want to upload it as form submission. How can I achieve that with drag and drop.

  13. pangratz says:

    i made a jquery plugin which allows multiple drag and drop file upload in chrome, chromium, safari and firefox…

  14. Inversion says:

    >evt.target.files,
    Seems like you have used comma instead “;” in both of code samples.

    Thanks for very useful article!

  15. ziggy says:

    I was baffled why Chrome worked without the file api done yet. Thanks for this, very informative.

  16. stilldrummer says:

    cool file API stuff, I could use this

  17. @Bardan – That’s an easy change, in the TCNDDU.handleDrop function it cycles through the files and executes the TCNDDU.processXHR(files[i], i); function which does the async uploading. To stop that just remove the processXHR function and attach it to the submit button.

  18. @Inversion – Thanks for that, have updated the code examples.

  19. Andrew says:

    Great post! I created an uploader based on some of these ideas. Thanks for the help!
    http://jsjoy.com/awesomeuploader

  20. Great work as usual, CSS Ninja. got my own demo code working in Firefox 3.6, Safari 4, and Chrome 5 on Windows Vista x64.

    Next step: incorporate Google Gears so Internet Explorer can work similarly. Gears can be used to read files off a drop, but not sure how to send them asynchronously yet.

    Tried it in the latest version of Opera just for the hell of it… of course it didn’t do anything but I was surprised that Opera now supports the CSS3 border-radius (without any vendor prefix).

  21. @Michael B – Thank you. Never really played with Gears so I couldn’t help you there.

    Opera doesn’t support the drag and drop API, which is ridiculous, though I hear it may make it into the next point release.

  22. Vikash ~vic~ says:

    Great article thanks …!

  23. Bx says:

    Sending file via xhr.send is outdated solution. XHR2 provides class FormData. You may pass instance of this class to xhr.send and emulate true multipart request! As advantage of this approach – you could handle with files on server side as usual.

    P.S. Sorry for my english :-)

  24. Bx says:

    Of course, I know that at time of postiong this article there was no browser (maybe except FF4 beta) with support of FormData. But now Chrome supports this API.

  25. dlo says:

    @m0rgen and others using his code: looks like you’re fetching the entire contents of the file into memory–a large file will make php process balloon. Elsewhere I’ve seen folks suggest stream_copy_to_stream() directly between php://input and the output file handle. On the php man page for that function, ‘felix’ wrote a custom function using an fread loop that appears to be even more memory efficient: http://php.net/manual/en/function.stream-copy-to-stream.php

  26. dlo says:

    dear ninja- your work is impressive. May I suggest a new related challenge? I’ve been investigating drag and drop of images between browser windows–trying to get image data so that I can upload it to server. Been largely stymied by lack of image data in dataTransfer object–I just see URL–and cross-domain security seems to block the constructive use of that URL.

    Gmail also fails to make it work when dragging within the same browser (chrome to chrome, or firefox to firefox), but if you drag an image from firefox into chrome, it does attach the file properly–at least on windows. I’m guessing windows is using its clipboard bitmap format, and chrome just happens to expose that in the dataTransfer object.

    I wonder if I’ve hit the end of the line with this investigation, or if there is some additional cleverness you might be able to make intra-browser cross-domain image dragging work.

  27. @dlo – Would have to look into that and see what can be done.

  28. CYMA says:

    Hey, love your site. Great examples and ideas.
    I tried to set the width and height of the html file input but it doesn’t seem to work (tested in chrome and ff). Is this no longer possible?

Leave a comment