« Posts tagged JQuery

Creating an Image Preloader

Image Preloader using JQuery

I recently was creating an image viewer for a customer.  It had the usual qualities of an image viewer where you need preload images to improve response times for the user.

Requirements

  • Load X images behind/infront of current image
  • Handle image load errors
  • Callback for load complete
  • Tie attributes to an image

In addition, we will use JQuery to facilitate this.

Lets Get Started

So if we abstract this a little, we want to have an Image Object with the following attributes…

ImageObject

  • url – The url of the image to load
  • callback – A callback function for when the image load is complete
  • scope – With javascript, closure, we want to call the callback within a scope other than window.  This will hold this information.
  • loaded – Boolean so we can track the images state.
  • loadFailed – So with an image, we may have failed to load or timeout.  So we need to track that.
  • timeout - How long to wait on things to load.
		/**
		 * @classDescription Image object which holds information about the image which is preloading.
		 * This object can be prototyped with extendImage.
		 * @param {String} url The url of image
		 * @param {Function} callback The users callback function
		 * @param {Object} scope The scope to call callback function in
		 * @param {Boolean} loaded Whether the image has been loaded
		 * @param {Jquery Object} $img The reference to the jquery image object in preloading node.
		 * @param {Object} objRef This object's reference
		 * @see extendImage()
		 */
		_Image : function(url, callback, scope, loaded, $img, objRef){
			this.url = url;
			this.callback = callback;
			this.scope = scope;
			this.loaded = loaded;
			this.$img = $img;
			this.func = objRef._imageLoadComplete;
			this.loadFailed = true;
			this.timeout = 5000;//5 seconds
			this.start = new Date().getTime();
			this.finish = 0;
		}

Now We Need to Store the Image Objects

So if we are going to have an image object to represent each image, then we need a way to store these and be able to get them.  Unfortunately javascript doesn’t by default have a good way for use to store and retrieve data.  The default array, is somewhat limited.  Fortunately we can create indexes of the array with strings.  Basically giving us a simple hash table.

However, I kinda wanted to spice this up a little.  I could have used the URL of the image as the index, but what fun is that.  So lets create a hash of the URL and use that as an index.

hashCode = function(str){
	str = str + "";
	var hash = 0;
	if (str.length == 0) return hash;
	var len = str.length;
	for (var i = 0; i < len; i=i+1) {
		char = str.charCodeAt(i);

		hash = ((hash<<5)-hash)+char;
		hash = hash & hash; // Convert to 32bit integer
	}

	return hash >>> 1; //I want positive numbers
}

Now you will probably argue that looping over each character  is bad, why not just use the URL.  Well, your probably right, but this is more fun.

hash = ((hash<<5)-hash)+char;

This is better explained here.

Initiate the Pre-loading of an Image

We will need a simple function to load images. Everything that refers to “this” refers to the object that wraps all these functions.

preload = function(url, callback, scope, timeout){
	var hash = this._hashCode(url);
	if(this.isLoaded(hash)) return;

	if(typeof scope === "undefined") scope = window;

	//create image object
	var $img = $(document.createElement('img'))
		.attr('src',url)
		.attr('id',hash);
	this._images[hash] = new this._Image(url, callback, scope, false, $img, this);

	//bind the onload...pass vars using closure ;) 
	this._images[hash].$img.load(function(obj, hash){
		//only executes on successful load
		return function(){obj._images[hash].func.call(obj, obj._images[hash]);};
	}(this, hash));

	//we always want to trigger our functions, but jquery only
	//callsback on success, so have to keep polling at the image timeout
	if(typeof timeout !== "undefined") this._images[hash].timeout = timeout;
	this.waitOnImage(url, this._imageLoadComplete, this, this._images[hash].timeout, 2);

	this._$loadNode.append(this._images[hash].$img); //append loaded image to the loading DOM element

	return this._images[hash];
};

There are actually a couple interesting segments of this code.

scope - This will allow us to scope the context to the callback.  This is useful to avoid namespace collisions and ensure you are triggering the correct event to specified image loading.

this._images[hash].$img.load(function(obj, hash){
		//only executes on successful load
		return function(){obj._images[hash].func.call(obj, obj._images[hash]);};
	}(this, hash));

This uses the JQuery default load callback.  The problem with the load event is it is unreliable.  It won’t fire if the image fails to load and won’t fire reliably cross-browser and in other circumstances.  These are describer in the jquery link.

Handle the load event not working

To do this we basically need some sort of polling to check if the image is loaded.  That is the waitOnImage() function.

WaitOnImage Function

		/**
		 * Bind specific callback to wait on image
		 * @param {String} url The url of image to wait on load for
		 * @param {Function} callback Callback function to call when loaded
		 * @param {Object} scope The scope to call callback in.  Default to window.
		 * @param {Integer} perdiod Time in milliseconds between recalling wait
		 * @param {Integer} maxTries max attempts to call this function
		 * @param {Integer} tryCount defaults to 0.  Used for cancelling this.
		 */
		waitOnImage : function(url, callback, scope, period, maxTries, tryCount){
			if(typeof period === "undefined") period = 50;
			if(typeof maxTries === "undefined") maxTries = 100;
			if(typeof scope === "undefined") scope = window;
			if(typeof tryCount == "undefined") tryCount = 0;

			var hash = this._hashCode(url);
			var imgObj = this._images[hash];

			if(typeof imgObj === "undefined"){
				this.preload(url, null, null);
			} else if(imgObj.loaded || tryCount > maxTries){
				callback.call(scope, imgObj);
				return;
			}

			//use closures to call self
			setTimeout(
				function(url, callback, scope, period, maxTries, tryCount, _this){
					return function(){
						_this.waitOnImage(url, callback, scope, period, maxTries, tryCount);
					};
				}(url, callback, scope, period, maxTries, ++tryCount, this)
			,period);

		}

