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

Post Comment