Utilizing the HTML5 File API to choose, upload, preview and see progress for multiple files

To me, something about HTML5 that makes it quite interesting is all the new support for file interaction. I’ve written about the File API and reading file information before, and I thought I’d expand on that and add uploads and progress bars.

Functionality

In my File API for uploading and reading file information for multiple files demo, as part of my Information and samples for HTML5 and related APIs demo web site, I’ve added two things:

  • An input filed with the type="file" and having the multiple attribute set to be able to choose more than one file at a time.
  • A drop area, i.e. element that you can drop files from your desktop/Finder/Explorer and upload them right away.

Please note that while files are uploaded in my demo, they are not actually saved on the server. It’s only to be able for you to see the file upload in Firebug/Web Inspector etc and also to present progress events.

Code needed

First, some simple HTML code:

<p>
	<input id="files-upload" type="file" multiple>
</p>
<p id="drop-area">
	<span class="drop-instructions">or drag and drop files here</span>
	<span class="drop-over">Drop files here!</span>
</p>

The span elements are shown respectively hidden to give instructions and user feedback.

Adding change and drag events

Then we add events for detecting when the user has chosen any number of files for the input element or have dropped files on the drop area:

	filesUpload.addEventListener("change", function () {
		traverseFiles(this.files);
	}, false);
	
	dropArea.addEventListener("dragleave", function (evt) {
		var target = evt.target;
		
		if (target && target === dropArea) {
			this.className = "";
		}
		evt.preventDefault();
		evt.stopPropagation();
	}, false);
	
	dropArea.addEventListener("dragenter", function (evt) {
		this.className = "over";
		evt.preventDefault();
		evt.stopPropagation();
	}, false);
	
	dropArea.addEventListener("dragover", function (evt) {
		evt.preventDefault();
		evt.stopPropagation();
	}, false);
	
	dropArea.addEventListener("drop", function (evt) {
		traverseFiles(evt.dataTransfer.files);
		this.className = "";
		evt.preventDefault();
		evt.stopPropagation();
	}, false);

Don’t worry about all the events for drag and drop, they just have to be there, ok? :-)

Traversing files

We then have access to the files, either through a files property on the input element, or as a files property on the evt.dataTransfer property for drag and drop events:

function traverseFiles (files) {
	if (typeof files !== "undefined") {
		for (var i=0, l=files.length; i<l; i++) {
			uploadFile(files[i]);
		}
	}
	else {
		fileList.innerHTML = "No support for the File API in this web browser";
	}	
}

Reading file information and previewing image files

For each chosen file we can then display three things for them:

  • File name
  • File size (in bytes)
  • File type
// Present file info and append it to the list of files
fileInfo = "
Name: " + file.name + "
"; fileInfo += "
Size: " + parseInt(file.size / 1024, 10) + " kb
"; fileInfo += "
Type: " + file.type + "
"; div.innerHTML = fileInfo;

An extra nifty feature, for any of the files that are an image, we can immediately read out the image data and display a preview of that image:

/*
	If the file is an image and the web browser supports FileReader,
	present a preview in the file list
*/
if (typeof FileReader !== "undefined" && (/image/i).test(file.type)) {
	img = document.createElement("img");
	li.appendChild(img);
	reader = new FileReader();
	reader.onload = (function (theImg) {
		return function (evt) {
			theImg.src = evt.target.result;
		};
	}(img));
	reader.readAsDataURL(file);
}

Uploading files

What we use to upload the files is a regular XMLHttpRequest object, and post the form appropriately:

// Uploading - for Firefox, Google Chrome and Safari
xhr = new XMLHttpRequest();
xhr.open("post", "upload/upload.php", true);

// Set appropriate headers
xhr.setRequestHeader("Content-Type", "multipart/form-data");
xhr.setRequestHeader("X-File-Name", file.name);
xhr.setRequestHeader("X-File-Size", file.size);
xhr.setRequestHeader("X-File-Type", file.type);

// Send the file (doh)
xhr.send(file);

Progress and load events

To complement our file upload, it’s nice to show the upload progress for each file, and also detect when they have finished uploading. That is done by the help of the progress and load events:

// Update progress bar
xhr.upload.addEventListener("progress", function (evt) {
	if (evt.lengthComputable) {
		progressBar.style.width = (evt.loaded / evt.total) * 100 + "%";
	}
	else {
		// No data to calculate on
	}
}, false);

