Creating a Complete HTML5 Drag and Drop File Uploader with jQuery

August 7, 2012 at 5:27 pm By
Download Demo

Today we’re going to be creating a file uploader using HTML5 drag and drop, along with the file reader API and some PHP. We’ll also be using local storage to remember which files were uploaded by the user.

Getting Started

For this tutorial I’m using jQuery and an icon font called symbolset. If you don’t want to get symbolset you could use an alternative or just not use a symbol font all together! So first off, we need to include the appropriate files in our index.html file:

<link rel="stylesheet" type="text/css" href="style.css" />
<link rel="stylesheet" type="text/css" href="ss-standard.css" />
<script src="jquery.js"></script>
<script src="javascript.js"></script>

Next lets do the main HTML in the body:

	<div id="drop-files" ondragover="return false"> <!-- ondragover for firefox -->
		Drop Images Here
	</div>
	
	<div id="uploaded-holder">
		<div id="dropped-files">
			<div id="upload-button">
				<a href="#" class="upload"><i class="ss-upload"> </i> Upload!</a>
				<a href="#" class="delete"><i class="ss-delete"> </i></a>
				<span>0 Files</span>
			</div>
		</div>
		<div id="extra-files">
			<div class="number">
				0
			</div>
			<div id="file-list">
				<ul></ul>
			</div>
		</div>
	</div>
	
	<div id="loading">
		<div id="loading-bar">
			<div class="loading-color"> </div>
		</div>
		<div id="loading-content">Uploading file.jpg</div>
	</div>
	
	<div id="file-name-holder">
		<ul id="uploaded-files">
			<h1>Uploaded Files</h1>
		</ul>
	</div>

There is also a little bit of simple CSS.

#drop-files {
	width: 400px;
	height: 125px;
	background: rgba(0,0,0,0.1);
	border-radius: 10px;
	border: 4px dashed rgba(0,0,0,0.2);
	padding: 75px 0 0 0;
	text-align: center;
	font-size: 2em;
	float: left;
	font-weight: bold;
	margin: 0 20px 20px 0;
}

#dropped-files {
	float: left;
	position: relative;
	width: 560px;
	height: 125px;
}

#upload-button {
	position: absolute;
	top: 87px;
	z-index: 9999;
	width: 210px;
	display: none;
}

#dropped-files .image {
	height: 200px;
	width: 300px;
	border: 4px solid #fff;
	position: absolute;
	box-shadow: 0px 0px 10px rgba(0,0,0,0.1);
	background: #fff;
	border-radius: 4px;
	overflow: hidden;
}

#upload-button .ss-upload {
	font-size: 0.7em;
}

#upload-button a {
	text-decoration: none;
	color: #fff;
	font-weight: bold;
	box-shadow: 0 0 1000px 62px rgba(255, 255, 255, 1), inset 0 -35px 40px -10px #0A9FCA;
	font-size: 20px;
	padding: 10px 20px;
	background-color: #4bc1e3;
	border-radius: 10px;
}

#upload-button span {
	display: block;
	width: 160px;
	text-align: center;
	margin: 20px 0 0 0;
	background: white;
	border-radius: 10px;
	font-size: 1.1em;
	padding: 4px 0;
	position: relative;
	left: -14px;
}
#upload-button a:hover {
	box-shadow: 0 0 1000px 62px rgba(255, 255, 255, 1), inset 0 -5px 40px 0px #0A9FCA;	
}

#extra-files {
	display: none;
	float: left;
	position: relative;
}

#extra-files .number {
	background: rgba(0,0,0,0.6);
	border-radius: 4px;
	display: inline-block;
	position: relative;
	font-weight: bold;
	color: #fff;
	padding: 20px 30px;
	margin: 60px 0 0 0;
	cursor: pointer;
	font-size: 30px;
}

#dropped-files #upload-button .delete {
	padding: 7px 6px 4px 6px;
	border-radius: 100px;
	background: rgba(0,0,0,0.6);
	box-shadow: none;
	font-size: 1em;
	margin-left: 8px;
}

#dropped-files #upload-button .delete:hover {
	background: rgba(0,0,0,0.8);
}


#extra-files .number:after {
	position: absolute;
	content: " ";
	top: 18px;
	left: -40px;
	display: block;
	border: 20px solid;
	border-color: transparent rgba(0, 0, 0, 0.6) transparent transparent;
}

#extra-files #file-list {
	display: none;
	background: white;
	padding: 20px 0;
	border-radius: 5px;
	box-shadow: 0 0 15px rgba(0,0,0,0.1);
	width: 250px;
	top: 100px;
	border: 1px solid #dadada;
	left: -10px;
	left: -16px;
	max-height: 220px;
	top: 150px;
	position: absolute;
	color: #545454;
}

#file-list ul {
	overflow: scroll;
	padding: 0;
	border-top: 1px solid #dadada;
	max-height: 200px;
	width: 250px;
	list-style: none;
	border-bottom: 1px solid #dadada !important;
}


#file-list ul li:last-of-type {
	border-bottom: 0 !important;
}

#uploaded-holder {
	width: 700px;
	height: 250px;
	display: none;
	float: left;
}

#extra-files #file-list:after, #extra-files #file-list:before {
	position: absolute;
	content: " ";
	top: -40px;
	left: 40px;
	display: block;
	border: 20px solid;
	border-color: transparent transparent #ffffff transparent;
}

#extra-files #file-list:before {
	border-color: transparent transparent #dadada transparent;
	top: -41px;
}

#extra-files #file-list li {
	border-bottom: 1px solid #eee;
	font-weight: bold;
	font-size: 1.5em;
	padding: 10px;
}

#loading {
	display: none;
	float: left;
	width: 100%;
	position: relative;
}

#loading-bar {
	width: 404px;
	height: 40px;
	background: #fff;
	box-shadow: 0 0 15px rgba(0,0,0,0.1);
	border-radius: 5px;
	padding: 2px;
}

.loading-color {
	width: 0%;
	height: 100%;
	-webkit-transition: all 0.1s ease-in;
	-moz-transition: all 0.1s ease-in;
	-ms-transition: all 0.1s ease-in;
	-o-transition: all 0.1s ease-in;
	transition: all 0.1s ease-in;
	border-radius: inherit;
	background-color: #4edbf1;
}

#loading-content {
	position: absolute;
	top: 15px;
	font-size: 1.2em;
	font-weight: bold;
	text-align: center;
	width: 405px;
}

#file-name-holder {
	width: 100%;
	float: left;
}

#file-name-holder h1 {
	text-align: center;
	border-bottom: 1px solid #dadada;
	padding: 20px 0;
	font-size: 3em;
	margin: 0;
}

#uploaded-files {
	background: white;
	border-radius: 5px;
	box-shadow: 0 0 15px rgba(0,0,0,0.1);
	width: 407px;
	top: 100px;
	padding: 0;
	border: 1px solid #dadada;
	max-height: 320px;
	overflow: scroll;
	color: #545454;
}

#uploaded-files li {
	padding: 10px;
	border-bottom: 1px solid #eee;
	font-size: 1.5em;
	font-weight: bold;
	line-height: 25px;
	color: #545454;
}

#uploaded-files a {
	color: #1bacbf;
}

jQuery

Effectively what we want to do is get the Data URI for all the images that the user drags into the drag area. Data URIs represent the data of the image. Basically, what we want to do at the simplest level is put all the data URIs the user drags into the box into an array, and post it to a PHP file. This PHP file will process the URIs and upload them to the server.

To begin, we need to initiate some variables:

$(document).ready(function() {
	
	// Makes sure the dataTransfer information is sent when we
	// Drop the item in the drop box.
	jQuery.event.props.push('dataTransfer');
	
	var z = -40;
	// The number of images to display
	var maxFiles = 5;
	var errMessage = 0;
	
	// Get all of the data URIs and put them in an array
	var dataArray = [];
	

Next we need to bind a function to the drop event. This will mean when the user drops something onto the drop area we can run a function to do what we want to do. We want to support multiple file uploads so we will run each in jQuery to run a function for each file dragged into the drag area.

	$('#drop-files').bind('drop', function(e) {
			
		// This variable represents the files that have been dragged
		// into the drop area
		var files = e.dataTransfer.files;
		
		// Show the upload holder
		$('#uploaded-holder').show();
		
		// For each file 
		$.each(files, function(index, file) {

Now we want to check for any errors. If the file isn’t an image, we will show a little quirky error in the drop box.

			// Some error messaging
			if (!files[index].type.match('image.*')) {
				
				if(errMessage == 0) {
					$('#drop-files').html('Hey! Images only');
					++errMessage
				}
				else if(errMessage == 1) {
					$('#drop-files').html('Stop it! Images only!');
					++errMessage
				}
				else if(errMessage == 2) {
					$('#drop-files').html("Can't you read?! Images only!");
					++errMessage
				}
				else if(errMessage == 3) {
					$('#drop-files').html("Fine! Keep dropping non-images.");
					errMessage = 0;
				}
				return false;
			}

When the user drops an image in the drop box, the first 5 will be shown as images. All the rest will be shown as a little +8 which when clicked will show a drop down of the files uploaded. On top of the first 5 images we’ll place the upload button. To make sure the upload button is positioned correctly, we check the position based on how many files are in the dropped files area.

			if($('#dropped-files > .image').length < maxFiles) {
				// Change position of the upload button so it is centered
				var imageWidths = ((220 + (40 * $('#dropped-files > .image').length)) / 2) - 20;
				$('#upload-button').css({'left' : imageWidths+'px', 'display' : 'block'});
			}

Now we can run the file reader function. First off, push the correct data into the array and move the dropped images so they appear to be overlapping.

			var fileReader = new FileReader();
				
				// When the filereader loads initiate a function
				fileReader.onload = (function(file) {
					
					return function(e) { 
						
						// Push the data URI into an array
						dataArray.push({name : file.name, value : this.result});
						
						// Move each image 40 more pixels across
						z = z+40;
						// This is the image
						var image = this.result;

The rest of this function is to do with grammar and placing the images in the correct div.

						// Just some grammatical adjustments
						if(dataArray.length == 1) {
							$('#upload-button span').html("1 file to be uploaded");
						} else {
							$('#upload-button span').html(dataArray.length+" files to be uploaded");
						}
						// Place extra files in a list
						if($('#dropped-files > .image').length < maxFiles) { 
							// Place the image inside the dropzone
							$('#dropped-files').append('<div class="image" style="left: '+z+'px; background: url('+image+'); background-size: cover;"> </div>'); 
						}
						else {
							
							$('#extra-files .number').html('+'+($('#file-list li').length + 1));
							// Show the extra files dialogue
							$('#extra-files').show();
							
							// Start adding the file name to the file list
							$('#extra-files #file-list ul').append('<li>'+file.name+'</li>');
							
						}
					}; 
					
				})(files[index]);
				
			// For data URI purposes
			fileReader.readAsDataURL(file);
	
		});
		

	});

Next we need a function which will restart everything to its default position and value. We’ll run this when the user presses cancel or finishes an upload.

	function restartFiles() {
	
		// This is to set the loading bar back to its default state
		$('#loading-bar .loading-color').css({'width' : '0%'});
		$('#loading').css({'display' : 'none'});
		$('#loading-content').html(' ');
		// --------------------------------------------------------
		
		// We need to remove all the images and li elements as
		// appropriate. We'll also make the upload button disappear
		
		$('#upload-button').hide();
		$('#dropped-files > .image').remove();
		$('#extra-files #file-list li').remove();
		$('#extra-files').hide();
		$('#uploaded-holder').hide();
	
		// And finally, empty the array/set z to -40
		dataArray.length = 0;
		z = -40;
		
		return false;
	}

Now we need to figure out what to do when the user presses the upload button. As I mentioned, we want to pass the data to a PHP file. We’ll also show a little CSS loading bar. We’re using each again to upload each file one by one. This is where our data URI array comes into play.

	$('#upload-button .upload').click(function() {
		
		// Show the loading bar
		$("#loading").show();
		// How much each element will take up on the loading bar
		var totalPercent = 100 / dataArray.length;
		// File number being uploaded
		var x = 0;
		var y = 0;
		
		// Show the file name
		$('#loading-content').html('Uploading '+dataArray[0].name);
		
		
		// Upload each file separately
		$.each(dataArray, function(index, file) {	

We want to post the data to a file called upload.php (create this file!). We use jQuery’s AJAX functions to accomplish this. In the callback function for the post, we’ll update the loading bar and check if the loading process is finished completely.

			// Post to the upload.php file
			$.post('upload.php', dataArray[index], function(data) {
			
				// The name of the file
				var fileName = dataArray[index].name;
				++x;
				
				// Change the loading  bar to represent how much has loaded
				$('#loading-bar .loading-color').css({'width' : totalPercent*(x)+'%'});
				
				if(totalPercent*(x) == 100) {
					// Show the upload is complete
					$('#loading-content').html('Uploading Complete!');
					
					// Reset everything when the loading is completed
					setTimeout(restartFiles, 500);
					
				} else if(totalPercent*(x) < 100) {
				
					// Show that the files are uploading
					$('#loading-content').html('Uploading '+fileName);
				
				}

The PHP file will return some data which we must interpret. One piece of this is the name we generate for the file, and the other is whether or not the file upload was successful. This data is separated by a colon so we have to separate it into an array and use the appropriate data. We then combine all of this data and append it to the ‘uploaded files’ section.

				// Show a message showing the file URL.
				var dataSplit = data.split(':');
				if(dataSplit[1] == 'uploaded successfully') {
					var realData = '<li><a href="images/'+dataSplit[0]+'">'+fileName+'</a> '+dataSplit[1]+'</li>';
					
					$('#uploaded-files').show();
					
					$('#uploaded-files').append('<li><a href="images/'+dataSplit[0]+'">'+fileName+'</a> '+dataSplit[1]+'</li>');

Another important thing to note is we add each file to the local storage. We can then call this later so the user can still see his or her uploaded files when they leave and come back.

					// Add things to local storage 
					if(window.localStorage.length == 0) {
						y = 0;
					} else {
						y = window.localStorage.length;
					}
					
					// We set this item in the local storage
					window.localStorage.setItem(y, realData);
				
				} else {
					$('#uploaded-files').append('<li><a href="images/'+data+'. File Name: '+dataArray[index].name+'</li>');
				}
				
			});
		});
		
		return false;
	});

The next little bit is mostly to do with aesthetics and showing the extra files drop down (when the user clicks the +.. bit after the images).

	// Just some styling for the drop file container.
	$('#drop-files').bind('dragenter', function() {
		$(this).css({'box-shadow' : 'inset 0px 0px 20px rgba(0, 0, 0, 0.1)', 'border' : '4px dashed #bb2b2b'});
		return false;
	});
	
	$('#drop-files').bind('drop', function() {
		$(this).css({'box-shadow' : 'none', 'border' : '4px dashed rgba(0,0,0,0.2)'});
		return false;
	});
	
	// For the file list
	$('#extra-files .number').toggle(function() {
		$('#file-list').show();
	}, function() {
		$('#file-list').hide();
	});

	// Restart files when the user presses the delete button
	$('#dropped-files #upload-button .delete').click(restartFiles);


To finish up, we append the local storage items to the uploaded data section.

	// Append the localstorage the the uploaded files section
	if(window.localStorage.length > 0) {
		$('#uploaded-files').show();
		for (var t = 0; t < window.localStorage.length; t++) {
			var key = window.localStorage.key(t);
			var value = window.localStorage[key];
			// Append the list items
			if(value != undefined || value != '') {
				$('#uploaded-files').append(value);
			}
		}
	} else {
		$('#uploaded-files').hide();
	}
});

