Martini Lab Blog

A better jQuery slider

March 25th, 2009 | Tags: , , ,

If you haven’t seen Apple’s product slider in action, you must check it out. It’s a horizontal gallery of their products with horizontal slider controlled with javascript. I believe they are using the scriptaculous motion for this, but there should be a way to build this in jQuery.

Truth be told, I was a die-hard fanatic for scriptaculous/prototype, but after ExpressionEngine announce their adoption of jQuery for EE 2.0, it was time to grow beyond the one framework pony.

Luckily, someone already set out to build the slider in jQuery. And major hat’s off to Remy Sharp for a great approach to this. If you looking for a way to build your own slider, start there first.

The Problem

Actually, there was no problem until I had a client to didn’t fit into the strict mold set out for this widget. As with most inventive solutions, they are begat from necessity.

The slider is built on an unordered list, but doesn’t support nested lists. For my needs, the slider had to be able to not only show multiple categories of products, but also visually differentiate.

Now, this can be resolved by manually setting css values. This approach would “hard code” the positions of the categories. That isn’t very code may be puled from a database.

Also, the slider needs to work without javascript. I know. Who doesn’t use javascript? The answer is, I don’t know, but someone always points out if something doesn’t degrade properly.

The Solution

I built this out backwards, but it works. So don’t question it. This demo shows the slider with wireframes to show off the block level div’s and li’s.

You can see by this code example that the lists are simple and even has a space for the category description if your site requires it.

HTML

This is a simple template shoing how to format your product groups and your categories for them. There can be as many needed. This example should be able to display them all successfully.

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
<div id="mlSlideWidget" class="slideWidget">
	<div id="sliderWindow" class="sliderGallery">
		<ul class="sliderList">
			<!-- Each category -->
			<li class="group">
				<div class="groupContent">
					<!-- Description info for category -->
					<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Vestibulum vitae diam vitae leo hendrerit aliquet. Donec dolor. Integer placerat rhoncus metus. Proin sagittis. Nullam sed lacus accumsan orci convallis interdum. Aenean aliquet, nisi et sollicitudin posuere, orci tortor fermentum.</p>
					<ul class="groupButtons">
						<!-- Each product for this category -->
						<li class="singleButton"><a href="#">Product 1</a></li>
						<li class="singleButton"><a href="#">Product 2</a></li>
						<li class="singleButton"><a href="#">Product 3</a></li>
					</ul>
				</div>
			</li>
			<li class="group">
				<div class="groupContent">
					<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Vestibulum vitae diam vitae leo hendrerit aliquet. Donec dolor. Integer placerat rhoncus metus. Proin sagittis. Nullam sed lacus accumsan orci convallis interdum. Aenean aliquet, nisi et sollicitudin posuere, orci tortor fermentum.</p>
					<ul class="groupButtons">
						<li class="singleButton"><a href="#">Product 4</a></li>
						<li class="singleButton"><a href="#">Product 5</a></li>
						<li class="singleButton"><a href="#">Product 6</a></li>
					</ul>
				</div>
			</li>
			<li class="group">
				<div class="groupContent">
					<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Vestibulum vitae diam vitae leo hendrerit aliquet. Donec dolor. Integer placerat rhoncus metus. Proin sagittis. Nullam sed lacus accumsan orci convallis interdum. Aenean aliquet, nisi et sollicitudin posuere, orci tortor fermentum.</p>
					<ul class="groupButtons">
						<li class="singleButton"><a href="#">Product 7</a></li>
						<li class="singleButton"><a href="#">Product 8</a></li>
						<li class="singleButton"><a href="#">Product 9</a></li>
					</ul>
				</div>
			</li>
			<li class="group">
				<div class="groupContent">
					<p>Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Vestibulum vitae diam vitae leo hendrerit aliquet. Donec dolor. Integer placerat rhoncus metus. Proin sagittis. Nullam sed lacus accumsan orci convallis interdum. Aenean aliquet, nisi et sollicitudin posuere, orci tortor fermentum.</p>
					<ul class="groupButtons">
						<li class="singleButton"><a href="#">Product 10</a></li>
						<li class="singleButton"><a href="#">Product 11</a></li>
						<li class="singleButton"><a href="#">Product 12</a></li>
					</ul>
				</div>
			</li>
		</ul>
	</div>
 
	<div id="scroll" class="slider">
		<!-- the handler to action the slide -->
		<div class="handle"></div>
		<!-- labels appear against the slider, as pointers to the user -->
		<ul>
			<!-- These are categories -->
			<li class="marker">Category 1</li>
			<li class="marker">Category 2</li>
			<li class="marker">Category 3</li>
			<li class="marker">Category 4</li>
		</ul>
		<div class="fakeHandle">
			<table width="100%" cellpadding="0" cellspacing="0" border="0">
				<tr>
					<td width="15"><img src="images/handle-left.png" width="15" height="115" alt="&lt;" /></td>
					<td background="images/handle-middle.png">&nbsp;</td>
					<td width="15"><img src="images/handle-right.png" width="15" height="115" alt="&gt;" /></td>
				</tr>
			</table>
		</div>
	</div>
 
 
