Recreating the Windows Aero Desktop in HTML/CSS/JS

August 17th, 2011 3 comments

Hi everyone, it’s been a long time since I’ve written a tutorial. Today I’m gonna bring you a new one and it’s the biggest of them all: We are going to recreate the Windows Aero Desktop using HTML, CSS, jQuery and jQuery UI

View Demo     Download Files

This demo is not compatible with Internet Explorer!!!

This is not gonna be a perfect copy, I had to exclude a lot of features because this tutorial was already taking too long to finish, also some features such as Program Preview and the blurring behind the glass are impossible in HTML as far as I know (I tried making the blurring using the feGaussianBlur SVG filter, but it seems it doesn’t work in browsers when you use the attribute in=”BackgroundImage”, which is what we need)

Here is the features I had to cut due to time constraints: (I might do the later depending on how well this tutorial is received)

  • Internet Explorer 8+ support
  • Animated Sortable Programs in Taskbar
  • Multiple Program instances per Icon in Taskbar
  • Start Menu
  • Desktop Icons

Note: It was a lot of work to make this tutorial: Coding everything, Creating all images in Inkscape, writing this post. If you plan on using this tutorial or the images in an open-source project, such as a jQuery plugin, please give me credit with a link to this post. Thanks!

Anyway, enough talking, let’s start the tutorial. First download yourself a copy of jQuery and jQuery UI with the Core, Draggable, Resizable, Sortable and Effects Core components. We will use them in the tutorial

As always, we start by defining our HTML code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
<div id="desktop">
	<div class="preload"></div>
 
	<div class="windows"></div>
 
	<div class="wallpaper">
		<img src="images/wallpaper.jpg"/>
	</div>
 
	<div class="taskbar">
		<ul class="apps">
			<li data-app-id="1" data-app-name="Internet Explorer"><img src="icons/ie.png"/></li>
			<li data-app-id="2" data-app-name="Mozilla Firefox"><img src="icons/firefox.png"/></li>
			<li data-app-id="3" data-app-name="Google Chrome"><img src="icons/chrome.png"/></li>
			<li data-app-id="4" data-app-name="Opera"><img src="icons/opera.png"/></li>
			<li data-app-id="5" data-app-name="Inkscape"><img src="icons/inkscape.png"/></li>
			<li data-app-id="6" data-app-name="GNU Image Manipulation Program"><img src="icons/gimp.png"/></li>
			<li data-app-id="7" data-app-name="About"><img src="icons/about.png"/></li>
		</ul>
	</div>
 
	<div class="contents">
		<div data-app-id="1">
			<iframe src="bsod.html"></iframe>
		</div>
		<div data-app-id="2">
			<iframe src="http://www.firefox.com"></iframe>
		</div>
		<div data-app-id="3">
			<iframe src="http://www.google.com/chrome/intl/en/more/index.html"></iframe>
		</div>
		<div data-app-id="4">
			<iframe src="http://www.opera.com"></iframe>
		</div>
		<div data-app-id="5">
			<iframe src="http://www.inkscape.org"></iframe>
		</div>
		<div data-app-id="6">
			<iframe src="http://www.gimp.org"></iframe>
		</div>
		<div data-app-id="7" data-glass-only="true">
			<p>Windows Aero HTML Demo</p>
			<p>by Victor Cisneiros</p>
			<p>See the tutorial here: <a href="http://www.victorcisneiros.com">http://www.victorcisneiros.com</a></p>
		</div>
	</div>
</div>

The #desktop div is where we will call our desktop() jQuery function, it contains a .preload div that is invisible to the user and will be used to load images before they appear on the screen. We don’t want the user waiting for the images to load when he first opens a window.

The .windows div is where all the desktop windows will be stored.

The .wallpaper div will cover the entire screen and will show the windows wallpaper.

The .taskbar is where we’ll store all the icons, notice that I’m using attributes with the data prefix: they are custom HTML5 attributes that are invisible to the user and can be used to store private data that can be accessed via javascript.

Finally we have the .contents div that stores the initial content of the windows of each app in the taskbar, notice that it has the data-app-id attribute just like the icons in the taskbar, this app-id is what will link an icon to it’s respective content. The data-glass-only indicates that this window’s content will be transparent, it will not be covered by a white background

Now let’s do some CSS coding, first thing we will do is set the wallpaper to ocupy the whole screen. We do that by setting position: absolute, width: 100% and height: 100%

1
2
3
4
5
6
7
8
9
body { font-family: Arial, Helvetica, sans-serif; margin: 0; padding: 0 }
 
.preload, .contents { position: absolute; left: -9999px; top: -9999px }
.contents iframe { width: 800px; height: 600px }
.contents div { display: inline-block }
 
.wallpaper img { position: absolute; top: 0; left: 0; width: 100%; height: 100% }
 
.windows { position: absolute; width: 100%; height: 100%; overflow: hidden; left: 0; top: 0 }

Notice that we are not using display: none to hide the .preload div. That’s because we want the browser to think that the .preload is visible, so we change it’s position to -9999px relative to the document. Same for .contents. We then set a default width and height for our .contents iframes and set display: inline-block on the divs because we don’t want them to ocupy the whole line, like block elements do

The .windows div has overflow set to hidden because we don’t want scrollbars to appear when the user moves the window out of the desktop area

Now let’s style the taskbar

