Advertisement
JavaScript & AJAX

Building an Image Gallery with Progressive Enhancement

by

Who doesn’t love to completely trick out their website with neat features? But what happens when your viewers aren’t using the latest browser, or they have JavaScript turned off? In today’s tutorial, you’ll learn how to create a image gallery that will work in almost all environments, using progressive enhancement techniques.


Introduction

Final Product

So what exactly is progressive enhancement? Formally, it is this:

Progressive enhancement is a strategy for web design that emphasizes accessibility, semantic markup, and external stylesheet and scripting technologies. Progressive enhancement uses web technologies in a layered fashion that allows everyone to access the basic content and functionality of a web page, using any browser or Internet connection, while also providing those with better bandwidth or more advanced browser software an enhanced version of the page. (Wikipedia).

Progressive enhancement is the opposite of graceful degradation, where you build your site/app with all the features, and then make sure it looks good and functions decently in older browsers. With progressive enhancement, we’ll lay a solid foundation for our image gallery that will work no matter where you view it. Then, we’ll layer on eye-candy and functionality until we’ve got a good-looking, well-functioning image gallery. Let’s begin!

What we’re After

Here’s what we want to end up with: if all the bells and whistles are turned on, we’ll be able to drag our images around to view them; it will be a very basic simulation of a stack of photos on your coffee table. When you click on one, it will slide open to reveal some details about the image. If JavaScript is turned off, we’ll have a nice grid of image to choose from; clicking them will take us to a page with a larger version of the image and the details. If there’s no CSS support, we’ll get an ugly (but working) list of the images.

Here’s a screen-shot of our final product:

Screenshot of finished gallery

Laying the Foundation: POSH

We start with some plain old semantic HTML. This is our foundation, since every browser out there is good at parsing HTML.

index.htm

<!DOCTYPE html>
<html>
<head>
	<meta charset='utf-8' />
	<title>Progressively Enhanced Image Gallery</title>
</head>
<body>
	<div id="container">
			<h1>Click on an image below to view it!</h1>
		
		<ul id="images">
			<li><div>
				<a href="3dOcean.htm"><img alt="3dOcean" src="images/thumbnails/3dOcean_tn.jpg"/></a>
			</div></li>
			<li><div>
				<a href="AudioJungle.htm"><img alt="AudioJungle" src="images/thumbnails/AudioJungle_tn.jpg"/></a>
			</div></li>
			<li><div>
			<a href="ActiveDen.htm"><img alt="ActiveDen" src="images/thumbnails/ActiveDen_tn.jpg"/></a>
			</div></li>
			<li><div>
				<a href="GraphicRiver.htm"><img alt="GraphicRiver" src="images/thumbnails/GraphicRiver_tn.jpg"/></a>
			</div></li>
			<li><div>
				<a href="ThemeForest.htm"><img alt="ThemeForest" src="images/thumbnails/ThemeForest_tn.jpg"/></a>
			</div></li>
			<li><div>
				<a href="VideoHive.htm"><img alt="VideoHive" src="images/thumbnails/VideoHive_tn.jpg"/></a>
			</div></li>
		</ul>
		
	</div>
</body>
</html>

That’s it; pretty basic stuff, eh? No browser worth that title should have a problem with it. And this is our finished first layer. No, it’s not pretty, but that wasn’t our goal: we wanted something that will work everywhere, no matter what. A few things to notice about this code: firstly, it’s semantic, as we said it should be. You might wonder about the divs inside the list items. What’s up with them? Even though we’re starting with the bare bones, we are anticipating that most of our viewers will have JavaScript enabled, in which case we’ll need those divs. We could insert them with jQuery, but since we do expect them to be used most of the time, it’s easier to hard-code it in. The other thing to notice is that it’s usable. Try viewing it in Lynx, or another text-only browser:

Gallery in Lynx

By the way, the pages linked to in the HTML above will be available in the downloadable source; they’re all similar to this:

<!DOCTYPE html>
<html>
<head>
	<meta charset='utf-8' />
	<title>Themeforest MarketPlace by Envato</title>
