CastlesBlog.com

a blog about a blog

Subscribe

HTML5 Drag and Drop Upload

I've been playing around with drag and drop in HTML5 and I'm pretty excited about it. A lot of the examples I've found online are firefox specific which isn't ideal so I've come up with a solution that works in as many browsers as possible.

Browser Support

FirefoxSafariChromeWebkit

Support for drag and drop & the file API is limited to cutting edge browsers and as the spec isn't finalised features are inconsistent... to say the least.

I've come up with a solution thats tested and works in Firefox 3.6, Safari 5, Chrome 6 and Webkit. I'll be adding support for Opera and IE if/when its available.

What I've discovered:

Because of these issues I've coded three ways to upload files.

  1. Browsers that support sendAsBinary send the data using proper headers and PHP can easily read the $_FILES array
  2. Browsers that support FileReader send the file as base64 encoded string. The file information is passed in via headers
  3. Browsers that don't support FileReader send the file itself and is read the same way as no. 2

The Code

So, without any further ado, here is a demo. And here is the code:

<?php
$upload_folder = 'data';

if(count($_FILES)>0) { //browser supported sendAsBinary()
	if( move_uploaded_file( $_FILES['upload']['tmp_name'] , $upload_folder.'/'.$_FILES['upload']['name'] ) ) {
		echo 'done';
	}
	exit();
} else if(isset($_GET['up'])) {

	if(isset($_GET['base64'])) {
		$content = base64_decode(file_get_contents('php://input'));
	} else {
		$content = file_get_contents('php://input');
	}

	$headers = getallheaders();
	
	$headers = array_change_key_case($headers, CASE_UPPER); //different case was being used for different browsers
	
	if(file_put_contents($upload_folder.'/'.$headers['UP-FILENAME'], $content)) {
		echo 'done';
	}
	exit();
}
?>
<!DOCTYPE html>
<html>
<head>
    <meta charset=utf-8>
    <title>HTML5 Upload Demo</title>
    
    <style>
    body{font-family:Helvetica,Arial;font-size:12px}
	#drop-area{border:2px solid black;padding:10px;background-size:contain;min-height:200px;overflow:auto}
	#drop-area.hover{border:2px dashed red}
	#drop-area div{width:150px;height:150px;border:1px solid #CCC;font-size:.5em;padding:5px;background-position:center;background-repeat:no-repeat;background-size:contain;float:left;margin:10px;word-wrap:break-word}
	#status {background-color:black;color:white;padding:5px 20px;margin-bottom:10px}
	</style>
    
	<!--[if lt IE 9]>
	<script src=http://html5shiv.googlecode.com/svn/trunk/html5.js></script>
	<![endif]-->
	
	<script src=http://ajax.googleapis.com/ajax/libs/jquery/1.4.2/jquery.min.js></script>
</head>

<body>

<div id=status>Drag and Drop files to begin...</div>
<div id=drop-area></div>

<script>