The interesting part of the code here is the setTimeout function call.  This accomplishes 2 things.

  • Keeps this from blocking.  Meaning, setTimeout will execute in parallel to this code blocks execution.  If you look at the preload function you see it call this function, which if it just continuously called itself, it would block and prevent the function from completing until the image finished loading.  This would be bad as images can sometimes take quite a while to load.
  • allows for closure to scope the variables to this execution.

Extensibility?

So we have covered the basics of creating a preloader, but its so basic we really need a mechanism to extend its functionality.

/**
		 * Access the prototype to _Image to add your own attributes.
		 * @param {String} name Name of the reference in prototype.
		 * @param {Object} obj The function that will be referenced by name.
		 */
		extendImage : function(name, func){
			//todo add prototype stuff
			this._Image.prototype[name] = func;
		}

This alows us to add whatever we want to the _Image object.  Its really just for accessibility, but a nice feature none the less.  Check out the source code and documentation here.

Jquery UI Select Box Plugin

 Jquery UI Select Box

This is available on JQuery as a plugin.

So a couple of weeks ago I was working on a project for a customer and decided I needed to use JQuery UI since I was going to have some popup boxes and wanted some pretty buttons.  So I applied JQuery UI no problem, but forgot that it does not have a select box.  Yeah I could work around it using Autocomplete or just try and style the select menu up a little with some css, but that would’t be very cross browser consistent.

And then I thought how hard could it be?  I mean all I need to do is change the css and put an overlay to hide the drop down arrow.  And then I could simply trigger the drop down to show.  Something like this…

select_box

Basically I would just inspect the jquery css styles and apply some of the classes to the html to make a jquery styles selct box overlay.  So if the HTML would look something like this…

<div class="my-ui-select">
	<span class="ui-spinner ui-widget ui-widget-content ui-corner-all">
		<select style="border:none;" >
			<option>Option 1</option>
			<option>Option 2</option>
		</select>
		<a class="ui-spinner-button ui-spinner-down ui-state-default ui-corner-right">
			<span class="ui-button-text"><span class="ui-icon ui-icon-triangle-1-s"></span></span>
		</a>
	</span>
</div>

I figured it would be pretty simple to then bind a click event to my <a> tag.  Which is a custom drop down arrow using jquery styles.

<script>
//bind event to <a> tag
$('.my-ui-select a').click(
	function(){
		//trigger the select menu click event
		$(this).parent().find('select').first().trigger('click');
	}
);
</script>

Simple enough right?  Well in actually there is a huge difference in programmatically triggering an event versus a user’s mouse triggered event.  This is probably due to security concerns.

Possible Work Around

So in my searching I came across this entry in stack overflow.  One of the recommendations is to change the size of the select menu.  This would work if you absolutely position the element and use the wrapper description I gave above.

The javascript would look something like this…

<script>
$('.my-ui-select a').click(function(){
	//trigger the select menu click event
	var select = $(this).parent().find('select').first();
	select.attr('size',select.children().length);
});
</script>

Now, this is a very reasonable work around but it has its downsides.

  • The drop down list will not look like other JQuery UI elements when the drop down happens.  Some css styling might make this work alright though.
  • The size attribute can be a little tricky to get right.
    • If your select list has 2 elements, making it a size of 2 will give a lot of white space.
  • Positioning is tricky
    • The <a> tag has to be in-front of the default drop down arrow, but the wrapper which creates the border has to be below the select menu so the default actions work on the select menu.  Getting this position correct on all browsers will be tricky, but not impossible.
  • The scroll bar is default and cannot be removed (might be able to apply styleing, but this wont be corss 100% browsers).
  • In the end, it just doesn’t look quite right.  And this is just in Chrome.  Figuring out how to tweak styles to be good in all browsers will be a serious pain.

In the end you end up with something like this…
selec_using_size

Download the above example here.  Please keep in mind, this was only for Chrome and looks bad in some other browsers.  You will need to spend some time playing with the css, but in the end you should be able to get this working fairly nice.  Also might be a great lightweight solution.

My Solution

selec_using_final

So the above solution is pretty good and super simple.  You are using the native select menu with a little bit of a hack.  This is great, because all your events for the select menu make sense.  But the down side is it has some issues looking polished and the drop down doesn’t really follow the other Jquery UI.

So I needed to resolve these problems.  My solution was to combine the css styling used above coupled with Jquery  UI AutoComplete Widget.

How will this work…

  • Use above css to make something that used JQuery UI css to look nice.
  • Use jquery fn to create our own selector function
    •  Basically we will parse select elements, hide them and create our new css to make things look nice.
  • Apply AutoComplete Widget to our styled drop down menu to give the illusion of a select menu.

Why did I go this route?

  • Cross browser functionality
  • Cross browser looks
  • Maintain Jquery UI consistancy in my own widget
  • Use Jquery UI widget to hadle the heavy lifting of the select menu.  I just need to write an API wrapper for AutoComplete widget to make it do a little more.

In the end this was very simple to implament.  About 100 lines of code and very straight forward.  The real issues arose in event handling.  The AutoComplete Widget API is ok, but frankly a little lacking.  So working around its limitation and forcing it into my own needs took a little fiddling.

Here is the example page.

If you have any questions let me know.  The code should be pretty simple to step through on your own, but I would be happy to explain what I can.