</head>
<body>
<h1>ThemeForest</h1>
<img src="images/ThemeForest.jpg" alt="ThemeForest" />
<p>Themeforest offers: HTML Templates, WordPress, 
Joomla, Flash Sites, PSD Templates, Javascript, PHP Scripts</p>
</body>
</html>

On a real site, you’d surround this with your site template, but it’s just fine for our purposes.

Dressing the Structure: CSS

Although semantic HTML is nice, it looks a bit bare. Let’s dress it up with some CSS. Of course, we first have to reference the stylesheet:

<link type="text/css" rel="stylesheet" href="styles/default.css" media="screen" />

We’ll level the playing field first with a stripped-down Meyer reset:

/* Meyer's Reset */
html, body, div, h1, h2, h4, p, a, img, ul, li
{ margin: 0; padding: 0; border: 0; outline: 0; font-weight: inherit; font-style: inherit; font-size: 100%; font-family: inherit; vertical-align: baseline; }
/* remember to define focus styles! */
:focus { outline: 0; }
body { line-height: 1; color: black; background: white; }
ol, ul { list-style: none; }
/* END Meyer's Reset */

Now we have to style our gallery for use without JavaScript. We’ll start with some general elements and background styling:

body{
	font:13px/1.5 'Helvetica Neue',Arial,'Liberation Sans',FreeSans,sans-serif; /* <-- from 960.gs text.css */
	background: #36b4dd;
}
h1 { font-size: 30px; }
#container > h1 { padding: 10px;}
h4 { font-size: 20px; padding-bottom:10px;}

Now we’ll take care of our heading and list items.

#container h1 {
	padding: 10px;
}
#images li { 
	float:left;
	background:#ececec;
	border:1px solid #ccc;
	margin:10px;
	width: 256px;
	padding: 10px;
	overflow: hidden;
}
#images li div {
	width: 512px;
	overflow:hidden;
}
#images li a {
	float:left;
}
#images li div.info {
	width: 246px;
	padding:0 0 0 10px;
	float:left;
}

You’ll notice that we’ve set a width on our list elements. We need to do that for our JavaScript functionality; that’s also why overflow:hidden is set. This is easy in our case, because I’ve made all the images the same width. If yours are different widths, you’ll probably have to set the width for each list item with JavaScript. That will work because the CSS only version doesn’t require the width. The div directly inside our list item (that wraps all the content) is 512px wide, with overflow hidden. We’ve floated our anchor to the left so we can float the div.info to the left beside it, as you see further on.

So, here are the fruits of our labours so far:

Our Gallery with CSS only

We’ll come back to CSS in a bit; but now, let’s turn to the JavaScript!

Adding the Functionality: JavaScript

We’ll be using jQuery here; so start by importing that from Google’s CDN. We’ll also need the jQueryUI library. We could get that from Google as well, but we don’t need the whole library. I’ve downloaded a copy from the jQueryUI site, with just the core and the draggable components, which is all we’ll need. You can do whichever you prefer.

<script src='http://ajax.googleapis.com/ajax/libs/jquery/1.3.2/jquery.min.js'></script>
<script src='js/jqueryui-core-drag.js'></script>

Before we start coding, let’s determine what we need to do.

  • The h1 we’ve hard-coded provides instruction for the non-JavaScript version. We’ll remove this and add different instructions.
  • We need to configure the dragging on the list elements; we’ll add a splash of fun: when the user releases the list item, it will slide a bit further and slow down (sounds like an iEffect). Like we said earlier, it’s supposed to be somewhat like a stack of photos on a table.
  • When a list item is clicked, it should ‘slide open,’ doubling in width. Before it does, however, we’ll send an Ajax call to get the page that the user would go to if JavaScript wasn’t enabled. Then, we’ll get the values we want from that page and plug them into our list item in a div. We’ll check for this div before making the call, though, so if the user has already clicked on it, we won’t send another request.

Alright, open up a script tag and let’s code!

var imgs;

$(document).ready(function () {
	
});

$(window).load(function () {

});

We’ll start by creating a global variable: an array of the list items (well, it will be an array soon). Then, we set up event handlers for a) when the DOM is ready, and b) when the window is finished loading. The effect we’ll do when the window loads (which I haven’t told you about yet) doesn’t require that we wait until then, but I think it will be nicer when the images have been loaded.