var up = {
	
	$drop :			null,
	queue :			[],
	processing :	null,
	uploading :		false,
	binaryReader :	null,
	dataReader :	null,
	xhr:			null,

	init : function() {
		up.$drop = $('#drop-area');
		
		up.$drop.bind('dragenter',up.enter);
		up.$drop.bind('dragleave',up.leave);
		up.$drop.bind('dragover',up.over);
		up.$drop.bind('drop',up.drop);
		
		$('#status').click(up.cancel);
		
		up.xhr = new XMLHttpRequest();
		up.xhr.upload.addEventListener('progress', up.uploadProgress , false);
		up.xhr.upload.addEventListener('load', up.uploadLoaded , false);
		
	},
	
	enter : function(e){
		$(e.target).addClass('hover');
		return false;
	},
	
	leave : function(e){
		$(e.target).removeClass('hover');
		return false;
	},
	
	over : function(e){
		return false;
	},
	
	drop : function(e){
		$(e.target).removeClass('hover');
		
		var files = e.originalEvent.dataTransfer.files;
		for (var i = 0; i<files.length; i++) {
			var file = files[i];
			up.queue.push(file);
		}

		if(up.uploading == false) {
			up.uploading = true;
			up.process();
		}
		
		return false;
	},
	
	process : function() {
		up.processing = up.queue.shift();
		
		up.$drop.append('<div>'+up.processing.name+'</div>');
		
		if(window.FileReader) { //firefox 3.6, Chrome 6, Webkit

			if(up.processing.type.match(/image/gi) != null) { //is an image - read it
				up.dataReader = new FileReader();
				if(up.dataReader.addEventListener) { //firefox
					up.dataReader.addEventListener('loadend', up.binaryLoad, false);
					up.dataReader.addEventListener('error', up.loadError, false);
					up.dataReader.addEventListener('progress', up.loadProgress, false);
				} else { //chrome / webkit
					up.dataReader.onloadend = up.binaryLoad;
					up.dataReader.onerror = up.loadError;
					up.dataReader.onprogress = up.loadProgress;
				}
				up.dataReader.readAsDataURL(up.processing);
			}
			
			up.binaryReader = new FileReader();
			if(up.binaryReader.addEventListener) { //firefox
				up.binaryReader.addEventListener('loadend', up.binaryLoad, false);
				up.binaryReader.addEventListener('error', up.loadError, false);
				up.binaryReader.addEventListener('progress', up.loadProgress, false);
			} else { //chrome / webkit
				up.binaryReader.onloadend = up.binaryLoad;
				up.binaryReader.onerror = up.loadError;
				up.binaryReader.onprogress = up.loadProgress;
			}
			up.binaryReader.readAsBinaryString(up.processing);
			
			
		} else { // safari 5 + others?
		
			up.xhr.abort(); //make sure xhr is a new request
			up.xhr.open('POST', '/html5_upload.php?up=true', true);
		
			up.xhr.setRequestHeader('UP-FILENAME', up.processing.name);
			up.xhr.setRequestHeader('UP-SIZE', up.processing.size);
			up.xhr.setRequestHeader('UP-TYPE', up.processing.type);
			
			up.xhr.send(up.processing); 
			up.xhr.onload = up.onload;
		}
		
	},
	
	loadError : function(e) {
		switch(e.target.error.code) {
			case e.target.error.NOT_FOUND_ERR:
				alert('File Not Found!');
			break;
			case e.target.error.NOT_READABLE_ERR:
				alert('File is not readable');
			break;
			case e.target.error.ABORT_ERR:
			break; 
			default:
				alert('An error occurred reading this file.');
		};

	},
	
	loadProgress : function(e) {
		if (e.lengthComputable) {
			var percentage = Math.round((e.loaded * 100) / e.total);
			$('#status').html('loaded: '+percentage+'%');
		}
	},
		
	binaryLoad : function(e) {
		
		var isimage = (up.processing.type.match(/image/gi)!=null);
		if( isimage && up.dataReader.readyState == 2 && up.$drop.find('div:last').css('background-image')=='none') {
			up.$drop.find('div:last').css('background-image','url(' + up.dataReader.result + ')');	
		}
		
		if(isimage && up.dataReader.readyState == 2 && up.binaryReader.readyState == 2 || !isimage && up.binaryReader.readyState == 2 ) {
			
			up.xhr.abort(); //make sure xhr is a new request
			
			var binary = e.target.result;
			
			if(up.xhr.sendAsBinary != null) { //firefox
			
				up.xhr.open('POST', '/html5_upload.php?up=true', true);
	
				var boundary = 'xxxxxxxxx';
			    
				var body = '--' + boundary + "\r\n";  
				body += "Content-Disposition: form-data; name='upload'; filename='" + up.processing.name + "'\r\n";  
				body += "Content-Type: application/octet-stream\r\n\r\n";  
				body += binary + "\r\n";  
				body += '--' + boundary + '--';  
			    
			    up.xhr.setRequestHeader('content-type', 'multipart/form-data; boundary=' + boundary);
			    up.xhr.sendAsBinary(body);        	
			    
		    } else { //for browsers that don't support sendAsBinary yet
			 
			 	up.xhr.open('POST', '/html5_upload.php?up=true&base64=true', true);
			 
			 	up.xhr.setRequestHeader('UP-FILENAME', up.processing.name);
			 	up.xhr.setRequestHeader('UP-SIZE', up.processing.size);
			 	up.xhr.setRequestHeader('UP-TYPE', up.processing.type);
			 	
				up.xhr.send(window.btoa(binary)); 
		    }
			 
			up.xhr.onload = up.onload;
			
		}
	},
	
	uploadProgress : function(e) {
		if (e.lengthComputable) {
			var percentage = Math.round((e.loaded * 100) / e.total);
			$('#status').html('uploaded: '+percentage+'%');
		}
	},
	
	uploadLoaded : function(e) {
		$('#status').html('Uploaded: 100%');
	},
	
	onload : function (e) {
		if(up.queue.length > 0) {
			up.process();
		} else {
			up.uploading = false;
			$('#status').html('Queue Uploaded');
		}
	},
	
	cancel : function(e) {
		if(up.dataReader) {
			up.dataReader.abort();
		}
		if(up.dataReader) {
			up.binaryReader.abort();
		}
		if(up.xhr) {
			up.xhr.abort();
		}
		up.uploading = false;
		up.queue = [];
		up.processing = null;
		$('#status').html('Drag and Drop files to begin...');
		return false;
	}

}

$(up.init);

</script>
</body>
</html>

To keep it simple there is no size or filetype limit but could be added fairly easily. If the browser has FileReader support and the dropped file is an image it will read contents and display a preview.

If you have any questions please post them in the comments.

References

Comments:

  1. Bakudan - 31st October 2010

    It won`t work on IE, mainly because there is no "file" property like in Mozilla - https://developer.mozilla.org/En/DragDrop/DataTransfer
    http://msdn.microsoft.com/en-us/library/ms535861(v=VS.85).aspx
    And there is one thing that maybe is good idea - change "up.xhr = new XMLHttpRequest();" with the example from http://msdn.microsoft.com/en-us/library/ms535874(VS.85).aspx
    Nevertheless this is the best example I`ve found so far.

  2. TechJunkie99 - 20th November 2010

    Excellent work! The first demo I've seen that utilizes the sendAsBinary for firefox AND includes upload php code. Hopefully all browsers support the sendAsBinary soon so that it'll be easier to update currrent systems to use the new file upload since the same backend code using $_FILES will work.

  3. scott - 2nd December 2010

    I tried on safari 5 on mac os x but doesnt seem to work...

    but otherwise amazing work. thanks!

  4. Marc - 3rd January 2011

    Thats correct Bakudan, Microsoft haven't added any support for the HTML5 File API... even in IE9beta. Fingers crossed for the final release.

    Scott.. it works for me in Safari 5, are you getting any errors?

  5. max - 6th January 2011

    i got the drag and drop to work even uploaded the file to server succesfully, problem is i dont know how to get the Data the server produces after given the file .. json

    Please help. a code sample would be awesome.

  6. Marc - 7th January 2011

    Hi max. I\'m not quite sure what you mean. The above script creates the file and you can do whatever with it. Using php or whatnot.

  7. max - 9th January 2011

    I finally figured it out. Absolutely amazing work !!! Thank you so much!!!

  8. chris hansen - 10th January 2011

    Hey,

    how can we get the response from the php uploader file and append it to a div?

  9. Marc - 10th January 2011

    Hi Chris, you may be able to read a second parameter int the upload Loaded function. I think that will work but I haven\'t tested this.

  10. Chris Hansen - 10th January 2011

    Hey, thanks for your answer;

    Here is the code :
    uploadLoaded : function(e) {
    $('#status').html('Uploaded: 100%');
    $('#innit').append(''+up.xhr.responseText+'
    ');
    },
    However, up.xhr.responseText is blank (and the upload script works fine and echoes the right answer.

    Do you have any idea on how I could implement this?

    Thank you

  11. fresnillo - 16th February 2011

    I have test your solution and I have to said that it doesn't work in Safari (4 and 5). It doesn't work because Safari doesn't support drag and drop events, not only FileReader.
    In IE you can not read the file because it doesn't support File, FileReader, FileList and Blob and you have to use an ActiveXObject('Scripting.FileSystemObject').
    It isn't a good idea but... there is no choise with IE.

  12. söve - 19th August 2011

    Thank u for posting

  13. Joseh - 8th October 2011

    Hi,

    I'd like to process some JSON as a response when a file is uploaded, which is rendered on the php side. How would I retrieve this json using client side javascript, i think this is also what max meant, i tried up.xhr.responseText in uploadLoaded but i didn't get anything (blank response).

    Please help me out!

  14. Marc - 8th October 2011

    Hi Joseh, I think you'll need to send a separate ajax request once the upload is complete because of the different ways the upload is handled in different browsers.

  15. Ramanan G - 14th October 2011

    I tried google chrome 7.0 ,Opera 10 browser on fedora , but doesnt seem to work... and I tried on firefox browser , drag and dropped the multiple images . Images was seem's on FF browser, but only one image was saved perfectly on folder other's was not stored.

  16. cenk - 12th December 2012

    All are well, but I've come across to such a problem.. When the drop target is an <input> tagged element, the OPERA browser causes troubles in event handling. The (drop) event is not caught, and if the dropped item is a file from own computer, then it opens the file in the browser. IS there a way to avoid it? Any ideas? I've tried cancelBubble, stopPropagation, and many others...but useless....is it a problem with OPERA? 'cos FF,Chrome works excellent even with <input> tagged drop targets.

  17. sundar - 1st October 2013

    Great article. Useful information.
    http://www.cavinitsolutions.com/

Post Comment