And that’s all the Javascript! The next bit is uploading the data URI to the server, and for this we need to use PHP.

PHP

The PHP isn’t particularly hard. Our PHP file will be called upload.php. The data URI will be encoded using base 64. We have to decode it to begin with by taking apart the data URI, grabbing the data and changing it as appropriate. After that we find the mime type, create a random name, and move the file into a variable. From there we can upload the file to a server and show a success or failure message. For this particular tutorial we’re using a folder called images to store all our images in.

<?php

// We're putting all our files in a directory called images.
$uploaddir = 'images/';

// The posted data, for reference
$file = $_POST['value'];
$name = $_POST['name'];

// Get the mime
$getMime = explode('.', $name);
$mime = end($getMime);

// Separate out the data
$data = explode(',', $file);

// Encode it correctly
$encodedData = str_replace(' ','+',$data[1]);
$decodedData = base64_decode($encodedData);

// You can use the name given, or create a random name.
// We will create a random name!

$randomName = substr_replace(sha1(microtime(true)), '', 12).'.'.$mime;


if(file_put_contents($uploaddir.$randomName, $decodedData)) {
	echo $randomName.":uploaded successfully";
}
else {
	// Show an error message should something go wrong.
	echo "Something went wrong. Check that the file isn't corrupted";
}


?>

And now we’re done! Try out the demo (I’ve disabled uploading to the server so you wont be able to access your uploaded files on this website), or you can download the files and try it yourself (everything will work in the download). I hope you’ve enjoyed this tutorial!

Support

As you’d imagine, this doesn’t work in the latest stable release of internet explorer. However, it should work fine in any other modern browser.

Webkit Gecko Trident Presto
Yes Yes No Yes