1
2
3
4
.taskbar { background: url('images/taskbar.png') repeat-x; position: absolute; height: 40px; width: 100%; bottom: 0; left: 0; z-index: 99999999 }
	.apps { margin: 0 0 0 60px; padding: 0; list-style: none; height: 40px }
	.apps li { background: url('images/sprites.png') repeat scroll 0 0 transparent; background-position: 0 0; color: #fff; font-size: 12px; font-weight: bold; float: left; width: 60px; height:40px }
	.apps li img { vertical-align: top; margin: 4px 14px 0 }

Code is simple, we use position: absolute to position the taskbar relative to it’s first parent element that has a position other than static (in this case, the document). We use a really high value for z-index so that the taskbar stays above everything. And then we set float: left to the li so that they stay each beside the other, instead of below

We are ready to do some Javascript coding. Start by adding this to the end of the body

1
2
3
4
5
<script type="text/javascript">
$(document).ready(function() {
	$('#desktop').desktop();
});
</script>

Now let’s write our desktop() function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
(function($) {
	$.fn.desktop = function() {
		$(this).each(function() {
			$(this).addClass('desktop');
			$(this).find('.wallpaper, .taskbar').disableSelection();
			$(this).find('ul.apps').sortable({ axis: 'x', revert: 250, distance: 5 });
 
			var preload = $(this).find('.preload');
			$(preload).append('<img src="images/glass.png"/>');
			$(preload).append('<img src="images/glass_inactive.png"/>');
			$(preload).append('<img src="images/window.png"/>');
			$(preload).append('<img src="images/window_inactive.png"/>');
			$(preload).append('<img src="images/window_maximized.png"/>');
			$(preload).append('<img src="images/glow_left.png"/>');
			$(preload).append('<img src="images/glow_background.png"/>');
			$(preload).append('<img src="images/glow_right.png"/>');
		});
	};
 
	$.fn.disableSelection = function() {
		return $(this).attr('unselectable', 'on').css({ MozUserSelect: 'none', KhtmlUserSelect: 'none' }).bind('selectstart', function() { return false });
	};
})(jQuery);

In case you are wondering the first and last lines are the declaration of an anonymous function and invocation of it passing the jQuery object as argument, we then use the jQuery object as the $ parameter. This is a common practice when writing jQuery plugins because some others libraries also use the $ object, and here it is defined only inside our anonymous function

Our desktop() function iterates over each matched element (in our case, only the #desktop div), adds the class .desktop to it, then disables selection on both .wallpaper and .taskbar. The disableSelection is a method that we define below, it makes the wallpaper not able to be selected and dragged. Another useful case for disableSelection would be when dragging a div we would want the text contained on it not be able to be selected. There is a disableSelection that comes with jQuery UI that does that, but it was not working in Chrome so that is why defined our own

Then we call the sortable function on our ul.apps in the taskbar. the sortable() is a function that comes in jQuery UI that allows a group of DOM elements to be sorted by dragging it’s elements. The axis: ‘x’ parameter makes the items be able to be dragged only horizontally. The revert: 250 parameter makes the dragged element revert to it’s new position with a smooth animation that takes 250 milliseconds to complete. The distance: 5 is the minimum number of pixels that the mouse needs to move before the sort starts

Run the page, it should be looking like this now:

Try dragging the icons on the taskbar now, you’ll see that they are now sortable. The only thing lacking is the smooth animations when sorting like the Windows 7 taskbar. I tried doing it with jQuery UI during a whole day but was unable to make a decent solution, so I will leave this for the future

The last thing we did was append some images to the .preload div so that they start loading now

What we gonna do now is change the background of the taskbar buttons when the user hovers the mouse on them or clicks. For this we are gonna use CSS sprites, notice how the li is using a single image which contains different images as a background

To use CSS sprites we simply change the background-position to -Xpx -Ypx where X and Y are the pixel position where the background starts. The advantage of using one image is that less HTTP requests are required and also that when the background changes it is instantly replaced. If we used 2 background images there would be a delay because the new background would have to load and this is really bad for user experience

Add this CSS

1
2
3
.apps li.none { background-position: 0 0 }
.apps li.hover { background-position: 0 -40px }
.apps li.selected { background-position: 0 -80px }

And create these 2 new functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$.fn.sprites = function() {
	return $(this).each(function() {
		$(this).hover(function() {
			$(this).removeSpritesClasses().addClass('hover');
		}, function() {
			$(this).removeSpritesClasses().addClass('none');
		});
		$(this).mousedown(function() {
			$(this).removeSpritesClasses().addClass('selected');
		});
		$(this).mouseup(function() {
			$(this).removeSpritesClasses().addClass('hover');
		});
	});
};
 
$.fn.removeSpritesClasses = function() {
	return $(this).removeClass('hover').removeClass('none').removeClass('selected');
};

The first function iterates over each matched elements and sets the hover event to remove all of our sprite classes (hover, none, selected) and add the hover class when the mouse enters the element and to set the class to none when the mouse exits the element. The function also sets the mousedown event to set the class to selected and the mouseup to set the class to hover

Add this to our desktop function, below the preloads

1
$(this).find('.apps li').sprites();

We are now gonna add the code to start the application when the user clicks the taskbar icon, but it will lack the actual app window for now. Add this below the code above

1
2
3
4
5
6
7
8
9
$(this).find('.apps li').click(function(e) {
	if (e.which == 1 && $(this).css('position') != 'absolute') {
		if (!$(this).hasClass('active')) {
			$(this).startApp();
		} else {
			// nothing for now
		}
	}
});

And then define the needed functions

1
2
3
4
5
6
7
8
$.fn.startApp = function() {
	html = '<div>' + $(this).html() + '<span>' + $.maxLength($(this).attr('data-app-name')) + '</span></div>';
	$(this).removeClass().addClass('active').html(html).find('div').sprites();
};
 
$.maxLength = function(text) {
	return text.length > 12 ? text.substring(0, 12) + '...' : text;
};

The start app function adds the active class to the li, replaces its content with a div that wraps the old content plus a span with the app name (The app name is retrieved by calling the jQuery attr function which returns the value of the attribute passed as parameter). The function then finds our newly inserted div and calls our sprites function on it, so that it changes the background when it is hovered and clicked

Add this new CSS code

1
2
3
4
5
6
7
8
.apps li.active { background: none; width: 160px; white-space: nowrap }
	.apps li.active div { background: url('images/sprites.png') no-repeat; background-position: -60px 0; height: 40px }
	.apps li.active span { cursor: default; display: inline-block; margin: 12px 0 0 -5px }
	.apps li.active div.none { background-position: -60px 0 }
	.apps li.active div.hover { background-position: -60px -40px }
	.apps li.active div.selected { background-position: -60px -80px }
	.apps li.active div.selected img { margin: 5px 14px 0 15px }
	.apps li.active div.selected span { margin: 13px 0 0 -4px }

The li.active now has no background, instead, the children div will have. The white-space: nowrap is inserted so that app name won’t break into new lines if it has spaces on it. The display: inline-block is inserted on the span to allow an inline element (a span) to have the margin set.

Run the page now, clicking on a desktop icon will change it’s background and show the app name

Now let’s add the code to actually show the Window. First declare this variable (the backslashes are there to make multi-line strings):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var windowHTML = 
	'<div class="window">\
		<div class="title"><span><span class="start"></span><span class="text"></span><span class="end"></span></span>\</div>\
		<div class="buttons">\
			<div class="minimize"><div><span></span></div></div>\
			<div class="maximize"><div><span></span></div></div>\
			<div class="close"><div><span></span></div></div>\
		</div>\
		<div class="glass">\
			<div class="background"></div>\
		</div>\
		<div class="container">\
			<div class="outer_border">\
				<div class="content"></div>\
			</div>\
		</div>\
	</div>';

Then add this code to the startApp function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var window = $(windowHTML);
$(this).parents('.desktop').find('.windows').append(window);
 
var content = $('.contents div[data-app-id=' + $(this).attr('data-app-id') + ']');
var width = $(content).width() +44;
var height = $(content).height() + 62;
var wallpaper = $(this).parents('.desktop').find('.wallpaper img');
 
if ($(content).attr('data-glass-only') == 'true') $(window).addClass('glass_only');
 
$(window).attr('data-app-id', $(this).attr('data-app-id'));
$(window).find('.title .text').text($(this).attr('data-app-name'));
$(window).find('.content').append($(content).html());
$(window).width(width).height(height).css({ left: ($(wallpaper).width() - width) / 2, top: ($(wallpaper).height() -40 - height) / 2 });

First we create a DOM object for the window using our html code defined before, we then append this new window to the .windows div of our deskto

We then need to retrieve the default contents for our window. We do that by selecting the div inside .contents that has the same data-app-id as this (our li). We calculate the size of the window (+44 is because 20 + 20 pixels from the .container border + 1 + 1 from the .outer_border and + 1 + 1 from the .content border. +62 is the same + 18 from .container padding-top) and then search for the image of the desktop. If the li has the attribute data-glass-only we add a class glass_only to the div. We then set the data-app-id attribute on the window to the same data-app-id of the calling li, we will need this information later

Next we fill the span .title .text in the window with the attribute data-app-name from the li. Then we copy the html of our selected content to the .content div of our window. We then set the window left and top positions so that it stays at the center of our wallpaper. The -40 value is to ignore the taskbar part of our wallpaper

Now we need some CSS to style the window. Add this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.window { z-index: 1; position: absolute }
	.window .title { margin-right: 140px; position: relative; height: 30px; top: 10px; left: 15px; z-index: 1; font-size: 13px; overflow: hidden /*; text-shadow: 0 0 10px #fff, 0 0 10px #fff, 0 0 10px #fff, 0 0 10px #fff, 0 0 10px #fff */ }
		.window .title span { cursor: default; vertical-align: middle; white-space: nowrap; display: inline-block; height: 30px }
		.window .title span.text { background: url('images/glow_background.png'); display: inline-block; line-height: 30px }
		.window .title span.end { background: url('images/glow_right.png') no-repeat; display: inline-block; width: 10px }
		.window .title span.start { background: url('images/glow_left.png') no-repeat; display: inline-block; width: 10px }
	.window .buttons { position: absolute; height: 20px; top: 11px; right: 19px; z-index: 10 }
	.window .glass { position: absolute; top: 0; left: 0; width: 100%; height: 100%; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; -o-box-sizing: border-box; box-sizing: border-box; padding: 12px }
		.window .glass .background { background: url('images/glass.png'); height: 100% }
	.window .container { -webkit-border-image: url('images/window.png') 20 repeat; -moz-border-image: url('images/window.png') 20 repeat; border-image: url('images/window.png') 20 repeat; padding-top: 18px; border-width: 20px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; -o-box-sizing: border-box; box-sizing: border-box; width: 100%; height: 100%; position: absolute; left: 0; top: 0 }
		.window .container .outer_border { width: 100%; height: 100%; background: #fff; border: 1px solid #bddaed; -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; -o-box-sizing: border-box; box-sizing: border-box }
		.window .container .content { overflow: hidden; width: 100%; height: 100%; border: 1px solid #1d3e58; -webkit-border-radius: 1px; -moz-border-radius: 1px; border-radius: 1px; -moz-box-sizing: border-box; -webkit-box-sizing: border-box; -o-box-sizing: border-box; box-sizing: border-box; }
		.window .container iframe { border: 0; width: 100%; height: 100% }
	.glass_only .container .outer_border { border: 1px solid transparent; background: transparent }
	.glass_only .container .content { border: 1px solid transparent }
	.glass_only .container .content p { margin: 10px }

First thing we do is set the .window position to absolute to make each window position relative to the document. The z-index: 1 property is used to each window appear in front of the wallpaper (which has the default z-index: 0)

Originally I had the glow in the window’s title made using multiple text-shadow properties, but this was making the animation run not as smooth as possible, not to mention Internet Explorer doesn’t support text-shadow, so I would need to get rid of these text-shadow if I decided to implement IE support. Because of this I decided to use background images for the glow. You can see there’s a span for the start of the glow, one for the text and one for the end

The window div contains two divs, one for the glass background (named .glass) and another for the content (named .container). Both should be positioned at the same location, so we use position: absolute and left: 0; top: 0 so they start at the beginning of our window. Since .container is declared later it will stay above the glass. You’ll also notice that they are using the box-sizing: border-box CSS property, that’s because they have width: 100% and so any additional border or padding will make it’s width go beyond 100%. To make the padding and border count on the width we use this CSS3 box-sizing property

Another CSS3 property we are using is the border-image: url(‘images/window.png’) 20 repeat. Basically it divides our image into a grid of 9 images each with a size of 20pixels, and then uses these images as the borders of our div. Repeat is there to repeat the background of our top/bottom/left/right borders which can be much longer than the image. Below is a picture of our image. Notice how the interior of the box only starts at 12px inside. That is why the .glass div has a padding of 12px

Run the page now and click on a program, the window will appear:

Let’s make the minimize, maximize and close buttons now. On a first look it seems simple to implement, but is quite complicated because the glows when the user hover a button overlap the other buttons. Look back at our sprites image and see how each button image is bigger than the actual button because of the glow

If you look at our window HTML code you’ll notice each button is formed by a div with the button class, another div inside it (that will have our hover/selected/none sprite classes applied to it) and a span. The sprite div will have a size equal to the whole button image with the glow and each div will overlap the others. The span will have the size of the button without the glow and will thus be the area that responds to the mouse events

Add this line our window creation code (the latest javascript code we created)

1
$(window).find('.minimize div span, .maximize div span, .close div span').sprites(true);

Notice we are using our same sprites function, but this time with a boolean parameter. This parameter will indicate that the sprite classes (hover/selected/none) will be applied to the parent element instead of our spans

Change our sprites function to this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$.fn.sprites = function(parent) {
	return $(this).each(function() {
		var element = parent ? $(this).parent()[0] : this;
		$(this).hover(function() {
			if (!$(element).hasClass('button')) $(element).removeSpritesClasses().addClass('hover');
		}, function() {
			if (!$(element).hasClass('button')) $(element).removeSpritesClasses().addClass('none');
		});
		$(this).mousedown(function() {
			if (!$(element).hasClass('button')) $(element).removeSpritesClasses().addClass('selected');
		});
		$(this).mouseup(function() {
			if (!$(element).hasClass('button')) $(element).removeSpritesClasses().addClass('hover');
		});
	});
};

It’s the same function as before, except now we check for the boolean parameter and if true we will select the parent of the element (the parent() function actually returns a jQuery object that can wrap many elements, to select the first DOM object we need to use [0])

Now all we need is some CSS code and the buttons are ready!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.window .minimize div { background: url('images/sprites.png'); background-position: -220px 0; position: absolute; width: 66px; height: 64px; right: 58px; top: -22px }
	.window .minimize div span { position: absolute; z-index: 2; display: inline-block; width: 30px; height: 20px; left: 18px; top: 22px }
	.window .minimize div.none { z-index: auto; background-position: -220px 0 }
	.window .minimize div.hover { z-index: 1; background-position: -220px -64px }
	.window .minimize div.selected { z-index: 1; background-position: -220px -128px }
.window .maximize div { background: url('images/sprites.png'); background-position: -286px 0; position: absolute; width: 66px; height: 64px; right: 30px; top: -22px }
	.window .maximize div span { position: absolute; z-index: 2; display: inline-block; width: 26px; height: 20px; left: 20px; top: 22px }
	.window .maximize div.none { z-index: auto; background-position: -286px 0 }
	.window .maximize div.hover { z-index: 1; background-position: -286px -64px }
	.window .maximize div.selected { z-index: 1; background-position: -286px -128px }
.window .restore div { background: url('images/sprites.png'); background-position: -352px 0; position: absolute; width: 66px; height: 64px; right: 30px; top: -22px }
	.window .restore div span { position: absolute; z-index: 2; display: inline-block; width: 26px; height: 20px; left: 20px; top: 22px }
	.window .restore div.none { z-index: auto; background-position: -352px 0 }
	.window .restore div.hover { z-index: 1; background-position: -352px -64px }
	.window .restore div.selected { z-index: 1; background-position: -352px -128px }
.window .close div { background: url('images/sprites.png'); background-position: -418px 0; position: absolute; width: 74px; height: 64px; right: -11px; top: -22px }
	.window .close div span { position: absolute; z-index: 2; display: inline-block; width: 49px; height: 20px; left: 13px; top: 22px }
	.window .close div.none { z-index: auto; background-position: -418px 0 }
	.window .close div.hover { z-index: 1; background-position: -418px -64px }
	.window .close div.selected { z-index: 1; background-position: -418px -128px }

Notice we are using position: absolute to position each div because they overlap each other. Same for the spans. Another thing worthy noting is that the currently hovered/selected div has z-index: 1, this is to make it appear in front of the other divs. The .none div has z-index set to auto to reset a previous z-index that had been set to 1

Run the page now, hovering on the buttons now will change it’s background

Now it is time to actually set the events of the button we created. Add this to our window creation code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$(window).find('.minimize span').click(function() {
	$(window).minimizeWindow();
});
$(window).find('.maximize span').click(function() {
	if ($(this).parent().parent().hasClass('maximize')) {
		$(window).maximizeWindow();
	}
	else if ($(this).parent().parent().hasClass('restore')) {
		$(window).restoreWindow();
	}
});
$(window).find('.close span').click(function() {
	$(window).closeWindow();
});

Now let’s define the 4 functions above, and some others

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
$.fn.maximizeWindow = function() {
	var wallpaper = $(this).parents('.desktop').find('.wallpaper img');
	$(this).resizable('disable');
	$(this).saveSize('old');
	$(this).css('left', '-20px').css('top', '-12px').width(wallpaper.width() + 40).height(wallpaper.height() -6);
	$(this).find('.maximize').removeClass('maximize').addClass('restore');
	return $(this).removeClass('minimized').addClass('maximized');
};
 
$.fn.minimizeWindow = function() {
	var wallpaper = $(this).parents('.desktop').find('.wallpaper img');
	var button = $(this).parents('.desktop').find('.apps li[data-app-id=' + $(this).attr('data-app-id') + ']');
	$(this).saveSize();
	$(this).stop().animate({ left: $(button).offset().left -60, width: 280, height: 120, top: $(wallpaper).height() - 150, opacity: 0 }, 500, function() {
		$(this).hide();
	});
	return $(this).addClass('minimized');
};
 
$.fn.restoreWindow = function() {
	$(this).resizable('enable');
	$(this).css('left', $(this).attr('data-old-left') + 'px').css('top', $(this).attr('data-old-top') + 'px');
	$(this).width($(this).attr('data-old-width')).height($(this).attr('data-old-height'));
	$(this).find('.restore').removeClass('restore').addClass('maximize');
	return $(this).removeClass('maximized');
};
 
$.fn.closeWindow = function() {
	var button = $(this).parents('.desktop').find('.apps li[data-app-id=' + $(this).attr('data-app-id') + ']');
	$(button).html($(button).find('img').outerHTML()).removeClass('active');
	$(this).remove();
};
 
$.fn.showWindow = function() {
	$(this).show().stop().animate({ 
		left: $(this).attr('data-left'), width: $(this).attr('data-width'), 
		height: $(this).attr('data-height'), top: $(this).attr('data-top'),
		opacity: 1, 
	}, 500);
	return $(this).removeClass('minimized');
};
 
$.fn.saveSize = function(prefix) {
	var prefix = prefix ? prefix + '-' : '';
	if (!$(this).is(':animated')) {
		$(this).attr('data-' + prefix + 'left', $(this).offset().left);
		$(this).attr('data-' + prefix + 'top', $(this).offset().top);
		$(this).attr('data-' + prefix + 'width', $(this).width());
		$(this).attr('data-' + prefix + 'height', $(this).height());
	}
	return $(this);
};
 
$.fn.outerHTML = function() {
	return $(this).clone().wrap('<div></div>').parent().html();
};

The maximizeWindow function first finds the desktop wallpaper (to use it’s height), then disables the resizable behavior on it, calls the saveSize function with a prefix = ‘old’. The saveSize function saves the size and position of a window inside some custom data attributes. The prefix old here is used to differentiate this size (which represents the size of the window before being maximized) from other sizes that will be saved later

Next we find the .maximize button and change it’s class to .restore to change the button to a restore window button. Last thing we do is remove the .minimized class on the window (if present) and add the .maximized class to indicate the window has been maximized

The minimizeWindow function finds the desktop wallpaper, finds the correspoding li in the taskbar. Saves the current size and position of the window. Then it calls the jQuery animate function. This function receveices a hash of parameters representing the CSS properties this function will animate to. The second parameter is the number of milliseconds this animation will take to complete and the third parameter is a callback the animate function will call when the animation is completeded. What all of this will do is shrink the window and move it to the position of our li while also making it transparent. The last thing we do is remove the maximized class if present and adding the minimized class

The restoreWindow is simple, it reverts the size and position of the window back to the values stored in our custom data attributes with the prefix old. It then changes the restore window to a maximize window and then removes the .maximized class from the window

The closeWindow finds the corresponding li and removes the class active from it. It also changes it’s content to include only the outerHTML of it’s image tag, and finally it removes the window from the document

The showWindow will restore the window back to it’s position and size when it has been minimized and the user clicks on the icon on the taskbar. Remember our (// nothing for now) comment on the desktop function? Replace it with this:

1
2
var window = $(this).parents('.desktop').find('.window[data-app-id=' + $(this).attr('data-app-id') + ']');
$(window).hasClass('minimized') ? $(window).showWindow() : $(window).minimizeWindow();

Run the code now, you should be able to maximize, minimize, restore and close windows

Let’s now make them able to be dragged and resizable, for this we will use jQuery UI. Add this to our window creation code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$(window).draggable({
	cancel: '.content., .buttons',
	iframeFix: true,
	start: function(event, ui) {
		$(ui.helper).restoreWindow(event, ui);
	},
	drag: function(event, ui) {
		$(ui.helper).glassPosition();
	}
});
$(window).resizable({ 
	handles: 'all',
	iframeFix: true,
	stop: function(event, ui) {
		$(ui.helper).saveSize().saveSize('old');
	},
	resize: function(event, ui) {
		$(ui.helper).glassPosition();
	}
});
$(window).saveSize().saveSize('old').glassPosition();

We are calling jQuery UI draggable function passing ‘.content, .buttons’ as the cancel parameter. This prevents the user from dragging the window when he is pressing the mouse on these divs. When the drag starts we restore the window size in case it is maximized. When dragging we call the glassPosition function which will be responsible for making the glass background remain static while the window are dragged. This is the code:

1
2
3
4
5
$.fn.glassPosition = function() {
	var top  = -1 * $(this).offset().top -13;
	var left = -1 * $(this).offset().left - 13;
	return $(this).find('.glass .background').css('background-position', left + 'px ' + top + 'px');
};

Basically what the function does is make the glass background always start from the start of the desktop, independently of the window position. The -13 value corrects the following bug which I don’t know why happens when the window is on the top edge of the desktop

We are also using the iframeFix parameter in both draggable and resizable. What this does is insert a div in front of the iframes to prevent the iframe from capturing the mouse while we are dragging and resizing. But there is one problem: resizable doesn’t actually come with this option so we will include it ourselves

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$.ui.plugin.add("resizable", "iframeFix", {
	resize: function(event, ui) {
		$("div.ui-resizable-iframeFix").remove();
		var o = $(this).data('resizable').options;
		$(o.iframeFix === true ? "iframe" : o.iframeFix).each(function() {
			$('<div class="ui-resizable-iframeFix" style="background: #fff;"></div>')
			.css({
				width: $(this).width() +"px", height: $(this).height() +"px",
				position: "absolute", opacity: "0.001", zIndex: 99999999
			})
			.css($(this).offset())
			.appendTo("body");
		});
	},
	stop: function(event, ui) {
		$("div.ui-resizable-iframeFix").each(function() { this.parentNode.removeChild(this); });
	}
});

I got this code from the internet (with some changes made by me). Like I said, what it does is add and div in front of the iframe when the user is resizing and remove the div when the user stop resizing

Run the page now, you should be able to drag and resize the windows now

You’ll notice the resizable handlers are not positioned correctly on our borders, that’s because part of our border are transparent. Add this CSS to fix this

1
2
3
4
5
6
7
8
9
10
.window .ui-resizable-se { background-image: none }
.window .ui-resizable-handle { z-index: 5 }
.window .ui-resizable-n { top: 4px; height: 10px }
.window .ui-resizable-s { bottom: 4px; height: 10px }
.window .ui-resizable-e { right: 4px; width: 10px }
.window .ui-resizable-w { left: 4px; width: 10px }
.window .ui-resizable-se { right: 4px; bottom: 4px; width: 20px; height: 20px }
.window .ui-resizable-sw { left: 4px; bottom: 4px; width: 20px; height: 20px }
.window .ui-resizable-ne { right: 4px; top: 4px; width: 15px; height: 20px }
.window .ui-resizable-nw { left: 4px; top: 4px; width: 20px; height: 20px }

If you maximize the window of our About program you’ll see that the glass background is not correctly positioned. We need to call our glassPosition function. Change our maximizeWindow and restoreWindow to this (notice the glassPosition() at the end of it):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$.fn.maximizeWindow = function() {
	var wallpaper = $(this).parents('.desktop').find('.wallpaper img');
	$(this).resizable('disable');
	$(this).saveSize('old');
	$(this).css('left', '-20px').css('top', '-12px').width(wallpaper.width() + 40).height(wallpaper.height() -6);
	$(this).find('.maximize').removeClass('maximize').addClass('restore');
	return $(this).removeClass('minimized').addClass('maximized').glassPosition();
};
 
$.fn.restoreWindow = function() {
	$(this).resizable('enable');
	$(this).css('left', $(this).attr('data-old-left') + 'px').css('top', $(this).attr('data-old-top') + 'px');
	$(this).width($(this).attr('data-old-width')).height($(this).attr('data-old-height'));
	$(this).find('.restore').removeClass('restore').addClass('maximize');
	return $(this).removeClass('maximized').glassPosition();
};

You’ll notice that when dragging, showing and resizing windows they do not appear in front ot the others. To do this add this variable

1
var zIndex = 1;

And add this to our minimize, maximize and show window functions

1
$(this).css('z-index', zIndex++);

And finally add this to our window creation code

1
2
3
$(window).mousedown(function() {
	$(this).css('z-index', zIndex++);
}).css('z-index', zIndex++);

This way, every new window will have the highest z-index and when the user click on the window it will also set the highest z-index to it

If you maximize a window you’ll notice that it’s bottom border appear behind the taskbar. To remove it we will change the border-image of a maximized window to a image without the bottom border. Add this CSS below everything

1
2
.maximized .glass { padding: 12px 12px 20px }
.maximized .container { -webkit-border-image: url('images/window_maximized.png') 20 repeat; -moz-border-image: url('images/window_maximized.png') 20 repeat }

If you use Windows 7 you’ll see that when the user clicks on a window or on the wallpaper, all the other windows become inactive, with their background and buttons becoming more transparent. To do this create these functions

1
2
3
4
5
6
7
8
$.fn.makeOthersInactive = function() {
	$(this).siblings().makeInactive();
	return $(this).removeClass('inactive');
};
 
$.fn.makeInactive = function() {
	return $(this).addClass('inactive').find('.minimize div, .maximize div, ,.restore div, .close div').removeClass().addClass('none');
};

The makeInactive function adds the class inactive to the window, and then search for the divs from the buttons and removes every sprite class from them and ads the class none

The makeOthersInactive make all the windows inactive, except the current window (this). This is done by calling the jQuery siblings function. The function also removes the class inactive from the current window

Now we need some CSS to style the inactive windows, changing the border to one with less shadow, the background to one more transparent, and changing the buttons background position to match the correct inactive position in the sprites image. Add this to the end of our CSS file

1
2
3
4
5
6
.inactive .container  { -webkit-border-image: url('images/window_inactive.png') 20 repeat; -moz-border-image: url('images/window_inactive.png') 20 repeat; border-image: url('images/window_inactive.png') 20 repeat }
	.inactive .glass .background { background: url('images/glass_inactive.png') }
	.inactive .minimize div.none { z-index: auto; background-position: -220px -192px }
	.inactive .maximize div.none { z-index: auto; background-position: -286px -192px }
	.inactive .restore div.none { z-index: auto; background-position: -352px -192px }
	.inactive .close div.none { background-position: -418px -192px }

Now add this to our desktop function to make all the windows inactive when the user clicks on the wallpaper

1
2
3
$(this).find('.wallpaper').mousedown(function() {
	$('.window').makeInactive();
});

And change or mousedown event on the window creation code to this

1
2
3
$(window).mousedown(function() {
	$(this).makeOthersInactive().css('z-index', zIndex++);
}).css('z-index', zIndex++);

And also add this to the end of our showWindow and minimizeWindow functions

1
.makeOthersInactive();

Run the page now. Clicking on the wallpaper or on another window will make the windows transparent

There’s still one problem with our code. To reproduce it first maximize a window then drag it. It will correctly restore the window but the position will be wrong. To correct this change our restoreWindow function to this

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
$.fn.restoreWindow = function(event, ui) {
	if ($(this).hasClass('maximized') && event && ui) {
		var ratio = event.pageX / $('.windows').width();
		var wallpaper = $(this).parents('.desktop').find('.wallpaper');
		if (ratio > 0.5) {
			var offsetWidth = $(wallpaper).width() - event.pageX;
			if (offsetWidth < $(this).attr('data-old-width') / 2) {
				$(this).data('draggable').offset.click.left = $(this).attr('data-old-width') - offsetWidth - 20;
			} else {
				$(this).data('draggable').offset.click.left = $(this).attr('data-old-width') * ratio;
			}
		} else {
			if (event.pageX < $(this).attr('data-old-width') / 2) {
				$(this).data('draggable').offset.click.left = event.pageX + 20;
			} else {
				$(this).data('draggable').offset.click.left = $(this).attr('data-old-width') * ratio;
			}
		}
	}
	$(this).resizable('enable');
	$(this).css('left', $(this).attr('data-old-left') + 'px').css('top', $(this).attr('data-old-top') + 'px');
	$(this).width($(this).attr('data-old-width')).height($(this).attr('data-old-height'));
	$(this).find('.restore').removeClass('restore').addClass('maximize');
	return $(this).removeClass('maximized').glassPosition();
};

Notice we added two parameters (event and ui). They are from jQuery UI. Look back at our draggable call in the window creation code and see how the jQuery UI start callback receives these two parameter. The event parameter is the actual browser mouse event and the ui parameter is a helper that contains some objects like the current dragged window

Our restoreWindow now checks for these parameters, and if the window is maximized and these parameters are not null it does some additional logic to correctly position the window. The logic is kind complicated and I did my best to emulate the Windows 7 behavior. Basically it checks how far the mouse X position is relative to the Wallpaper. If the mouse X is past the middle (0.5) it calculates the difference between the wallpaper width and the mouse X position. If this difference is past half of the old window width (before being maximized) it positions the window in a manner that the distance between the mouse and the end of the window is mantained in the restored window, else it mantains the distance ratio. If the mouse X is before the middle it does the same logic but using the start of the window

One thing that our code is lacking is the ability to maximize windows by double clicking. To do that, add this to our window creation code

1
2
3
4
5
6
7
8
$(window).dblclick(function() {
	if (!$(this).hasClass('maximized')) {
		$(window).maximizeWindow();
	}
	else {
		$(window).restoreWindow();
	}
});

And finally to finish this tutorial we just need to add some animations to show and close the windows. Right now they just instantly appear and disappear. In Windows 7 the windows fades in or out and shrinks or grow by a small ammount. To do that let’s create these functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$.fn.animatedShow = function() {
	var offsetLeft = $(this).offset().left;
	var offsetTop = $(this).offset().top;
	var windowWidth = $(this).width();
	var windowHeight = $(this).height();
	$(this).width(windowWidth - 60);
	$(this).height(windowHeight - 60);
	$(this).css('left', offsetLeft + 30);
	$(this).css('top', offsetTop + 30);
	return $(this).css('opacity', 0).animate({ opacity: 1, left: offsetLeft, top: offsetTop, width: windowWidth, height: windowHeight }, 300, 'easeOutCubic');
};
 
$.fn.animatedHide = function(callback) {
	var offsetLeft = $(this).offset().left;
	var offsetTop = $(this).offset().top;
	var windowWidth = $(this).width();
	var windowHeight = $(this).height();
	return $(this).animate({ opacity: 0, left: offsetLeft +30, top: offsetTop +30, width: windowWidth -60, height: windowHeight -60 }, 300, 'easeInCubic', callback);
};

The animatedShow shrinks the window and expand back to it’s original size with an animation. The ‘easeOutCubic’ parameter is a jQuery UI easing function that starts fast and becomes slow when the animation is about to end. To view a list of all jQuery UI easings available click here. The animatedHide shrinks the window by a small ammount using an animation. The ‘easeInCubic’ is an easing that starts slowly and gets faster by the end

Add this to the end of our window creation code

1
$(window).animatedShow();

And in the closeWindow function change $(this).remove() to

1
2
3
$(this).animatedHide(function() {
	$(this).remove();
});

Thats it! The tutorial is finished!

I hope you were able to learn something new by following this tutorial, if you have any questions post a comment and I will try to answer as soon as possible

And if you want more tutorials check this blog soon because I already have the code ready for the next tutorial, I just need to write the post

Categories: CSS, jQuery Tags:

Mimicking the Steam Holiday Sale Book Widget using jQuery

January 22nd, 2011 No comments

In this post I will teach you how to mimic the Steam Holiday Sale Book Widget. The original demo isn’t available at the Steam Store page anymore so I have hosted it elsewhere. You can view it here or view my demo below

View Demo     Download Files

Let’s start by writing the HTML code that will define the widget

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<div id="background">
	<div class="book_pages">
	</div>
	<img class="previous_page" src="previous_page.png" />
	<img class="next_page" src="next_page.png" />
	<div class="book_links">
		<div class="red">1</div>
		<div class="red">2</div>
		<div class="red">3</div>
		<div class="red">4</div>
		<div class="red">5</div>
		<div class="red">6</div>
		<div class="red">7</div>
		<div class="red">8</div>
		<div class="red">9</div>
		<div class="red">10</div>
		<div class="red">11</div>
		<div class="red">12</div>
		<div class="red separator">13</div>
		<div class="green">14</div>
		<div class="green separator">15</div>
		<div class="blue">16</div>
		<div class="blue">17</div>
		<div class="blue">18</div>
		<div class="blue">19</div>
		<div class="blue">20</div>
		<div class="blue">21</div>
		<div class="blue">22</div>
		<div class="blue">23</div>
		<div class="blue">24</div>
		<div class="blue">25</div>
		<div class="blue">26</div>
		<div class="blue">27</div>
		<div class="blue">28</div>
		<div class="blue">29</div>
		<div class="blue separator">30</div>
		<div class="light_blue">31</div>	
	</div>
</div>

This is very simple, we have the .book_pages div where we will store the images of each page of the book and the .book_links div containing a link to each pair of pages. We also have two images that will serve as the next and previous page buttons. To save time and space we will generate the contents of the .book_pages div using javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
body, html { font-family: Arial,Helvetica,sans-serif }
 
#background { background: url('background.png'); height: 780px; margin: 0 auto; position: relative; width: 1004px }
 
.page_left, .page_right { background: #fff; height: 525px; position: absolute; top: 191px; width: 470px }
.page_left { left: 32px }
.page_right { background-position: right 0; left: 502px }
 
.page_left .shadow { background: url('shadow_left_page.png'); float: right; width: 470px; height: 525px; display: none }
.page_right .shadow { background: url('shadow_right_page.png'); float: left; width: 470px; height: 525px; display: none }
 
.book_links { bottom: 18px; left: 110px; position: absolute }
.previous_page { bottom: 18px; cursor: pointer; left: 30px; position: absolute }
.next_page { bottom: 18px; cursor: pointer; right: 30px; position: absolute } 
 
.red, .green, .blue, .light_blue { color: #000; cursor: pointer; float: left; font-size: 10px; line-height: 21px; text-align: center; width: 20px }
.red { background: #c8a9a9 }
.green { background: #afd1a9 }
.blue { background: #a9b0c3 }
.light_blue { background: #a9d1d1 }
.separator { margin-right: 3px }
.selected { background-image: url('active_page.png') }

There isn’t anything complicated about this code. We are just using the css position absolute property to correctly position each element inside the #background div. In case you don’t know, here’s what w3schools says about position: absolute

Generates an absolutely positioned element, positioned relative to the first parent element that has a position other than static. The element’s position is specified with the “left”, “top”, “right”, and “bottom” properties

The first parent element with a position other than static is the #background div which has position: relative, so the .page_left, .page_right, .previous_page, .next_page and .book_links elements will be positioned relative to it. Another thing worth noting is that the .page_right has the background-position set to right 0. This is because each image contains the background for both the .page_left and .page_right divs, so the one at .page_right needs to be aligned horizontally to the right

Let’s write some initial Javascript code now

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$(document).ready(function() {
	$('#background').book();
});
 
var speed = 700;
 
$.fn.book = function() {
	return $(this).each(function() {
		var book = this;
 
		for (var i = 1; i <= 62; i++) {
			var position = i % 2 == 1 ? 'page_left' : 'page_right';
			var image = i % 2 == 1 ? '"page' + i + ',' + (i + 1) + '.jpg"' : '"page' + (i - 1) + ',' + i + '.jpg"';
			var shadow = $.browser.msie ? '' : "<div class='shadow'></div>"
			$('.book_pages').append("<div class='page" + i + " " + position + "' style='background-image: url(" + image + ")'>" + shadow + "</div>");
		}
	});
};

In line 1 we call the jQuery read function which is called when the page is completely loaded. In line 2 we search for the element on the page that has the id = background and calls the book function we define below on it. Line 5 defines a variable that will specify the duration in milliseconds of the page flip animation.

The book function iterates over each matched element and for each of them adds 62 pages to the .book_pages. For each page, we check if the i variable is odd, if true it is a page positioned at the left, else it is a page positioned at the right. So if i equals 1 we will add a page on the left that will have page1,2.jpg as the background. Each page also has a .shadow div but these will only be used in a browser other than IE.

Your page should be looking like this now

We are now going to set the events, so that when the user clicks the numbers on the bottom or the next and previous buttons it changes to the corresponding page on the book. Add this to our book function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$(this).find('.red, .green, .blue, .light_blue').click(function() {
	var index = parseInt($(this).html());
	selectPage(book, index, true);
});
 
$(this).find('.previous_page').click(function() {
	selectPage(book, Math.max(1, book.selectedPage -1), true);
});
 
$(this).find('.next_page').click(function() {
	selectPage(book, Math.min($(book).find('.book_links > div').size(), book.selectedPage +1), true);
});
 
selectPage(book, 1, false);

The first 4 lines sets the click event on every div on the book that has the red, green, blue or light_blue classes. This event parses the element html as an int and calls a function to change the book page to this int

We then set the click event for the .previous_page button. This event calls a function to change the book page to the maximum between 1 and the current selected page -1

The click event for the .next_page button changes the page to the minimum between the number of children divs in the .book_links div and the current selected page +1

Let’s now write the selectPage function

var selectPage = function(book, index, animate) {
	if (book.animating) return;
	$(book).find('.red, .green, .blue, .light_blue').removeClass('selected');
	$(book).find('.book_links > div:nth-child(' + index + ')').addClass('selected');
 
	if (!animate) {
		animationCallback(book, index);
	} else {
		animationCallback(book, index); // temporary code
	}
	book.selectedPage = index;
};
 
var animationCallback = function(book, index) {
	$(book).find('.book_pages > div').css({ 'display': 'none', 'z-index': '0' });
	$(book).find('.page' + (index * 2 - 1) + ', .page' + (index * 2)).show().css('z-index', '1');
}

The first thing we do is check if the book is currently animating, if true we do nothing

We then remove remove the class selected from every .red, .green, .blue or .light_blue div in the book and add it only to the nth-child div of the .book_links div

We now check if we are going to select this page by animating or not. For now in both cases let’s just call the animationCallback function, later we will add the code to switch pages by animating.

The animationCallback find every child div of the .book_pages div and hides it (by setting the css display property to none) and sets the z-index css property to 0. The z-index property works this way: If an element has a bigger z-index than another element it will appear on front of that element.

We then select the corresponding pages and shows them and sets the z-index to 1 so that they appear in front of the others. So if we click the 3 link it will show the 5th and 6th pages.

The book is now working, all that is left to do now is add the more complicated animation code

Change the line with the // temporary code comment to this

if (index > book.selectedPage) {
	// set previous right page on front and animate
	var previousRightPage = $(book).find('.page' + (book.selectedPage * 2));
	previousRightPage.show().css('z-index', '2').css('background-position', '470px 0').animate({ width: 0 }, speed);
 
	// set current right page on front
	$(book).find('.page' + (index * 2)).show().css('z-index', '1');
 
	// animate current left page
	var leftPage = $(book).find('.page' + (index * 2 - 1));
	leftPage.show().css({ 'left' : '972px', 'z-index': '3', 'width' : '0' });
	book.animating = true;
	leftPage.animate({ left: 32, width: 470 }, speed, function() { animationCallback(book, index); }).find('.shadow').show().fadeOut(speed * 2);
} else if (index < book.selectedPage) {
	// set previous left page on front
	$(book).find('.page' + (book.selectedPage * 2 - 1)).show().css('z-index', '1');
 
	// set current left page on front and animate
	var currentLeftPage = $(book).find('.page' + (index * 2 - 1));
	currentLeftPage.show().css('z-index', '2').width(0).animate({ width: 470 }, speed);
 
	// animate current right page
	var rightPage = $(book).find('.page' + (index * 2));
	rightPage.show().css({ 'left' : '32px', 'z-index': '3', 'width' : '0' });
	book.animating = true;
	rightPage.animate({ left: 502, width: 470 }, speed, function() { animationCallback(book, index); }).find('.shadow').show().fadeOut(speed * 2);
}

And the animationCallback function to this

1
2
3
4
5
6
7
8
9
var animationCallback = function(book, index) {
	book.animating = false;
	$(book).find('.book_pages > div').css({ 'display': 'none', 'z-index': '0' }).width(470);
 
	// reset every right page
	$(book).find('.book_pages > div:nth-child(even)').css('background-position', 'right 0').width(470);
 
	$(book).find('.page' + (index * 2 - 1) + ', .page' + (index * 2)).show().css('z-index', '1');
}

The first thing we do in the animation code is check if the selectPage index is bigger than the current selectedPage, if true we will do some kind animation else we will do another

Let’s examine the first case:

We first select the previous right page, set it on front of the current right page, change its background position so that it’s not aligned to the right anymore, but starts at 470px horizontally and animates it’s width to 0, so that it will disappear and the current right page will take it’s position

We then make the current right page show and set it in front of the others, but below the previous right page and current left page

We then select the current left page, set it’s width to 0, set it in front of all the other pages, set it’s position to 972px (the end of the book) and animates it so that it grows back to 470 width and goes back to its original 32px left position. When the animation is complete we call the animationCallback function. We also display the current left page shadow during the animation and fades it out for a duration a bit longer than the animation duration

Since we have messed with the previous right page width and background position during the animation, we now have to reset it back to it’s original values in the animationCallback function, this is what line 6 does

Now let’s examine the second case:

The logic is almost the same. We set previous left page on front, but below current left page and current right page

Then select current left page, set it in front of previous left page and animate it’s width from 0 to 470

We then select the current right page set it’s width to 0, set in front of all the other pages, set it’s position to 32 (the start of the book) and animates it so that it grows back to 470 width and goes back to its original 502px left position.

That’s it the book is finished. Hope you liked this tutorial. If you have any questions post a comment and I will try to answer

Categories: CSS, jQuery Tags:

Todolistr is back

December 28th, 2010 No comments

Now that I have my server again I decided to put back on air my old project: Todolistr

It is a to-do list application written in Rails with lots of AJAX

I rewrote the code from scratch to run in Rails 3 and Ruby 1.9.2 and plan releasing the source code soon either on github (if I can get git to work on my windows machine) or googlecode

Categories: Rails Tags:

Creating an Image Gallery using CSS and jQuery

December 28th, 2010 2 comments

In this post I will teach you how to create a cool Image Gallery like the one you see in the Globo site

View Demo     Download Files

First let’s write the HTML code that will define the structure of the gallery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<div class="image_gallery">
	<div class="header"></div>
	<div class="thumbnails">
		<div class="content">
			<div class="background"></div>
			<div class="pictures">
				<div class="previous_thumbnail"><a href="#" class="normal"></a></div>
				<div class="viewport"><div class="viewport_content"></div></div>
				<div class="next_thumbnail"><a href="#" class="normal"></a></div>
			</div>
		</div>
		<div class="border"></div>
		<div class="slider"><a href="#"><img src="slider_up.gif"/></a></div>
	</div>
	<div class="picture"></div>	
	<a class="previous_image" href="#"><img src="previous_image.gif"/></a>
	<a class="next_image" href="#"><img src="next_image.gif"/></a>
	<div class="footer">
		<div class="slider"><a href="#"><img src="slider_down.gif"/></a></div>
		<div class="border"></div>
		<div class="content">
			<div class="background"></div>
			<div class="text"><h3>Adriano chega à Vila Cruzeiro com seu filho no colo</h3><p>André Durão / Globoesporte.com</p></div>
		</div>
	</div>
	<div class="hidden"></div>
</div>

The .header div will contain the text x of 12 pictures.

The .thumbnails div contains 3 divs. The .content div is where the thumbnails and the next/previous thumbnails buttons are located. The .border div contains a border of 5px height and the .slider div contains the button for collapsing/expanding the .thumbnails div. The .content div is divided in 2 divs: one for the transparent background and another one for the thumbnails and buttons. The div with the thumbnails has another div called .viewport which will have a fixed width and a child div called .viewport_content which will grow as large as needed to fit all the thumbnails.

The .picture div will contain current selected picture in full size. The .footer div has the same logic as the .thumbnails div, except it has a div for caption text.

The .hidden div is not actually visible, it will be used to place the full size images and wait for them to load

Now that we have our HTML defined, let’s start coding the CSS to style this gallery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
body, html { font: arial, helvetica, freesans, sans-serif }
img { border: 0 }
 
.image_gallery { background: #333; height: 640px; position: relative; width: 940px }
	.header { background: #000; color: #fff; font-size: 15px; height: 40px; line-height: 40px; padding: 0 15px }
	.header strong { font-size: 17px }
 
	.border { background: #191919; font-size: 0; height: 5px }
	.slider { font-size: 0; text-align: center }
	.content { overflow: hidden; position: relative }
	.background { background: #000; top: 0; height: 100%; position: relative }
	.hidden { display: none }
 
	.picture { position: absolute; z-index: 0 }
	.picture img { height: 600px; width: 940px }
 
	.thumbnails { position: absolute; top: 40px; width: 100%; z-index: 1 }
		.pictures { height: 88px; font-size: 0; position: absolute; top: 0; width: 100% }
 
	.next_image { right: 0; position: absolute; top: 299px; z-index: 1 }
	.previous_image { left: 0; position: absolute; top: 299px; z-index: 1 }
 
	.footer { bottom: 0; position: absolute; width: 100%; z-index: 1 }
		.text { position: absolute; top: 0 }
		.text h3 { color: #fff; font-size: 17px; padding: 8px 15px 5px; margin: 0 }
		.text p { color: #999; font-size: 15px; padding: 0 15px 10px; margin: 0 }

This css alone isn’t sufficient to give good results so we will start writing javascript now and later I will explain the css code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
$(document).ready(function() {
	$('.image_gallery').imageGallery([
		{ thumbnail: 'thumbnail-1.jpg', picture: 'picture-1.jpg', text: "<h3>Adriano chega à Vila Cruzeiro com seu filho no colo</h3><p>André Durão / Globoesporte.com</p>" },
		{ thumbnail: 'thumbnail-2.jpg', picture: 'picture-2.jpg', text: "<h3>No auditório, explica como será o projeto 'Imperadores da Vila'</h3><p>André Durão / Globoesporte.com</p>" },
		{ thumbnail: 'thumbnail-3.jpg', picture: 'picture-3.jpg', text: "<h3>Depois senta no chão para virar apenas espectador</h3><p>André Durão / Globoesporte.com</p>" },
		{ thumbnail: 'thumbnail-4.jpg', picture: 'picture-4.jpg', text: "<h3>Observa, sorridente, sua avó falar sobre o projeto</h3><p>André Durão / Globoesporte.com</p>" },
		{ thumbnail: 'thumbnail-5.jpg', picture: 'picture-5.jpg', text: "<h3>Ao final dos comentários de sua avó, ele a abraça e sorri</h3><p>André Durão / Globoesporte.com</p>" },
		{ thumbnail: 'thumbnail-6.jpg', picture: 'picture-6.jpg', text: "<h3>Presentes que Adriano distribuirá na Vila Cruzeiro</h3><p>André Durão / Globoesporte.com</p>" },
		{ thumbnail: 'thumbnail-7.jpg', picture: 'picture-7.jpg', text: "<h3>O carisma de Adriano é tão grande que a amiga exibe uma foto sua com ele</h3><p>André Durão / Globoesporte.com</p>" },
		{ thumbnail: 'thumbnail-8.jpg', picture: 'picture-8.jpg', text: "<h3>Vários moradores da Vila Cruzeiro aparecem na grade apenas para ver o craque do Roma</h3><p>André Durão / Globoesporte.com</p>" },
		{ thumbnail: 'thumbnail-9.jpg', picture: 'picture-9.jpg', text: "<h3>Adriano distribui presentes para os moradores da Vila Cruzeiro</h3><p>André Durão / Globoesporte.com</p>" },
		{ thumbnail: 'thumbnail-10.jpg', picture: 'picture-10.jpg', text: "<h3>E se divert com seus filhos</h3><p>André Durão / Globoesporte.com</p>" },
		{ thumbnail: 'thumbnail-11.jpg', picture: 'picture-11.jpg', text: "<h3>Adriano é ovacionado pela criançada</h3><p>André Durão / Globoesporte.com</p>" },
		{ thumbnail: 'thumbnail-12.jpg', picture: 'picture-12.jpg', text: "<h3>Adriano descansa sem camisa na porta de uma loja</h3><p>André Durão / Globoesporte.com</p>" }
	]);
});
 
$.fn.imageGallery = function(data) {
	$(this).each(function() {
		this.firstVisibleThumbnail = 0;
		$(this).find('.background').css('opacity', '0.75');
 
		addThumbnails(this, data)
		sliderUp(this, data);
		sliderDown(this, data);
		nextImage(this, data);
		previousImage(this, data);
		nextThumbnail(this, data);
		previousThumbnail(this, data);
		cssSprites(this, data);
	});
};
 
var addThumbnails = function(gallery, data) {
};
 
var sliderUp = function(gallery, data) {
};
 
var sliderDown = function(gallery, data) {
};
 
var nextImage = function(gallery, data) {
};
 
var previousImage = function(gallery, data) {
};
 
var nextThumbnail = function(gallery, data) {
};
 
var previousThumbnail = function(gallery, data) {
};
 
var cssSprites = function(gallery, data) {
};
 
var selectPicture = function(gallery, data, index, fade) {
};

In line 1 we call the jQuery read function which is called when the page is completely loaded. In line 2 we search for every element on the page which has the class image_gallery and calls the imageGallery function we define below. The argument passed to the imageGallery function is an array containing for every picture in the gallery the url of the thumbnail, the url of the full-size image and the caption text

The imageGallery iterates over each matched element, sets the opacity of the .background div to 0.75 and calls several functions:

The addThumbnails function adds the thumbnails images to the .viewport_content and sets their click events.

The sliderUp function sets the events for collapsing/expanding the .thumbnails div when the user clicks the slider up button. The sliderDown does the same thing for the .footer div and the slider down button.

The nextImage and previousImage functions set the events for when the user clicks the big buttons on the side of the full-size image

The previousThumbnail and nextThumbnail functions set the events for when the user clicks the
small buttons on the side of the thumbnails

The cssSprites function changes the images of the previous and next thumbnails buttons when the user hovers or clicks these buttons

Let’s start by defining the sliderUp and sliderDown functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
var sliderUp = function(gallery, data) {
	var content = $(gallery).find('.thumbnails .content');
	content.height($(gallery).find('.pictures').height());
 
	$(gallery).find('.thumbnails .slider a').click(function() {
		if (!this.collapsed) {
			this.collapsed = true;
			content.animate({ height: 0 }, 200);
		} else {
			this.collapsed = false;
			content.animate({ height: $(gallery).find('.pictures').height() }, 200);
		}
		return false;
	});
};
 
var sliderDown = function(gallery, data) {
	var content = $(gallery).find('.footer .content');
	content.height($(gallery).find('.text').height());
 
	$(gallery).find('.footer .slider a').click(function() {
		if (!this.collapsed) {
			this.collapsed = true;
			content.animate({ height: 0 }, 200);
		} else {
			this.collapsed = false;
			content.animate({ height: $(gallery).find('.text').height() }, 200);
		}
		return false;
	});
};

The first thing the sliderUp does is set the height for the .content div to a value equal to the height of the .pictures div which is 88px. This is necessary because the .background div, which is a child of the .content div has the css property height: 100%. This height declaration is useless unless the parent element has the height set. You may be wondering why I did this height calculation in javascript instead of css. I did it because the sliderDown function does the same thing but the height of it’s content may vary depending on the size of the caption text

The next thing these functions do is set the events for when the user clicks the slider button. This event check if the slider is not collapsed, if true it animates the height of the .content div to 0, else it animates the height to the height of the .pictures or .text div

The gallery should be looking like this now. Clicking the slider up or down buttons should collapse or expand the header or footer:

Now let me explain the css code for the footer and header

1
2
3
4
5
6
7
8
	.content { overflow: hidden; position: relative }
	.background { background: #000; top: 0; height: 100%; position: relative }
 
	.thumbnails { position: absolute; top: 40px; width: 100%; z-index: 1 }
		.pictures { height: 88px; font-size: 0; position: absolute; top: 0; width: 100% }
 
	.footer { bottom: 0; position: absolute; width: 100%; z-index: 1 }
		.text { position: absolute; top: 0 }

The tricky part is that the .content div will have 2 child divs, the transparent background and another div (.pictures or .text) which need to be in front of the background div. I did this by setting the css position of the .pictures and .text div to absolute. Let’s see what w3schools says about position absolute:

Generates an absolutely positioned element, positioned relative to the first parent element that has a position other than static. The element’s position is specified with the “left”, “top”, “right”, and “bottom” properties

The first parent element which has a position other than static is .content (which has position relative). This way, since the top property is set to 0, the .pictures and .text divs will appear right in the beginning of the .content div, and in front of the .background div

Let’s write now the addThumbnails function

1
2
3
4
5
6
7
8
9
10
11
12
var addThumbnails = function(gallery, data) {
	for (var i in data) {
		var image = $('<a href="#"><img src="' + data[i].thumbnail + '"/></a>');
		$(image).click(function() {
			var index = $(this).prevAll().size();
			selectPicture(gallery, data, index, true);
			return false;
		});
		$(gallery).find('.viewport_content').append(image);
	}
	selectPicture(gallery, data, 0, false);
};

This function is very simple, it iterates over our data, creates the corresponding image thumbnails and appends them to the .viewport_content. It also sets the event for when the user clicks the thumbnail. The event calls the jQuery prevAll function which returns all preceding images of the clicked image. So if we clicked the 3rd image the function would return the 2 preceding images. We then calculate the size of this collection to find the correct index of the selectedPicture and call the selectPicture we will implement later

And now the CSS to style the viewport

1
2
3
4
5
6
7
.viewport { margin: 0 -2px; float: left; width: 847px; height: 88px; overflow: hidden }
.viewport_content { white-space: nowrap }
.viewport_content a { display: inline-block; margin: 10px 2px; overflow: hidden; vertical-align: bottom }
.viewport_content img { height: 68px; width: 90px }
 
.viewport a:hover, .image_gallery .selected { border: 3px solid #fff; _margin-bottom: 7px }
.viewport a:hover img, .image_gallery .selected img { margin: -3px }

We set a fixed width on .viewport and overflow to hidden so that the rest of the content that doesn’t fit the width be invisible. On .viewport_content we set white-space to nowrap so that the images will never wrap to a new line. The code for a, a:hover and a:hover img is all part of the inner border code for the current hovered or selected image. I got it from another site. The _margin-bottom property is a css hack that only IE6 will read

Now let’s style the next/previous thumbnails buttons

1
2
3
4
5
6
7
.previous_thumbnail, .next_thumbnail { float: left }
.previous_thumbnail a, .image_gallery .next_thumbnail a { display: inline-block; width: 28px; height: 67px; vertical-align: bottom; margin: 10px 10px }
.previous_thumbnail a { background: url('previous_thumbnail.png') }
.next_thumbnail a { background: url('next_thumbnail.png') }
.previous_thumbnail a.normal, .next_thumbnail a.normal { background-position: 0 0 }
.previous_thumbnail a.hover, .next_thumbnail a.hover { background-position: -28px 0 }
.previous_thumbnail a.clicked, .next_thumbnail a.clicked { background-position: -56px 0 }

We are using CSS sprites here. Instead of having 3 different images for each of the states of the button we will use a single image with all of the states. This is good because when the user hovers the button there won’t be a delay before the hover image appear due to downloading of the image.

To make use of CSS sprites we change the background-position of the a.hover class to -28 px 0, so that it starts at 28 pixels horizontally and 0 pixels vertically of the image

It’s now time to implement the selectPicture function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var selectPicture = function(gallery, data, index, fade) {
	if (index == gallery.selectedPicture) return;
	gallery.selectedPicture = index;
	if (fade) {
		$('.picture img').stop().fadeOut(500, function() {
			var image = $('<img src="' + data[index].picture + '"/>');
			$(gallery).find('.hidden').html(image.html());
			$(image).load(function() {
				$('.picture').html('<img src="' + data[index].picture + '"/>').find('img').hide().fadeIn(500);
			});
		});
	} else {
		$('.picture').html('<img src="' + data[index].picture + '"/>').show();
	}
	$(gallery).find('.text').html(data[index].text);
	$(gallery).find('.header').html('<strong>' + (index +1) + '</strong> of <strong>' + data.length + '</strong> pictures');
	$(gallery).find('.viewport_content a').removeClass('selected');
	$(gallery).find('.viewport_content a:nth-child(' + (index +1) + ')').addClass('selected');
 
	index == 0 ? $(gallery).find('.previous_image').hide() : $(gallery).find('.previous_image').show();
	index == data.length -1 ? $(gallery).find('.next_image').hide() : $(gallery).find('.next_image').show();
};

First it checks if the picture to selected is the current selectedPicture, if true it does nothing

Then it checks the fade argument, if false it just shows the full-size picture on the .picture div. If fade is set to true it first fadesOut the current displayed image then create the new image, then inserts this new image on the invisible hidden div and sets the load event (which is called when the image is completely loaded) to insert the image on the .picture div and fadeIn it

The function then sets the image caption on the .text div and also sets the x of 12 pictures on the .header div. It then removes the class selected from all .viewport_content a and add this class to the n-th a (the current selected one)

It then checks if the index is equal to 0, if true it hides the big previous image button else it shows it. Then does the same for the next image button if index is the last image

Now that we have the function for setting the image caption you can remove the initial caption from the HTML

Let’s now write the nextThumbnail and previousThumbnail functions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var nextThumbnail = function(gallery, data) {
	var viewportOffset = $(gallery).find('.viewport_content').attr('offsetLeft');
	$(gallery).find('.next_thumbnail').click(function() {
		if (gallery.firstVisibleThumbnail + 8 == data.length -1) return false;
		gallery.firstVisibleThumbnail++;
		var viewport = $(gallery).find('.viewport');
		var offset = $(gallery).find('a:nth-child(' + (gallery.firstVisibleThumbnail +1) + ')').attr('offsetLeft');
		viewport.stop().animate({ scrollLeft: offset - viewportOffset -2 }, 200);
		return false;
	});
};
 
var previousThumbnail = function(gallery, data) {
	var viewportOffset = $(gallery).find('.viewport_content').attr('offsetLeft');
	$(gallery).find('.previous_thumbnail').click(function() {
		if (gallery.firstVisibleThumbnail == 0) return false;
		gallery.firstVisibleThumbnail--;
		var viewport = $(gallery).find('.viewport');
		var offset = $(gallery).find('a:nth-child(' + (gallery.firstVisibleThumbnail +1) + ')').attr('offsetLeft');
		viewport.stop().animate({ scrollLeft: offset - viewportOffset -2 }, 200);
		return false;
	});
};

The nextThumbnail function first calculates the offsetLeft from the .viewport_content. The offsetLeft represents how far in pixels the element is horizontally from the left of the page

The function then sets the click event for the .next_thumbnail. We now see the firstVisibleThumbnail variable. This variable will store the index of the first thumbnail that is visible on the .viewport_content. We check if the firstVisibleThumbnail + 8 is equal to the length of the images data. If so it means we have reached the end of the .viewport_content and don’t need to do anything.

The function then increments the firstVisibleThumbnail variable and calculates the offset of the firstVisibleThumbnail. It then animates the scrollLeft property of the .viewport to our calculated offset – the offset of the viewport – 2 (the margin)

The previousThumbnail follows the same logic

Now let’s add the code to change the image of the buttons when the user hovers or clicks them

var cssSprites = function(gallery, data) {
	$(gallery).find('.previous_thumbnail a, .next_thumbnail a').hover(function() { 
		$(this).removeClass().addClass('hover');
	}, function() { 
		$(this).removeClass().addClass('normal');
	}).mousedown(function() {
		$(this).removeClass().addClass('clicked');
	}).mouseup(function() { 
		$(this).removeClass().addClass('hover');
	});
};

This function is very simple, it sets the class to hover when the user’s mouse enters the button. Sets the class to normal when the mouse exits the button. Sets the class to clicked when the user presses the mouse on button and sets the class on hover when the user releases the mouse button while still on the button

The gallery is almost ready. It should be looking like this now:

All that is left now is the more complicated nextImage and previousImage functions:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var nextImage = function(gallery, data) {
	var viewportOffset = $(gallery).find('.viewport_content').attr('offsetLeft');
	$(gallery).find('.next_image').click(function() {
		selectPicture(gallery, data, gallery.selectedPicture +1, true);
		if (gallery.selectedPicture > gallery.firstVisibleThumbnail + 8) {
			var viewport = $(gallery).find('.viewport');
			viewport.stop().animate({ scrollLeft: $(gallery).find('a.selected').attr('offsetLeft') - viewportOffset - 8 * 94 -2 }, 200);
			gallery.firstVisibleThumbnail = gallery.selectedPicture - 8;
		}
		if (gallery.selectedPicture < gallery.firstVisibleThumbnail) {
			var viewport = $(gallery).find('.viewport');
			viewport.stop().animate({ scrollLeft: $(gallery).find('a.selected').attr('offsetLeft') - viewportOffset -2 }, 200);
			gallery.firstVisibleThumbnail = gallery.selectedPicture;
		}
		return false;
	});
};

The nextImage function sets the click event on the .next_image a. The event calls the function selectPicture passing the selectedPicture +1 as the index argument. This works but what if the next image isn’t visible in the .viewport_content? We have then to animate the scrollLeft position to show the selectedPicture. The function checks if the selectedPicture is before or after the visible area of the .viewport_content. If it’s after it animates the scrollLeft to position the selectedPicture as the last visible thumbnail, else it animates to position as the first visible thumbnail.

The previousImage function follows the same logic

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var previousImage = function(gallery, data) {
	var viewportOffset = $(gallery).find('.viewport_content').attr('offsetLeft');
	$(gallery).find('.previous_image').click(function() {
		selectPicture(gallery, data, gallery.selectedPicture -1, true);
		if (gallery.selectedPicture > gallery.firstVisibleThumbnail + 8) {
			var viewport = $(gallery).find('.viewport');
			viewport.stop().animate({ scrollLeft: $(gallery).find('a.selected').attr('offsetLeft') - viewportOffset - 8 * 94 -2 }, 200);
			gallery.firstVisibleThumbnail = gallery.selectedPicture - 8;
		}
		if (gallery.selectedPicture < gallery.firstVisibleThumbnail) {
			var viewport = $(gallery).find('.viewport');
			viewport.stop().animate({ scrollLeft: $(gallery).find('a.selected').attr('offsetLeft') - viewportOffset -2 }, 200);
			gallery.firstVisibleThumbnail = gallery.selectedPicture;
		}
		return false;
	});
};

That’s it the gallery is finished. Hope you liked this tutorial. If you have any questions post a comment and I will try to answer

Categories: CSS, jQuery Tags:

Creating a Scrollable News Panel using CSS and jQuery

December 13th, 2010 No comments

In this post I will teach you how to create a Scrollable News Panel like the one you see in the Globo site

View Demo     Download Files

First let’s write the HTML code that will define this list

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
<div class="news_panel">
	<div class="news_button_up"><a href="#" class="inactive"></a></div><div class="clear"></div>
	<ul>
		<li class="first">
			<p>17:26</p>
			<h3><a href="#">Partido no poder lidera legislativas em Kosovo</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="sop-ap-spani-span-gb_fran.jpg" /></a>
			<p>17:18</p>
			<h3><a href="#">Enteado do guitarrista do Pink Floyd é preso após manifestações</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="guadalupe.jpg" /></a>
			<p>17:17</p>
			<h3><a href="#">Mexicanos celebram dia de Nossa Senhora de Guadalupe</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<p>16:58</p>
			<h3><a href="#">Tempestades causam devastação no Oriente Médio</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="atentado-estocolmo.jpg" /></a>
			<p>15:02</p>
			<h3><a href="#">Autor de atentado em Estocolmo estudou na Grã-Betanha, diz jornal</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="natal-hungria.jpg" /></a>
			<p>15:02</p>
			<h3><a href="#">Húngaros fazem corrida de caridade de Natal com pouca roupa</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="bloomberg.jpg" /></a>
			<p>14:32</p>
			<h3><a href="#">Bloomberg diz que não concorrerá à Presidência dos Estados Unidos</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="china.jpg" /></a>
			<p>12:12</p>
			<h3><a href="#">Em decisão 'prudente', China ressalta empenho para combater a inflação</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="papais-noeis.jpg" /></a>
			<p>12:11</p>
			<h3><a href="#">Papais Noéis disputam corrida de rua na Escócia</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<p>11:18</p>
			<h3><a href="#">Piratas da Somália capturam navio de bandeira panamenha</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="brasilia.jpg" /></a>
			<p>10:17</p>
			<h3><a href="#">Para EUA, Brasília é vulnerável a ataques, diz jornal citando...</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<p>09:12</p>
			<h3><a href="#">Situação na fronteira México-Guatemala é dramática, diz WikiLeaks</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="presidente-associacao.jpg" /></a>
			<p>09:08</p>
			<h3><a href="#">Presidente da associação iraniana de jornalistas é condenado à prisão</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="scotland-yard.jpg" /></a>
			<p>08:46</p>
			<h3><a href="#">Chefe da Scotland Yard pode sair após ataque a carro de príncipe</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="inteligencia-sueca.jpg" /></a>
			<p>08:32</p>
			<h3><a href="#">Inteligência sueca confirma que explosões foram terrorismo</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="confronto.jpg" /></a>
			<p>08:30<p>
			<h3><a href="#">Confronto entre grupos armados do México deixa 11 mortos</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<p>08:10<p>
			<h3><a href="#">Suécia foi alvo de 'ataque suicida' que falhou, diz chanceler</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<p>08:10<p>
			<h3><a href="#">Atentados contra policiais e xiitas deixam dez mortos no Iraque</a></h3>
			<div class="clear"></div>
		</li>
		<li>
			<a href="#" class="inner_border"><img src="votacao.jpg" /></a>
			<p>08:06<p>
			<h3><a href="#">Começa votação para eleger 120 deputados em Kosovo</a></h3>
			<div class="clear"></div>
		</li>
	</ul>
	<div class="news_button_down"><a href="#"></a></div><div class="clear"></div>
</div>

Then add CSS to style it

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
body, html { font: arial, helvetica, freesans, sans-serif }
img { border: 0 }
a { color: #a80000; text-decoration: none }
a:hover { text-decoration: underline }
.clear { clear: both }
 
.inner_border { float: left; font-size: 0; overflow: hidden; margin-right: 10px }
.inner_border:hover { border: 3px solid #a80000 }
.inner_border:hover img { margin: -3px }
 
.news_panel ul { background: url('background_news_panel.png') repeat-y; border: 1px solid #ddd; border-width: 0 1px; height: 305px; font-size: 13px; list-style: none; margin: 0; padding: 0 12px 0 52px; overflow-y: hidden; width: 234px }
.news_panel ul li { border-top: 1px dotted #666; display: block; padding: 8px 0 }
.news_panel ul li img { height: 68px; width: 90px }
.news_panel ul li p { color: #666; font-weight: bold; margin: 0; padding: 0 }
.news_panel ul li h3 { font-size: 13px; margin: 0 }
.news_panel ul li.first { border: none }

There’s nothing too complicated about this CSS code. Just the inner_border class that I copy pasted from another tutorial and the overflow-y: hidden property of our .news_panel ul that hides the scrollbar of the ul and the rest of it’s content that doesn’t fit on it’s height

Now let’s style the up and down buttons

1
2
3
4
5
6
7
.news_button_up, .news_button_down { float: left }
.news_button_up a { background: url('background_news_button_up.png'); display: block; height: 29px; width: 300px }
.news_button_down a { background: url('background_news_button_down.png'); display: block; height: 29px; width: 300px }
.news_button_up a.hover, .news_button_down a.hover { background-position: 0 -29px }
.news_button_up a.normal, .news_button_down a.normal { background-position: 0 0 }
.news_button_up a.clicked, .news_button_down a.clicked { background-position: 0 -87px }
.news_button_up a.inactive, .news_button_down a.inactive { background-position: 0 -58px }

We are using CSS sprites here. Instead of having 4 different images for each of the states of the button we will use a single image with all of the states. This is good because when the user hovers the button there won’t be a delay before the hover image appear due to downloading of the image.

To make use of CSS sprites we change the background-position of the a.hover class to 0 -29px, so that it starts at 0 pixels horizontally and 29 pixels vertically of the image

Your panel should look like this now:

Now all that is left to do is add the javascript code to make the panel scroll when the user presses the up and down button

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
$(document).ready(function() {
	$('.news_panel').newsPanel();
});
 
$.fn.newsPanel = function() {
	$(this).each(function() {
		var buttonUp = $(this).find('.news_button_up');
		var buttonDown = $(this).find('.news_button_down');
		var ul = $(this).find('ul');
		var height = ul.height();
		var totalHeight = 0;
		ul.find('li').each(function() { 
			totalHeight += $(this).outerHeight();
		});
 
		$(this).find('.news_button_up, .news_button_down').mousedown(function(e) {
			if (!$(this).find('a').hasClass('inactive')) {
				$(this).find('a').removeClass().addClass('clicked');
			}
		});
 
		$(this).find('.news_button_up, .news_button_down').hover(function() {
			if (!$(this).find('a').hasClass('inactive')) {
				$(this).find('a').removeClass().addClass('hover');
			}
		}, function() {
			if (!$(this).find('a').hasClass('inactive')) {
				$(this).find('a').removeClass().addClass('normal');
			}
		});
 
		var scrollCallback = function() {
			var scrollTop = ul.attr('scrollTop');
			if (scrollTop == 0) {
				buttonUp.find('a').removeClass().addClass('inactive');
			} else {
				if (buttonUp.find('a').hasClass('inactive')) {
					buttonUp.find('a').removeClass().addClass('normal');
				}
			}
			if (scrollTop >= totalHeight - height) {
				buttonDown.find('a').removeClass().addClass('inactive');
			} else {
				if (buttonDown.find('a').hasClass('inactive')) {
					buttonDown.find('a').removeClass().addClass('normal');
				}
			}
		};
 
		buttonUp.click(function() {
			if (!$(this).find('a').hasClass('inactive')) {
				var scrollTop = ul.attr('scrollTop');
				ul.stop().animate({ scrollTop: scrollTop - height }, 400, scrollCallback);
				$(this).find('a').removeClass().addClass('hover');
			}
			return false;
		});
		buttonDown.click(function() {
			if (!$(this).find('a').hasClass('inactive')) {
				var scrollTop = ul.attr('scrollTop');
				ul.stop().animate({ scrollTop: scrollTop + height }, 400, scrollCallback);
				$(this).find('a').removeClass().addClass('hover');
			}
			return false;
		});
	});
};

In line 1 we call the jQuery ready function, which is called when the page is completely loaded. In line 2 we select the element in the page which has class = news_panel and calls the newsPanel function we define below

The newsPanel iterates over each matched element and them calculates the visible height of the ul (using ul.height()) e and the total size of the ul (summing the outerHeight of each li, outerHeight() is the same as height() except it also counts the border and paddings)

We then set the mousedown event to change the CSS image by setting the class of the a tag to clicked if the tag doesn’t have the class inactive. Later we do the same for the hover event, setting the hover class when the mouse enters the button and the normal class when the mouse leaves the button

We then declare a callback function, that will be called when the scrolling animation is finished. It checks the scrolling position of the ul, and if it is 0 it sets the up button to inactive else it sets the top button to normal state. Then it does the same logic for the down button, checking if the scrolling position is >= the total height of the ul – the visible height of the ul

The last thing we do is setting the click events on the up and down buttons. The event checks if the button is inactive and if not animates the scrolling position by summing or subtracting the visible height of the ul to it. The function returns false to prevent the browser window url changing to #

That’s it the panel is finished. Hope you liked this tutorial. If you have any questions post a comment and I will try to answer

Categories: CSS, jQuery Tags:

Creating a Highlights Image Gallery using CSS and jQuery

December 12th, 2010 No comments

In this post I will teach you how to create a Highlights Image Gallery like the one you see in the front page of GameSpot

View Demo     Download Files

First add this your html

1
<div id="highlights"></div>

The let’s write that javascript that will create the image gallery

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
$(document).ready(function() {
	$('#highlights').highlights([
		{ image: 'mass-effect-2.jpg', link: '#1', description: "<h3>Mass Effect 2</h3><p>Meet the voices behind some of the characters in Mass Effect 2</p>" },
		{ image: 'dantes-inferno.jpg', link: '#2', description: "<h3>Dante's Inferno: Anger Preview</h3><p>Dante's tribulations continue with our hands-on look at the upcoming demo of the games, plus new details on the Anger level</p>" },
		{ image: 'splinter-cell-conviction.jpg', link: '#3', description: "<h3>12 minutes of Splinter Cell: Conviction</h3><p>Take a look at the first 12 minutes of Splinter Cell: Conviction</p>" },
		{ image: 'dantes-inferno.jpg', link: '#4', description: "<h3>Dante's Inferno: Anger Preview</h3><p>Dante's tribulations continue with our hands-on look at the upcoming demo of the games, plus new details on the Anger level</p>" },
		{ image: 'mass-effect-2.jpg', link: '#5', description: "<h3>Mass Effect 2</h3><p>Meet the voices behind some of the characters in Mass Effect 2</p>" }
	]);
});
 
$.fn.highlights = function(data) {
	$(this).each(function() {
		buildHTML(this, data);
		buildEvents(this, data);
		selectPicture(this, data, 0, false);
	});
};
 
function buildHTML(highlights, data) {
}
 
function buildEvents(highlights, data) {
}
 
function selectPicture(highlights, data, index, fade) {
}

In line 1 we call the jQuery ready function, which is called when the page is completely loaded. In line 2 we select the element in the page which has the id = highlights and calls the highlights function we define below. Lines 2 to 8 contain an array defining each item of the gallery

The highlights function calls 3 functions that we will define later. The buildHTML function will build the necessary HTML markup to render the menu. The buildEvents function sets the mouse events for displaying the current hovered image. The selectPicture function makes the given index image appear and also shows it’s text

Let’s write the buildHTML function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function buildHTML(highlights, data) {
	var previews = $('<div class="highlights_previews"></div>');
	for (var i in data) {
		var image = $('<img src="'+ data[i].image +'"/>');
		var link = $('<a href="'+ data[i].link +'"></a>');
		link.append(image);
		previews.append(link);
	}
 
	var images = $('<div class="highlights_images"></div>');
	for (var i in data) {
		var image = $('<img src="'+ data[i].image +'"/>');
		image.css('display', 'none')
		images.append(image);
	}
 
	var description = $('<div class="highlights_description"></div>');
 
	$(highlights).append(previews).append(images).append(description);
}

This function is very simple, it just iterates over our data and creates the corresponding HTML tags. Note that the big images in highlights_images are initially set to not appear. We will show the later using the selectPicture function

Now we need some CSS to style these elements

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
body, html { font-size: 13px; font-family: "Lucida Grande", "Lucida Sans Unicode", Arial, Verdana, sans-serif, sans; line-height: 1.6em }
 
#highlights { width: 620px; height: 380px; background: #000; position: relative }
 
.highlights_previews { width: 88px; padding: 6px 6px 0; font-size: 0; line-height: 0; position: absolute; z-index: 2 }
.highlights_previews a { float: left; margin-bottom: 7px }
.highlights_previews a img { border: 1px solid black; width: 88px; height: 66px }
 
.highlights_images { position: relative }
.highlights_images img { position: absolute; width: 620px; height: 380px }
 
.highlights_description { bottom: 0px; left: 100px; width: 480px; background: #000; position: absolute; padding: 10px 20px }
.highlights_description h3 { color: #ec8901; margin: 10px 0 }
.highlights_description p { margin: 10px 0; color: #fff }
.highlights_description a { text-decoration: none }

This css code uses position: absolute to correctly position each one of the 3 divs of the gallery. You can learn about the css position property here

Your gallery should look like this:

Now let’s write the buildEvents function

1
2
3
4
5
6
function buildEvents(highlights, data) {
	$(highlights).find('.highlights_previews img').hover(function() {
		var index = $(this).parent().prevAll().length;
		selectPicture(highlights, data, index, true);
	});
}

What this function does is find every image inside the .highlights_previews div and sets the hover function. The hover function executes the function given as argument when the mouse enters the image

What the function given as argument does is find the index of the hovered image, it does that by selecting the parent of the image, which is an a tag, and finds all a tags that precedes this tag, then calculates the length of this collection and calls the selectPicture function

Now let’s build the selectPicture function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function selectPicture(highlights, data, index, fade) {
	if (index != highlights.selectedIndex) {
		highlights.selectedIndex = index;
 
		$(highlights).find('.highlights_images img:visible').fadeOut('fast');
		var image = $(highlights).find('.highlights_images img:nth-child('+ (index +1) +')');
		fade ? image.fadeIn('fast') : image.show();
 
		var text = '<a href="'+ data[index].link +'">' + data[index].description + '</a>';
		var description = $(highlights).find('.highlights_description');
 
		description.css('opacity', '0.75').css('height', 'auto').html(text);
		if (fade) {
			var height = description.height();
			description.height(0);
			description.stop().animate({ 'height': height }, 'fast');
		}
	}
}

First the function checks if the current selected image is the one the user hovered. We don’t want to execute the animations when the user hovers the already selected image

Then the function finds all images that are currently visible in the highlights_images div and hides them using the fadeOut effect. After that the function finds the n-th image in the div where n is the value of index and shows it using fadeIn or show

The function then sets the text of the current image in the .highlights_description div, sets it’s transparency to 0.75 (we could have set the transparency in pure css but it wouldn’t work in IE6 that way) and then sets its height to auto, so that the browser automatically calculates the correct height of the div

If the fade variable is set to true the function will do a nice animation by setting the text div height to 0 then animating it until it hits the height calculated before

The gallery is now done, all that is left is to add the inner border to the current hovered images. I won’t explain this code because I just copy and pasted it from another site. It just works

1
2
3
.highlights_previews a { float: left; margin-bottom: 7px; overflow: hidden }
.highlights_previews a:hover { border: 3px solid #cb750c }
.highlights_previews a:hover img { margin: -3px }

Hope you liked this tutorial. If you have any questions post a comment and I will try to answer

Categories: CSS, jQuery Tags:

Creating a Multi-Level Dropdown Menu using CSS and jQuery

December 11th, 2010 2 comments

In this post i will teach you how to create a Multi-Level Dropdown Menu using CSS and jQuery

View Demo     Download Files

The menu must:

  • Support an infinite number of levels
  • Support menu items of variable width
  • Have a minimal ammount of CSS and Javascript code
  • Work across all major browsers including IE6 (although it won’t have the slide effect on it)

Let’s start by writing the HTML that will define the structure of menu

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<ul id="menu" class="clear">
	<li><a href="#">Item 1</a>
		<ul>
			<li><a href="#">Item with a long title 1.1</a></li>
			<li><a href="#">Item 1.2</a></li>
			<li><a href="#">Item 1.3</a></li>
			<li><a href="#">Item 1.4</a></li>
		</ul>
	</li>
	<li><a href="#">Item 2</a></li>
	<li><a href="#">Item 3</a></li>
	<li><a href="#">Item 4</a>
		<ul>
			<li><a href="#">Item with a very loooooooooooooooooooong title 4.1</a></li>
			<li><a href="#">Item 4.2</a>
				<ul>
					<li><a href="#">Item with a very long title 4.2.1</a></li>
					<li><a href="#">Item 4.2.2</a></li>
					<li><a href="#">Item 4.2.3</a></li>
					<li><a href="#">Item 4.2.4</a>
						<ul>
							<li><a href="#">Item with a very loooooooong title 4.2.3.1</a></li>
							<li><a href="#">Item 4.2.3.2</a></li>
							<li><a href="#">Item 4.2.3.3</a></li>
							<li><a href="#">Item 4.2.3.4</a></li>
						</ul>
					</li>
				</ul>
			</li>
			<li><a href="#">Item 4.3</a></li>
			<li><a href="#">Item 4.4</a></li>
		</ul>
	</li>
</ul>

As you can see this is very simple, just some nested unordered lists representing each group of items from the menu

Now let’s begin styling the menu

1
2
3
4
5
6
7
8
9
.clear { height: 100% }
.clear:after { content: ''; display: block; clear: both }
 
#menu, #menu ul { list-style: none; margin: 0; padding: 0 }
 
#menu li { background: #bdd2ff; border-right: 1px solid #fff; float: left; white-space: nowrap }
#menu li a { display: block; padding: 5px 20px; text-decoration: none; color: #13a }
 
#menu ul { display: none }

We put float: left on #menu li so that each li stay side by side, the a uses display: block so that it occupies the whole li, white-space: nowrap is used to prevent the text of each item from wrapping to the next line. We also hide any secondary menus, we will style them later

The .clear class is a class i use so that you don’t need to put an element with clear: both after floated elements. It does that by using CSS to insert an element with clear: both after the matched element. The height: 100% does this trick for IE

Your menu should look like this now:

Now let’s style the secondary menus

First add position: relative to line 6 from previous code

1
#menu li { background: #bdd2ff; border-right: 1px solid #fff; position: relative; float: left; white-space: nowrap }

Then add this

1
2
3
4
#menu ul { background: #fff; position: absolute }
#menu ul li { background: #aabde6; border-top: 1px solid #bdd2ff; border-right: 0px solid transparent; float: none }
 
#menu ul ul { top: -1px; left: 100% }

We put float: none on the lis because they will stay one below the other now. We also set position: absolute on each ul. What position: absolute does is position an element absolutely in the screen relative to the first parent element that has a position other than static, in our case their parent li which we put position: relative on it. The background declaration on the ul is to prevent a bug in IE

The first level ul don’t need anything else, it will be already positioned correctly. The second and further level ul will need to have it’s position changed. By setting top: 0px we make them have their top position right where their parent li starts. By setting left: 100% we make them have their left position at 100% the size of their parent li. We change top to -1px later because of the border-top 1px in the li

All the menus should be positioned correctly now:

We now need to use javascript to show and hide each menu window when the user hovers their parent li

First add display: none to each menu ul so that they start hidden

1
#menu ul { background: #fff; display: none; position: absolute }

Now let’s start coding javascript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$(document).ready(function() {
	$('#menu').menu();
});
 
var ie = $.browser.msie && $.browser.version < 8.0;
 
$.fn.menu = function() {
	$(this).find('li').hover(function() {
		$(this).addClass('hover');
		ie ? $(this).find('> ul').fadeIn() : $(this).find('> ul').slideDown(250);
	}, function() {
		$(this).removeClass('hover');
		ie ? $(this).find('> ul').fadeOut() : $(this).find('> ul').slideUp(250);
	});
}

In line 1 we call the jQuery ready function, which is called when the page is completely loaded. In line 2 we select the element in the page which has the id = menu and calls the menu function we define below

Line 4 declares a boolean variable to indicate if the browser is an old version of IE. We will need to make some IE specific code later

Line 8 finds every li on the menu and calls the hover function. The hover function receive two arguments, the first is a function that will be executed when the mouse enters the element, the second is executed when the mouse leaves the element

In the first function we make the first children ul appear. We do this using the slideDown function which receives the an integer as the duration of the slide down effect. This code doesn’t work (in this specific case of this menu) on older versions of Internet Explorer so we will use the less cool fadeIn effect for it. We also add the class hover to the li

The second function does the same thing, except it makes the first children ul disappear by sliding up or fading out and removes the class hover

Now let’s add some CSS to change the background of the lis that have the class hover

1
#menu li.hover  { background-color: #cfdeff }

Our menu is working nicely now, but there is a lot of things to do to improve it. First let’s add an arrow indicating that a li has child items. Add this after line 7 of previous code

1
2
3
4
5
$(this).find('li').each(function() {
	if ($(this).find('> ul').size() > 0) {
		$(this).addClass('has_child');
	}
});

What it does is for each li in the menu, check if it has a child ul and if true, add the class has_child to the li

And now we add this css to style the lis that have childs

1
#menu li.has_child { background-image: url('down.gif'); background-position: right center; background-repeat: no-repeat; padding-right: 10px }

Now add this line to make the uls have a nice shadow. Unfortunately this doesn’t work in Internet Explorer

1
#menu ul { -webkit-box-shadow: 3px 3px 4px #999; -moz-box-shadow: 3px 3px 4px #999; box-shadow: 3px 3px 4px #999 }

Our menu is almost done. The only problem now is that as soon as the mouse leaves the menu it hides it. This is bad for usability. We need to have a timer that will count if the mouse has left the menu for 500 milliseconds, then hide it. To do that change this:

1
2
3
4
5
6
7
	$(this).find('li').hover(function() {
		$(this).addClass('hover');
		ie ? $(this).find('> ul').fadeIn() : $(this).find('> ul').slideDown(250);
	}, function() {
		$(this).removeClass('hover');
		ie ? $(this).find('> ul').fadeOut() : $(this).find('> ul').slideUp(250);
	});

to this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
	var closeTimer = null;
	var menuItem = null;
 
	function cancelTimer() {
		if (closeTimer) {
			window.clearTimeout(closeTimer);
			closeTimer = null;
		}
	}
 
	function close() {
		$(menuItem).find('> ul ul').hide();
		ie ? $(menuItem).find('> ul').fadeOut() : $(menuItem).find('> ul').slideUp(250);
		menuItem = null;
	}
 
	$(this).find('li').hover(function() {
		cancelTimer();
 
		var parent = false;
		$(this).parents('li').each(function() { 
			if (this == menuItem) parent = true;
		});
		if (menuItem != this && !parent) close();
 
		$(this).addClass('hover');
		ie ? $(this).find('> ul').fadeIn() : $(this).find('> ul').slideDown(250);
	}, function() {
		$(this).removeClass('hover');
		menuItem = this;
		cancelTimer();
		closeTimer = window.setTimeout(close, 500);
	});

That’s a lot of code! Instead of hiding the menu immediately, the second function of the hover sets a timeout that will execute the close function after 500 milliseconds. The first function of the hover is also different, it cancels the timer and checks if the exited menuItem is different from the current hovered item or a parent of it, if true it instantly closes the current exited item.

That’s it. The menu is finished. But wait, let’s look at it on Internet Explorer 6

To fix these issues add this code to the menu function

1
2
3
4
	if (ie) {
		$(this).find('ul a').css('display', 'inline-block');
		$(this).find('ul ul').css('top', '0');
	}

Unfortunately setting the a as display: inline-block makes them not occupy the whole area of the li. If anyone has a better solution feel free to post in the comments

Now our menu is finished. Take a look at a screenshot of it on a non retarded browser

Hope you liked this tutorial. If you have any questions post a comment and I will try to answer

Categories: CSS, jQuery Tags:

About me and this blog

November 29th, 2010 No comments

My name is Victor Cisneiros. I’m 24 years old programmer from Aracaju, Brazil. I have a degree in Computer Science from UFPE (Universidade Federal de Pernambuco). I love building User Interfaces, specially websites in HTML, CSS and Javascript (with jQuery!).

I created this blog so I can share tutorials on how to create various jQuery plugins, explaining their code line by line so that you can understand how they are made and can customize them to your needs.

Categories: Uncategorized Tags: