Plupload Custom File Upload UI

Plupload is a tool that allows you to upload files using HTML5 Gears, Silverlight, Flash, BrowserPlus, normal forms, and provides features such as upload progress, image resizing and chunked uploads.

So I’m gonna discuss the process I went through to implement a custom Plupload UI.

Here is a video off it in action:

Now I’ll go into details on how it was built using the Plupload framework.
I modeled my version of the plupload custom example here.

So first of all I Init an instance of plupload and set the options. I set the drop area, and the container that the file progress will be displayed in.
Note: Only FF and Chrome support drag’n drop in Plupload.

$(document).ready(function(){
var uploader = new plupload.Uploader({
	// General settings
	runtimes: 'html5,flash,gears,browserplus,silverlight,html4',
	url:<%= SaveUrl %>,
	browse_button : "pickfiles",
	button_browse_hover : true,
	drop_element : "dropArea",
	autostart : true,
	max_file_size: '100mb',
	container: "FileContainer",
	chunk_size: '1mb',
	unique_names: true,
	// Flash settings
	flash_swf_url: <%= "'"+  Page.ResolveUrl("~/plugins/plupload/plupload.flash.swf") +"'" %>,
	// Silverlight settings
	silverlight_xap_url: <%= '"'+ Page.ResolveUrl("~/plugins/plupload/plupload.silverlight.xap") +'"' %>
});

Plupload doesn’t automatically hide the drop area when the current browser doesn’t support it so I bind some custom logic to the plupload “Init” event below. Drag and drop support requires that the browser support FileReader and will not work under the flash or silverlight runtimes.

uploader.bind('Init', function(up, params){
try{
    if(!!FileReader && !((params.runtime == "flash") || (params.runtime == "silverlight")))
         $("#dropArea").show();
         $("#fileSelectMsg").hide();
    }
catch(err){}});
uploader.init();

So now we need to be able to display files in our “FileContainer” div the script below fires on the FilesAdded event. It is called when a file is added or dropped in the drop area. When this event is fired I create a div that contains the elements we will need to display upload progress and the name of the file.

uploader.bind('FilesAdded', function(up, files) {
	$.each(files, function(i, file) {
		$('#filelist').append(
			'<div id="' + file.id + '" class="fileItem"><div class="name">' +
			file.name + '</div><a href="#" id="cancel'+file.id+'" class="cancel">cancel</a><div class="fileInfo"><span class="size">' + plupload.formatSize(file.size) + '</span>' +
			'<div class="plupload_progress"><div class="plupload_progress_container"><div class="plupload_progress_bar"></div></div></div>'+
			'<span class="percentComplete"></span></div></div>');

		//Fire Upload Event
		up.refresh(); // Reposition Flash/Silverlight
		uploader.start();

		//Bind cancel click event
		$('#cancel'+file.id).click(function(){
			$fileItem = $('#' + file.id);
			$fileItem.addClass("cancelled");
			uploader.removeFile(file);
			$(this).unbind().remove();
		});

		//Set ico_Ok to Uploading
		$confirmNext.attr("disabled", "disabled");
		$confirmNext.html("Uploading..");
		$confirmNext.unbind("click").click(function(e){e.preventDefault();});
	});
});

Now that we have files uploading we need to update the progress bars for each file div in the “FileContainer”. So yet again Plupload conviently has an event called “UploadProgress” that fires after each chunk has been successfully uploaded.

uploader.bind('UploadProgress', function(up, file) {
	var  $fileWrapper = $('#' + file.id);
	$fileWrapper.find(".plupload_progress").show();
	$fileWrapper.find(".plupload_progress_bar").attr("style", "width:"+ file.percent + "%");
	$fileWrapper.find(".percentComplete").html(file.percent+"%");
	$fileWrapper.find('#cancel'+file.id).remove();
});

So now we have files uploading and being added to the container and updating the upload progress in real time. So now I want to grey out the div once the file has been completed. In order to do this I add a class to that file’s div. The “FileUploaded” event gets fired after each file is successfully uploaded.

uploader.bind('FileUploaded', function(up, file) {
	$fileItem = $('#' + file.id);
	$fileItem.addClass("completed");

	$confirmNext.removeAttr("disabled");
	$confirmNext.html("Ok");
	$confirmNext.unbind().click(function(e){window.location.href = window.location.href;});

	$('#cancel'+file.id).unbind().remove();
});

Last but not least I’ll show you how to display errors that happen server side when a chunk is uploaded. On “ChunkUploaded” event I check the response message if it contains “Error:” in the response string. I kill the current upload and display that error message to the user and also log the error in my application. The following snippet performs just this.

uploader.bind("ChunkUploaded", function(up, file, response){
	//Should return a status 200 if the chunk was uploaded successfully
	if(response.status != null)
	{
		if(response.status != "200" || (response.response.indexOf("Error") != -1))
		{
			if(response.response.indexOf("Error") != -1)
			{
				//Prompt the user with the custom error message
				$("div.error:first").show().html('<p>'+response.response+'</p>');
			}
			else
			{
				//Log this as an error
				//Custom line of code to log error on server would go here
				
				//Notify user of error
				$("div.error:first").show().html('<p>There was an error uploading your file '+ file.name +' Support has been notified.</p>');
			}
			$('#' + file.id).addClass("cancelled");
			uploader.stop();
		}
	}    
});

So thats how I built my custom UI wrapper around the Plupload core. Hopefully that will help get you going in the right direction.

Advertisements

The non-breaking space ” “

Add to FacebookAdd to DiggAdd to Del.icio.usAdd to StumbleuponAdd to RedditAdd to BlinklistAdd to TwitterAdd to TechnoratiAdd to Yahoo BuzzAdd to Newsvine

This post may sound elementary to those of you who went through college after the web development was in full swing.  Admittedly, that was just slightly before my time.  So, what was a small epiphany for me may be common knowledge to you, but please pardon my ignorance and delight at what I learned.

Sometimes you just need a little more space somewhere, and fortunately, sometime long ago when I was just starting to work on the web, someone introduced me to the beloved “&nbsp;”.  Since that time, I have used it occasionally whenever it was too tedious to add space another way.

However, I recently needed to have a cell in a header row define the space for the column.  I don’t recall the exact circumstance now, but for whatever reason, defining no wrap wasn’t available.

Enter the often-used but seldom understood &nbsp;.  I needed the text in this column to not break at the spaces.  I was working with another person, when it occurred to me that the non-breaking space is probably a non-breaking space.

I replaced the spaces with &nbsp; and the problem was solved!

Make Table Rows Sortable Using jQuery UI Sortable

Add to FacebookAdd to DiggAdd to Del.icio.usAdd to StumbleuponAdd to RedditAdd to BlinklistAdd to TwitterAdd to TechnoratiAdd to Yahoo BuzzAdd to Newsvine

So you want to make table rows sortable using jQuery UI? Luckily, the Sortable interaction does most of the work for you.

But there’s a catch: one problem that I ran into when implementing this (with UI version 1.7) was the cell widths of the row would collapse once I started dragging it.

Suppose you have a table of data, like this one:

<table id="sort" class="grid" title="Kurt Vonnegut novels">
	<thead>
		<tr><th>Year</th><th>Title</th><th>Grade</th></tr>
	</thead>
	<tbody>
		<tr><td>1969</td><td>Slaughterhouse-Five</td><td>A+</td></tr>
		<tr><td>1952</td><td>Player Piano</td><td>B</td></tr>
		<tr><td>1963</td><td>Cat's Cradle</td><td>A+</td></tr>
		<tr><td>1973</td><td>Breakfast of Champions</td><td>C</td></tr>
		<tr><td>1965</td><td>God Bless You, Mr. Rosewater</td><td>A</td></tr>
	</tbody>
</table>

Your first attempt to make it sortable might look like this:

$("#sort tbody").sortable().disableSelection();

And it actually works, but there is a bit of a problem. The cell widths seem to be collapsing once you start dragging a row (notice how close the “C” cell is to the “Breakfast of Champions” cell). It looks like this:

Sortable row collapsed widths

The problem has to do with the helper object. The helper object is basically the DOM element that follows the cursor during the drag event. When it is created by default, the cells collapse to the size of the content inside of them.

You can specify a function that returns a jQuery object to create a custom helper object. By creating a function that will keep the cell widths consistent, this problem can be fixed.

// Return a helper with preserved width of cells
var fixHelper = function(e, ui) {
	ui.children().each(function() {
		$(this).width($(this).width());
	});
	return ui;
};

$("#sort tbody").sortable({
	helper: fixHelper
}).disableSelection();

Now it works as expected:
Sortable row fixed

Extending jQuery to Select ASP Controls

Add to FacebookAdd to DiggAdd to Del.icio.usAdd to StumbleuponAdd to RedditAdd to BlinklistAdd to TwitterAdd to TechnoratiAdd to Yahoo BuzzAdd to Newsvine

If you have worked with JavaScript in an ASP.NET Web Forms environment, you almost certainly have been frustrated that this markup:

	<asp:TextBox runat="server" ID="txtPhoneNumber" />

renders out as something like:

	<input type="text" id="ctl00_ctl00_ctl00_main_Content_txtPhoneNumber"
		name="ctl00$ctl00$ctl00$main$Content$txtPhoneNumber" />

The fastest and easiest way to get a reference to a DOM element in JavaScript is using the ID attribute and document.getElementById(). Unfortunately, the ID attribute generated by the server is unpredictable and based on the server naming containers. There are a couple ways of that I have previously dealt with that problem, but both have problems.

Old Solutions

  1. Accessing the ClientID of the server control

    If you use inline code inside <% %> tags, you can access the ClientID directly from the .aspx page.

    	var goodID = '<% txtPhoneNumber.ClientID %>';  // = 'ctl00_ctl00_ctl00_main_HeaderTabs_txtPhoneNumber'
    	var badID = 'txtPhoneNumber'; // The text box does not have this ID, it will not work
    	var uglyID = 'ctl00_ctl00_ctl00_main_Content_txtPhoneNumber'; // DO NOT hardcode the generated ID into your code!
    

    This is not ideal because you cannot reference the ClientID from outside of the page, so you cannot keep your JavaScript in external files.

  2. Setting attributes on control and accessing with jQuery selectors

    jQuery has an excellent selector API that can easily grab an element if you know attributes on it. So, if there were a couple controls defined as:

    	<asp:TextBox runat="server" ID="txtPhoneNumber" CssClass="txtPhoneNumber" />
    	<asp:TextBox runat="server" ID="txtAddress" ClientID="txtAddress" />
    

    You could access them with jQuery:

    	$(".txtPhoneNumber").keyup(...);   // This works
    	$("[ClientID='txtAddress']").keyup(...);   // This works
    
    	$("#txtPhoneNumber").keyup(...);   // This still DOESN'T work
    	$("#txtAddress").keyup(...);   // This still DOESN'T work
    

    This is not ideal because it requires adding extra attributes onto any server control that you want to access with JavaScript.

Original jQuery Solution

I first happened upon a solution to the same problem over on John Sheehan’s blog. This looked promising, but did not work the current latest release of jQuery (1.3). Another contributer to this blog, Tim Banks, updated the code in to work with the newer version.

However, I found an error in Internet Explorer and a more reliable way to get the ClientID would be to use the jQuery attribute selector and match based on an “id” attribute that ends with the Server ID. So, I wrote a JavaScript function called $asp that returned a jQuery collection. This function worked well and was implemented in the latest Foliotek release.

	function $asp(serverID) {
		return $("[id$='" + serverID+ "']");
	}

	// Once this function is included, you can call it and get back a jQuery collection
	$asp("txtPhoneNumber").keyup(...);

Updated jQuery and Sizzle Solution

It seemed like it would be better if the solution actually extended the selector engine rather than using a function to select objects. This would fit better into a jQuery development paradigm, allow more complex selectors, and also allow the selector to be used in other library functions, like “filter” or “find”. Also, it would be nice to be able to use the tag name in the selector to give a performance improvement, since getElementsByTagName is a fast operation that will narrow the element collection.

So, I returned to the selector, fixed the IE bug and made sure it worked with the now latest version of jQuery (1.3.2). This short function extends the Sizzle selector engine to return an element that has an ID that ends with the ID that is passed in the parenthesis after the “:asp()” selector.

	// Include this function before you use any selectors that rely on it
	jQuery.expr[':'].asp = function(elem, i, match) {
		return (elem.id && elem.id.match(match[3] + "$"));
	};

	// Now all of these are valid selectors
	// They show why this method has more functionality than the previous $asp() function.
	$(":asp(txtPhoneNumber)")
	$("input:asp(txtPhoneNumber):visible")
	$(":asp(txtPhoneNumber), :asp(txtAddress)")
	$("ul:asp(listTodos) li")
	$("#content").find("ul:asp(listTodos)")

This function allows access to server controls without adding additional markup and without the JavaScript existing on the .aspx page.


Note, there is a potential limitation if you had one control with the ID=”txtPhoneNumber” and another with ID=”mytxtPhoneNumber”. Both elements end with “txtPhoneNumber” and the selector would not necessarily return the correct value. This solution is not perfect in that sense, but the benefits it provides over other methods (cleaner markup and ability to use external JavaScript files) make it a good alternative.