</div>

CSS

I threw in a dash of YUIs css reset. Most of the id and class names are generic. I would recommend renaming these to something more specific to avoid conflict.

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
115
116
117
118
119
120
121
122
123
124
125
body, div, dl, dt, dd, ul, ol, li, h1, h2, h3, h4, h5, h6, pre, code, form, fieldset, legend, input, textarea, p, blockquote, th, td, img {border-width: 0px;margin:0;padding:0;}
#mlSlideWidget {
	font:11px arial,helvetica,clean,sans-serif;
	*font-size:small;
	*font:x-small;
	position: relative;
	height: 361px;
	width: 659px;
	background: url(images/slider-background.jpg) left top no-repeat;
}
 
#sliderWindow {
	margin: 1px;
	height: 365px;
	position: relative;
	width: 655px;
}
 
.sliderGallery {
	overflow: auto;
}
 
.sliderGallery ul.sliderList {
	top: 0px;
	left: 0px;
	margin-top: 135px;
	padding-bottom: 0;
	position: absolute;
	list-style: none;
	height: 210px;
	overflow: auto;
	width: 2000px;
	white-space: nowrap;
}
 
#mlSlideWidget .sliderGallery ul li.group {
	margin-left: 7px;
	margin-right: 7px;
	display: inline;
	float: left;
}
 
#mlSlideWidget .sliderGallery ul li.group .groupContent {
	margin-right: 10px;
	padding-left: 10px;
	padding-top: 10px;
	padding-bottom: 10px;
}
 
ul.groupButtons {
	height: 120px;
}
 
li.group {
	background: url(images/group-backgroundcap.png) no-repeat right top;
}
 
li.group .groupContent{
	background: url(images/group-background.png) no-repeat left top;
}
 
li.group p {
	width: 430px;
	height: 70px;
	font-size: 10px;
	line-height: 12px;
	white-space: normal;
}
 
li.group ul.groupButtons {
	list-style: none;
	white-space: nowrap;
}
 
li.group ul.groupButtons li.singleButton {
	width: 140px;
	float: left;
	height: 118px;
	text-align: center;
}
#scroll {
	position: absolute;
	top: 0px;
	left: 0px;
	z-index: 100;
	width: 639px;
	margin-left: 10px;
	margin-right: 8px;
}
 
#scroll ul {
	height: 123px;
	list-style: none;
}
 
#scroll li.marker {
	margin-top: 4px;
	padding-top: 60px;
	text-align: center;
	z-index: 90;
	font-size: 10px;
	height: 30px;
	width: 55px;
	padding-left: 20px;
	padding-right: 20px;
	margin-left: 10px;
	margin-right: 10px;
	float: left;
}
 
.handle {
	position: absolute;
	top: 8px;
	left: 0;
	z-index: 110;
	height: 115px;
}
 
.fakeHandle {
	position: absolute;
	top: 8px;
	left: 0;
	z-index: 80;
	height: 115px;
}

jQuery UI will need to be built with UI Core, Draggable, Slider, Effects Core, and Slide. Some of the values of this rules in the example are built to my specs and can be modified to whatever specs fit your site. Note the versions of jQuery at the time of this article.

This example doesn’t know how many objects exist in the slider. If you use a set amount, this example can be greatly reduced. However, if your categories are managed via a database or cms tool, the number of objects used can change.

Javascript