Now, this code goes in our document.ready function:

var  drag = {};
$('h1').remove();
$('#images').append('<li id='instructions'><h2>Toss the images around; if you see one you like, click on it!</h2></li>');

imgs = $('#images li');

Should be straightforward: we create an object that will hold some details about out dragging; then we remove the h1, append a list item with new instructions to our list, and put all the list items in our imgs variable.

Now we’ll build our dragging functionality. Really it’s as simple as this:

imgs.draggable();

But we’re going to add a few options. Here’s the code; persue it yourself and then we’ll stroll through it.

imgs.draggable({
			stack : { group : '#images li', min : 1},
			start : function () {
				$this = $(this);
				if($this.attr("id") === 'instructions') { $this.fadeOut().remove(); }

				imgs.each(function () {
				var $this = $(this);
				if($this.width() !== 256) {
					$this.stop().animate({width : 256 }).removeClass('top');
				}
			});
			
			drag.startTime = new Date();
			drag.startPos = $this.position();
		},
		stop : function () {
			var $this = $(this), top, left, time;
			drag.endTime = new Date();
			drag.endPos = $this.position();
			drag.leftOffset = drag.endPos.left - drag.startPos.left;
			drag.topOffset  = drag.endPos.top  - drag.startPos.top;

			time = (drag.endTime.getTime() - drag.startTime.getTime()) /60;
			
			top  = (drag.topOffset / time).toString();
			left = (drag.leftOffset / time).toString();
			
			$this.animate({
				top : '+=' + top, 
				left: '+=' + left 
			});
		}

}); 

We’ve added three properties to our draggable options object: stack, start, and stop. Stack controls the z-index of a group of objects, and takes an object with two properties of its own: group and min. Group is a jQuery selector; in our case, it’s the list items. Min is the minimum z-index any items in the group can take. So now, when you drag an item, it comes to the top of the pile.

The start function is run when you begin to drag an item. We start by caching $(this). Then, we check to see if the list item we grabbed has an id of ‘instructions.’ If it does, we fade it out and remove it. Then, we loop over each list item and if we find any that aren’t 256px wide, we animate the width to 256px and remove the class of ‘top.’ What’s ‘top’ do? We’ll style it in a few minutes, but it just gives the user some visual feedback when they click an item. After that, we do something very important: we set two properties on our drag object. One (startTime) is the time the dragging started, and the other (startPos) is the position the item started at. We’ll use this information to create our effect when the dragging stops.

Lastly, we have the stop function, which predicably runs when user stops dragging. Again, we start by caching $(this), as well as creating a few other variables we’ll give values to in a moment. Next, we put our end time and position into drag.endTime and drag.endPosition. Then we calculate our left and top offset by subtracting where we were from where we are; we can do this with the top and left properties that the position object has. Now for the slowing down animate logic: you could get very complicated with this algorithm, but we’re just going to keep it simple. We’ll find the time the drag took by subtracting our startTime from our endTime; the getTime method returns the number of milleseconds since 1970/01/01, so the difference is in milleseconds Then, we divide that value by 60, which I came up with through trial and error. On an average drag, this sets our time variable somewhere between 2 and 3. Then we divide our top and left offset by time, and convert those values to string, saving them in top and left. Finally, we animate the dragged list item, incrementing (that’s what ‘+=’ does) the value by top or left. At this point, you should be able to drag the images around and get our effect.

However, clicking the images will bring you to a new page. So let’s set up our click event handler.

imgs.click(function () {
			var $this = $(this);

		if ($this.attr('id') === 'instructions') {
			$this.fadeOut().remove();
		}
		else {
			if($this.width() !== 256) {
				$this.stop().animate({width : 256 }).removeClass('top');
			}
			else {
				if (!($this.find('.info').length)) {
					$.ajax({
						url : $this.find('a').attr('href'),
						dataType : 'html',
						success : function (data) {
							var $d = $(data),
								head = $d.filter('h1'),
								para = $d.filter('p');
								
							$this.children('div').append('<div class="info"></div>').find(".info").append(head, para);
						},
						error : function () {
							var msg = '<h1>Oops!</h1><p>It looks like there been a problem; we can\'t get this info right now.</p>';
							$this.children('div').append('<div class="info"></div>').find(".info").html(msg);
						}
					});
				}
				$this.css({'zIndex' : 8 })
					 .stop()
					 .animate({ width : 512})
					 .addClass('top')
						.siblings().removeClass('top')
								   .stop()
								   .animate({width : 256})
										.filter(function () { return $(this).css('zIndex') === '8' }).css({'zIndex' : 7});
			}
		}
		return false;
	});

Standard operating procedure today: begin by caching $(this). Once again, we check for the id of instructions; if it’s there, we fadeOut and remove the item. If its not there, we check the width of the element: if it isn’t 256px, that means this item has already been clicked, so we animate the width down to 256 and remove our top class (yes, we’ll get there). If the element is 256px wide, we check for a child element with the class of info. We can do this my calling the find method on the element, pass in the selector we’re looking for, and get the length property. If this element doesn’t exist, the result will be 0, which is a false value, so we wrap that in parentheses and use a ! to switch the boolean. Now, if there aren’t any child elements with a class of info, we’ll step into this block, which is our ajax call.

$.ajax() takes an object parameter, and we’ll use four properties: url, datatype, success, and error. Url and datatype are obvious: we simply find the anchor in our list item and set url to its href; our datatype is html. If our ajax call is successful, we’ll take the data we get, which is the entire HTML contents of the page, and turn it into a jQuery object. Then, we can filter out the heading and paragraph that we know we have there. Then we simply get the div inside our list item, append a div.info, and append the heading and paragraph to that. If our request fails, we’ll show an error message by a similar process, using the error function. After our ajax call, we want to perform some styling and animation on our list item. First, we want to set the z-index to 8, or any number higher than the number of draggable items we have. Then we want to stop all current animations on this list item and animate the width to 512px. Lastly, we’ll add that top class. Next, we get all the siblings, which are the other list items. We’ll stop any animation on them and then animate them to 256px wide. Finally, we’ll filter out only the elements with a z-index of 8 and change their z-index to 7. This allows the currently cliked list item to come ot the top. Right at the end, we return false, so we stay on our current page (because even though this is a click function on a list item, the users will most likely click our anchor-wrapped image inside the list item).

So that’s our click handler; only one piece of JavaScript left. If you give our example a try now, you’ll see it works … kind of. Whenever you click a list item to open it, it opens, but you’ll notice a rather shifty problem. It’s because the list items are floated to the left; let’s take care of that in our window ready handler.

$(window).load(function () {
	var $w = $(window);
	imgs.css({	position : 'absolute',
			left : $w.width() / 2 - imgs.width(),
			top  : $w.height() / 2- imgs.height() });
	for(var i = 0; imgs[i]; i++ ) {
		$(imgs[i]).animate({	left : '+=' + Math.random()*150,
						top  : '+=' + Math.random()*150 });
	}
});

If you’ve followed pretty well so far, you won’t flinch here: we simply use the jQuery’s css method to set the positioning to absolute and stack all the images so their right edges are aligned to the middle of the viewport, and their bottom edges are aligned to the vertical middle. Then we use a for loop to recurse over each list item and randomly animate it right and down. This creates the effect of a stack of images being scattered.

So that’s it for the JavaScript! Now, when a user loads the page, they should see something like this (after animation) :

Images scattered

Final Touches: CSS3

We could end there, but we want to reward those who use forward-thinking browsers, so it’s back to the CSS for a few minutes. And, yes, we’ll look at the top class.

The first thing we’ll do is add rounded corners to the selector #images li.

border-radius:5px;
-moz-border-radius:5px;
-webkit-border-radius:5px;

Then the top class, which list items only have when they are ‘open,’ looks like this:

.top {
	box-shadow:0 0 10px #000;
	-moz-box-shadow:0 0 10px #000;
	-webkit-box-shadow:0 0 30px #000;
}

Nothing incredibly fancy, but a few nice refinements nonetheless.

Closing Comments

Well, that’s it. We should now have an image gallery that works decently without CSS or JavaScript, but takes full advantage of them where those technologies are available. So, how would you improve our gallery? Let’s hear it in the comments!

Related Posts