// File uploaded
xhr.addEventListener("load", function () {
	progressBarContainer.className += " uploaded";
	progressBar.innerHTML = "Uploaded!";
}, false);

The complete code

Here’s the complete code for the example:

<h3>Choose file(s)</h3>
<p>
	<input id="files-upload" type="file" multiple>
</p>
<p id="drop-area">
	<span class="drop-instructions">or drag and drop files here</span>
	<span class="drop-over">Drop files here!</span>
</p>

<ul id="file-list">
	<li class="no-items">(no files uploaded yet)</li>
</ul>
(function () {
	var filesUpload = document.getElementById("files-upload"),
		dropArea = document.getElementById("drop-area"),
		fileList = document.getElementById("file-list");
		
	function uploadFile (file) {
		var li = document.createElement("li"),
			div = document.createElement("div"),
			img,
			progressBarContainer = document.createElement("div"),
			progressBar = document.createElement("div"),
			reader,
			xhr,
			fileInfo;
			
		li.appendChild(div);
		
		progressBarContainer.className = "progress-bar-container";
		progressBar.className = "progress-bar";
		progressBarContainer.appendChild(progressBar);
		li.appendChild(progressBarContainer);
		
		/*
			If the file is an image and the web browser supports FileReader,
			present a preview in the file list
		*/
		if (typeof FileReader !== "undefined" && (/image/i).test(file.type)) {
			img = document.createElement("img");
			li.appendChild(img);
			reader = new FileReader();
			reader.onload = (function (theImg) {
				return function (evt) {
					theImg.src = evt.target.result;
				};
			}(img));
			reader.readAsDataURL(file);
		}
		
		// Uploading - for Firefox, Google Chrome and Safari
		xhr = new XMLHttpRequest();
		
		// Update progress bar
		xhr.upload.addEventListener("progress", function (evt) {
			if (evt.lengthComputable) {
				progressBar.style.width = (evt.loaded / evt.total) * 100 + "%";
			}
			else {
				// No data to calculate on
			}
		}, false);
		
		// File uploaded
		xhr.addEventListener("load", function () {
			progressBarContainer.className += " uploaded";
			progressBar.innerHTML = "Uploaded!";
		}, false);

		xhr.open("post", "upload/upload.php", true);
		
		// Set appropriate headers
		xhr.setRequestHeader("Content-Type", "multipart/form-data");
		xhr.setRequestHeader("X-File-Name", file.name);
		xhr.setRequestHeader("X-File-Size", file.size);
		xhr.setRequestHeader("X-File-Type", file.type);

		// Send the file (doh)
		xhr.send(file);
		
		// Present file info and append it to the list of files
		fileInfo = "
Name: " + file.name + "
"; fileInfo += "
Size: " + parseInt(file.size / 1024, 10) + " kb
"; fileInfo += "
Type: " + file.type + "
"; div.innerHTML = fileInfo; fileList.appendChild(li); } function traverseFiles (files) { if (typeof files !== "undefined") { for (var i=0, l=files.length; i<l; i++) { uploadFile(files[i]); } } else { fileList.innerHTML = "No support for the File API in this web browser"; } } filesUpload.addEventListener("change", function () { traverseFiles(this.files); }, false); dropArea.addEventListener("dragleave", function (evt) { var target = evt.target; if (target && target === dropArea) { this.className = ""; } evt.preventDefault(); evt.stopPropagation(); }, false); dropArea.addEventListener("dragenter", function (evt) { this.className = "over"; evt.preventDefault(); evt.stopPropagation(); }, false); dropArea.addEventListener("dragover", function (evt) { evt.preventDefault(); evt.stopPropagation(); }, false); dropArea.addEventListener("drop", function (evt) { traverseFiles(evt.dataTransfer.files); this.className = ""; evt.preventDefault(); evt.stopPropagation(); }, false); })();

Web browser support

Web browser support is pretty good for this – it’s working in:

  • Firefox 3.5+
  • Google Chrome 5.0+
  • Safari 4.0+ (not complete support, and can be shaky/incorrect on Windows – but hey, who uses Safari on Windows anyway? ;-) )
  • Opera 11.10+ (partial support)
  • Internet Explorer 10+

For now, a reasonable fallback could be Plupload, which falls back to other technologies to try and do the trick.

Taking it to the next level

Here you have it. A nice, pretty simple script to read file information, preview thumbnails, upload files and offer the user feedback on the progress. Next steps would probably to be able to cancel uploads, testing out various fallback options for some web browsers etc. But hey, this is a good start, I think. :-)

78 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>