Try not to laugh at my n00b code. I’m sure there are much better ways to write 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
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
<script type="text/javascript" src="js/jquery-1.2.6.min.js"></script>
<script type="text/javascript" src="js/jquery-ui-1.6rc2.custom.min.js"></script>
 
<script>
//basic rules for controling the divs
$(document).ready(function(){
 
   //fix css rules
   //above css written for pages loaded without javascript
   $('.sliderGallery').css({'overflow-x': 'hidden', 'overflow-y': 'visible'});
   $('li.marker').css({'float': 'none', 'position': 'absolute', 'top': 0, 'left': 0});
 
	//set up some public variables and arrays
	var scrollBar = 639;
	var sliderListOffsetLeft = $('.sliderList').offset();
	var productWidth = 0;
	var groupWidth = 0;
	var sliderWidth = new Array();
	var sliderLeft = new Array();
	var groupRangeLeft = new Array();
	var marker = new Array();
 
	//measure the width of all the sub-categories
	//and the sub-sub-categories while we're at it
	$('li.group').each(function (i) {
		sliderLeft[i] = productWidth;
		$('li.singleButton', this).each(function (i) {
			groupWidth = groupWidth + $(this).width()
		});
		//hard coded minimum width for groups
		if (groupWidth < 450)
			groupWidth = 450;
		else
			groupWidth += 20;
		productWidth = productWidth + groupWidth + 16;
		//set width for each sub-caterogy
		sliderWidth[i] = groupWidth + 14;
		$(this).css({width: groupWidth});
		//reset the variable
		groupWidth = 0;
		//used in another function not used in demo
		groupOffsetleft = $(this).offset();
		groupRangeLeft[i] = groupOffsetleft.left - sliderListOffsetLeft.left - ((i > 0) ? 14 : 7 );
	});
 
	//Fix css to fit with slider content
	$('ul.sliderList').css({width: productWidth + 14});
 
	//Variables to make math easier
	var handle = (scrollBar / productWidth) * scrollBar;
	var markerLeft = 0;
	var scrollBarVisible = scrollBar - handle;
 
	$('li.marker').each(function (i) {
		markerLeft = (sliderLeft[i] / productWidth * scrollBar);
		markerWidth = (sliderWidth[i] / productWidth * scrollBar);
		$(this).css({left: markerLeft});
		marker[i] = (markerLeft / scrollBarVisible) * 100;
	});
 
 
	$('div.handle').css({width: handle});
	$('div.fakeHandle').css({width: handle});
 
	if (productWidth > scrollBar)
	{
		$('div.slideWidget').each(function () {
			var ul = $('ul.sliderList', this);
			var fake = $('.fakeHandle', this);
 
			var slider = $('.slider', this).slider({
				handle: '.handle',
				minValue: 0, 
				maxValue: productWidth, 
				slide: function (ev, ui) {
					ul.css('left', '-' + Math.round(ui.value / 100 * (productWidth - 657)) + 'px');
					fake.css('left', Math.round(ui.value / 100 * (scrollBar - handle)) + 'px');
				},
				stop: function (ev, ui) {
					move = ui.value / 100 * (productWidth - 657);
					ul.animate({ 'left' : '-' + Math.round(move) + 'px' }, 500);
					fake.animate({'left' : Math.round(ui.value / 100 * (scrollBar - handle)) + 'px'}, 500);
				}
			});
		})
	}
 
	else {
		$('.fakeHandle').css({width: 0});
		$('.handle').css({width: 0});
		$('.fakeHandle').css('visibility', 'hidden');
		$('.handle').css('visibility', 'hidden');
	}
 
});
</script>

Notice the versions of jQuery and UI. 1.7.1 has issues with this solution. As of yet, I haven’t found a fix.

Wrap it all together

Here is the slider in action.
example

Not all lists are the same. Here is the same page with an expanded and uneven list. In this example, one group has more products and another has fewer. The total list is longer so the scroll bar handle will be shorter.
example 2

Finally, and sadly, here is it with Jquery 1.3.2 / UI 1.7.1. The handle interaction is wonky across the board. Also, I had to subtract the length of the slider so the handle wouldn’t go careening off the widget.
example fail

  1. Harley
    April 4th, 2009 at 07:00
    Reply | Quote | #1

    that is pretty badass.