As another example of shuffling around page content, we'll implement an image gallery
for the front page of the bookstore site. The gallery will present a
few featured books for sale, with links to larger cover art for each.
Unlike the previous example, where the headlines in our news ticker
moved on a set schedule, here we'll use jQuery to slide the images
across the screen in response to user interaction.
An alternative mechanism for scrolling through a set of images is implemented by the jCarousel plugin for jQuery. Additionally, the highly flexible SerialScroll
plugin allows for scrolling any type of content. While not identical to
the result we'll achieve here, these plugins can produce high-quality
shuffling effects with very little code. More information on using
plugins can be found in Chapter 10.
Setting up the page
As always, we begin by
crafting the HTML and CSS so that users without JavaScript available
receive an appealing and functional representation of the information:
<div id="featured-books">
<div class="covers">
<a href="images/covers/large/1847190871.jpg"
title="Community Server Quickly">
<img src="images/covers/medium/1847190871.jpg"
width="120" height="148"
alt="Community Server Quickly" />
<span class="price">$35.99</span>
</a>
<a href="images/covers/large/1847190901.jpg"
title="Deep Inside osCommerce: The Cookbook">
<img src="images/covers/medium/1847190901.jpg"
width="120" height="148"
alt="Deep Inside osCommerce: The Cookbook" />
<span class="price">$44.99</span>
</a>
<a href="images/covers/large/1847190979.jpg"
title="Learn OpenOffice.org Spreadsheet Macro
Programming: OOoBasic and Calc automation">
<img src="images/covers/medium/1847190979.jpg"
width="120" height="148"
alt="Learn OpenOffice.org Spreadsheet Macro
Programming: OOoBasic and Calc automation" />
<span class="price">$35.99</span>
</a>
<a href="images/covers/large/1847190987.jpg"
title="Microsoft AJAX C# Essentials: Building
Responsive ASP.NET 2.0 Applications">
<img src="images/covers/medium/1847190987.jpg"
width="120" height="148"
alt="Microsoft AJAX C# Essentials: Building
Responsive ASP.NET 2.0 Applications" />
<span class="price">$31.99</span>
</a>
<a href="images/covers/large/1847191002.jpg"
title="Google Web Toolkit GWT Java AJAX Programming">
<img src="images/covers/medium/1847191002.jpg"
width="120" height="148"
alt="Google Web Toolkit GWT Java AJAX Programming" />
<span class="price">$40.49</span>
</a>
<a href="images/covers/large/1847192386.jpg"
title="Building Websites with Joomla! 1.5 Beta 1">
<img src="images/covers/medium/1847192386.jpg"
width="120" height="148"
alt="Building Websites with Joomla! 1.5 Beta 1" />
<span class="price">$40.49</span>
</a>
</div>
</div>
Each image is contained
within an anchor tag, pointing to the larger version of the cover. We
also have prices given for each cover; these will be hidden for now,
and we'll use JavaScript to display them later at an appropriate time.
To save space on the front
page, we want to show only three covers at a time. Without JavaScript,
we can accomplish this by setting the overflow property of the container to scroll, and adjusting the width appropriately:
#featured-books {
position: relative;
background: #ddd;
width: 440px;
height: 186px;
overflow: scroll;
margin: 1em auto;
padding: 0;
text-align: center;
z-index: 2;
}
#featured-books .covers {
position: relative;
width: 840px;
z-index: 1;
}
#featured-books a {
float: left;
margin: 10px;
height: 146px;
}
#featured-books .price {
display: none;
}
These styles bear a bit of discussion. The outermost element needs to have a larger z-index
property than the one inside it; this allows Internet Explorer to hide
the part of the inner element that stretches beyond its container. We
set the width of the outer element to 440px, which accommodates three images, the 10px margin around each, and an extra 20px for the scroll bar.
With these styles in place, the images can be browsed using a standard system scroll bar:
Revising the styles with JavaScript
Now that we have gone to the
work of making the image gallery usable without JavaScript, we need to
undo some of the niceties. The scroll bar will be redundant when we
implement our own scrolling mechanism, and the automatic layout of the
covers using the float property will get
in the way of the positioning we need to do to animate the covers. So
our first order of business will be overriding some styles:
$(document).ready(function() {
var spacing = 140;
$('#featured-books').css({
'width': spacing * 3,
'height': '166px',
'overflow': 'hidden'
}).find('.covers a').css({
'float': 'none',
'position': 'absolute',
'left': 1000
});
var $covers = $('#featured-books .covers a');
$covers.eq(0).css('left', 0);
$covers.eq(1).css('left', spacing);
$covers.eq(2).css('left', spacing * 2);
});
The spacing variable
is going to come in handy throughout many of our calculations. It
represents the width of one of the cover images, plus the padding on
either side of it. The width of the
containing element can now be set to exactly what is necessary to
contain three of the cover images, since we don't need space for the
scroll bar anymore. Indeed, we change the overflow property to hidden, and bye-bye scroll bar.
The cover images all get positioned absolutely, and start with a left coordinate of 1000. This places them out of the visible area. Then we move the first three covers into position, one at a time. The $covers variable holding all of the anchor elements will also come in handy later.
Now the first three covers are visible, with no scrolling mechanism available:
Shuffling images when clicked
Now, we need to add code to
respond to a click on either of the end images, and reorder the covers
as necessary. When the left cover is clicked, this means the user wants
to see more images to the left, which in turn means we need to shift
the covers to the right. Similarly, when the right cover is clicked we will have to shift the covers to the left. We want the carousel to wrap around,
so when images fall off the left side, they get appended to the right.
To begin, we will just change the image positions without animation.
$(document).ready(function() {
images, image carouselwrap aroundvar spacing = 140;
$('#featured-books').css({
'width': spacing * 3,
'height': '166px',
'overflow': 'hidden'
}).find('.covers a').css({
'float': 'none',
'position': 'absolute',
'left': 1000
});
var setUpCovers = function() {
var $covers = $('#featured-books .covers a');
$covers.unbind('click');
// Left image; scroll right (to view images on left).
$covers.eq(0)
.css('left', 0)
.click(function(event) {
$covers.eq(2).css('left', 1000);
$covers.eq($covers.length - 1)
.prependTo('#featured-books .covers');
setUpCovers();
event.preventDefault();
});
// Right image; scroll left (to view images on right).
$covers.eq(2)
.css('left', spacing * 2)
.click(function(event) {
$covers.eq(0).css('left', 1000);
$covers.eq(0)
.appendTo('#featured-books .covers');
setUpCovers();
event.preventDefault();
});
// Center image.
$covers.eq(1)
.css('left', spacing);
};
setUpCovers();
});
The new setUpCovers()
function incorporates the image positioning code that we wrote earlier.
By encapsulating this in a function, we can repeat the image
positioning after the elements have been reordered; this will be
important, as we shall soon see.
In our example, there are six images in total (which JavaScript will
reference with the numbers 0 through 5), and numbers 0, 1, and 2 are
visible. When image #0 is clicked, we want to shift all the images to
the right by one position. We first move image #2 out of the viewable
area (with .css('left', 1000)),
since we don't want it to be visible after the shift. Then, we move the
image at the end of the line (#5) to the front of the queue (using .prependTo()). This reorders all of the images so when setUpCovers()
is called again, the former #5 is now #0, #0 has become #1, and #1 has
become #2. The existing positioning code in this function is therefore
sufficient to move the covers to their new locations.
Clicking on image #2
performs the process in reverse. This time, it is #0 that gets hidden
from view, and then moved to the end of the queue. This shifts #1 to
the #0 spot, #2 to #1, and #3 to #2.
There are a couple of details that we have to take care of to avoid user interaction anomalies:
1. We need to call .preventDefault() within our click
handler, since we have made the covers into links to the large version.
Without this call, the link will be followed and we would never see our
shuffle effect.
2. We need to unbind all of the click handlers at the beginning of the setUpCovers() function, or we could end up with multiple handlers bound to the same image as the carousel rotates.
Adding sliding animation
It can be difficult to
understand what just happened when an image is clicked; since the
covers move instantaneously, they can appear to have just changed
rather than moved. To mitigate this issue, we can add an animation that
causes the covers to slide into place rather than just appearing in
their new positions. This requires a revision of the setUpCovers() function:
var setUpCovers = function() {
images, image carouselsliding animation, addingvar $covers = $('#featured-books .covers a');
$covers.unbind('click');
// Left image; scroll right (to view images on left).
$covers.eq(0)
.css('left', 0)
.click(function(event) {
$covers.eq(0).animate({'left': spacing}, 'fast');
$covers.eq(1).animate({'left': spacing * 2}, 'fast');
$covers.eq(2).animate({'left': spacing * 3}, 'fast');
$covers.eq($covers.length - 1)
.css('left', -spacing)
.animate({'left': 0}, 'fast', function() {
$(this).prependTo('#featured-books .covers');
setUpCovers();
});
event.preventDefault();
});
// Right image; scroll left (to view images on right).
$covers.eq(2)
.css('left', spacing * 2)
.click(function(event) {
$covers.eq(0)
.animate({'left': -spacing}, 'fast', function() {
$(this).appendTo('#featured-books .covers');
setUpCovers();
});
$covers.eq(1).animate({'left': 0}, 'fast');
$covers.eq(2).animate({'left': spacing}, 'fast');
$covers.eq(3)
.css('left', spacing * 3)
.animate({'left': spacing * 2}, 'fast');
event.preventDefault();
});
// Center image.
$covers.eq(1)
.css('left', spacing);
};
When the left image is clicked, we can move all three visible images to the right by one image width (reusing the spacing
variable we defined earlier). This part is straightforward, but we also
have to make the new image slide into view. To do this, we grab the
image from the end of the queue, and first set its screen position to
be just off-screen on the left side (-spacing). Then, we slide it into view along with the other items.
Even though the animation takes care of the initial move, we still need to change the cover order by calling setUpCovers() again. If we don't, the next click won't work correctly. Since setUpCovers()
changes the cover positions, we must defer the call until after the
animation completes, so we place the call in the animation's callback.
A click on the rightmost
image performs a similar set of animations, but in reverse. This time,
it's the leftmost image that moves out of view, and must be moved to
the end of the queue before we trigger setUpCovers() when the animation is complete. The new, rightmost image, on the other hand, must be moved into position (spacing * 3) before its animation can begin.
Displaying action icons
Our image carousel now
rotates smoothly, but we haven't provided any hint to the user that
clicking on the covers will cause them to scroll. We can assist the
user by displaying appropriate icons when the mouse hovers over the
images.
In this case, we'll place the icons on top of the existing images. By using the opacity
property, we can continue to see the cover underneath when the icon is
displayed. We'll use simple monochrome icons so that the cover is not
too obscured:
We'll need three icons, one
each for the left and right covers, which the user will choose to
scroll, and one for the middle cover, which the user can click for an
enlarged version. We can create HTML elements that reference the icons
and store them in variables for later use:
var $leftRollover = $('<img/>')
images, image carouselaction icons, displaying.attr('src', 'images/left.gif')
.addClass('control')
.css('opacity', 0.6)
.css('display', 'none');
var $rightRollover = $('<img/>')
.attr('src', 'images/right.gif')
.addClass('control')
.css('opacity', 0.6)
.css('display', 'none');
var $enlargeRollover = $('<img/>')
.attr('src', 'images/enlarge.gif')
.addClass('control')
.css('opacity', 0.6)
.css('display', 'none');
You may notice that we've
got a fair amount of repetition here. To minimize this extra code, we
can pull this work out into a function that we call for each icon that
needs to be created:
function createControl(src) {
return $('<img/>')
.attr('src', src)
.addClass('control')
.css('opacity', 0.6)
.css('display', 'none');
}
var $leftRollover = createControl('images/left.gif');
var $rightRollover = createControl('images/right.gif');
var $enlargeRollover = createControl('images/enlarge.gif');
In the CSS for the page, we set the z-index of these controls to be higher than the images', and then position them absolutely so that they can overlap the covers:
#featured-books .control {
position: absolute;
z-index: 3;
left: 0;
top: 0;
}
The rollover icons all share the same control class, so one might be tempted to place the opacity style in the CSS stylesheet. However, element opacity is not handled consistently between browsers; in Internet Explorer, the syntax for 60% opacity is filter: alpha(opacity=60). Rather than wrestle with these distinctions, we set the opacity style using jQuery's .css() method, which abstracts away these browser inconsistencies.
Now, all we have to do in our hover handlers is to place the images in the right DOM location and show them.
var setUpCovers = function() {
var $covers = $('#featured-books .covers a');
$covers.unbind('click mouseenter mouseleave');
// Left image; scroll right (to view images on left).
$covers.eq(0)
.css('left', 0)
.click(function(event) {
$covers.eq(0).animate({'left': spacing}, 'fast');
$covers.eq(1).animate({'left': spacing * 2}, 'fast');
$covers.eq(2).animate({'left': spacing * 3}, 'fast');
$covers.eq($covers.length - 1)
.css('left', -spacing)
.animate({'left': 0}, 'fast', function() {
$(this).prependTo('#featured-books .covers');
setUpCovers();
});
event.preventDefault();
}).hover(function() {
$leftRollover.appendTo(this).show();
}, function() {
$leftRollover.hide();
});
// Right image; scroll left (to view images on right).
$covers.eq(2)
.css('left', spacing * 2)
.click(function(event) {
$covers.eq(0)
.animate({'left': -spacing}, 'fast', function() {
$(this).appendTo('#featured-books .covers');
setUpCovers();
});
$covers.eq(1).animate({'left': 0}, 'fast');
$covers.eq(2).animate({'left': spacing}, 'fast');
$covers.eq(3)
.css('left', spacing * 3)
.animate({'left': spacing * 2}, 'fast');
event.preventDefault();
}).hover(function() {
$rightRollover.appendTo(this).show();
}, function() {
$rightRollover.hide();
});
// Center image.
$covers.eq(1)
.css('left', spacing)
.hover(function() {
$enlargeRollover.appendTo(this).show();
}, function() {
$enlargeRollover.hide();
});
};
Just as we did earlier with click, we unbind mouseenter and mouseleave handlers at the beginning of setUpCovers() so that the hover behaviors do not accumulate. Here, we use another feature of the .unbind() method: handlers for multiple event types can be unbound at once by separating the event type names with spaces.
Why mouseenter and mouseleave? When we call the .hover()
method, internally jQuery translates this into two separate event
bindings. The first function we supply is bound as a handler for the mouseenter event, and the second is bound to mouseleave. So, to remove the handlers bound using .hover(), we need to unbind and mouseleave. mouseenter
Now when the mouse cursor is over a cover, the appropriate rollover image is overlaid on top of the cover:
Image enlargement
Now, our image gallery is
fully functional, with a carousel that allows the user to navigate to a
desired image. A click on the center image leads to an enlarged view of
the cover in question. But, there is more we can do with this image
enlargement functionality.
Rather than lead the user
to a separate URL when the center image is clicked, we can overlay the
enlarged book cover on the page itself.
A number of
variations on the theme of displaying information overlaid on the page
are available as jQuery plugins. A few of the more popular ones include FancyBox, ShadowBox, Thickbox, SimpleModal, and jqModal. More information on using plugins can be found in Chapter 10.
This larger cover image will
require a new image element, which we can create at the same time that
the hover images are instantiated:
var $enlargedCover = $('<img/>')
.addClass('enlarged')
.hide()
.appendTo('body');
We will apply a set of style rules to this new class that are similar to the ones we have seen before:
img.enlarged {
position: absolute;
z-index: 5;
cursor: pointer;
}
This absolute positioning will allow the cover to float above the other images we have positioned, because the z-index
is higher than the ones we have already used. Now we need to actually
position the enlarged image when the center image in the carousel is
clicked:
// Center image; enlarge cover.
$covers.eq(1)
.css('left', spacing)
.click(function(event) {
$enlargedCover.attr('src', $(this).attr('href'))
.css({
'left': ($('body').width() - 360) / 2,
'top' : 100,
'width': 360,
'height': 444
}).show();
event.preventDefault();
})
.hover(function() {
$enlargeRollover.appendTo(this).show();
}, function() {
$enlargeRollover.hide();
});
We can take advantage of the
links already present in the HTML source to know where the larger
cover's image file resides on the server. We pluck this from the href src attribute of the enlarged cover image. attribute of the link, and set it as the
Now, we must position the image. The top, width, and height are hard-coded for now, but the left
requires a little calculation. We want the enlarged image to be
centered on the page, but we can't know in advance what the appropriate
coordinate is to achieve this positioning. We can find the halfway mark
across the page by measuring the width of the<body>
element and dividing this by two. Half of our enlarged image will be on
either side of this point, so the left coordinate of the image will be ($('body').width() - 360) / 2, since 360 is the width of the enlarged cover. The cover is now positioned appropriately, centered horizontally across the page:
Hiding the enlarged cover
We need a mechanism for dismissing the cover once it has been enlarged. The simplest way to do this is by making a click event on the cover fade it out:
// Center image; enlarge cover.
image enlargement, image carouselenlarged cover, dismissing$covers.eq(1)
.css('left', spacing)
.click(function(event) {
$enlargedCover.attr('src', $(this).attr('href'))
.css({
'left': ($('body').width() - 360) / 2,
'top' : 100,
'width': 360,
'height': 444
})
.show()
.one('click', function() {
$enlargedCover.fadeOut();
});
event.preventDefault();
})
.hover(function() {
$enlargeRollover.appendTo(this).show();
}, function() {
$enlargeRollover.hide();
});
We use the .one() method to bind this click handler, which sidesteps a couple of potential problems. With a regular .bind()
of the handler, the user could click on the image again as it was
fading out. This would cause the handler to fire again. Also, since we
are reusing the same image element every time the cover is enlarged,
the binding will occur again for each enlargement. If we do nothing to
unbind the handler, they will stack up over time. Using .one() ensures that the handlers are removed once used.
Displaying a close button
This behavior is
sufficient for removing the large cover, but we've given no indication
to the user that clicking the cover will make it go away. We can
provide this assistance by badging the enlarged image with a Close button. Creating the button is similar to defining the other singleton elements
we've used—the items that are guaranteed to appear only
once—and we can call the utility function that we created
earlier:
var $closeButton = createControl('images/close.gif')
image enlargement, image carouselenlarged image, badging.addClass('enlarged-control')
.appendTo('body');
When the center cover is clicked, and the enlarged cover is displayed, we need to position and show the button:
$closeButton.css({
'left': ($('body').width() - 360) / 2,
'top' : 100
}).show();
The coordinates of the Close button are identical to the enlarged cover, so their top-left corners are aligned:
We already have a behavior
bound to the image that hides it when the image is clicked. Typically
in this situation we could rely on event bubbling to cause a click on the Close button to cause the same effect. In this case, however, the Close button is not a descendant element of the cover, despite appearances. We've absolutely positioned the Close
button on top of the cover, which means that clicks on the button do
not get passed to the enlarged image. Instead, we must handle clicks on
the Close button ourselves:
// Center image; enlarge cover.
image enlargement, image carouselclose button used$covers.eq(1)
.css('left', spacing)
.click(function(event) {
$enlargedCover.attr('src', $(this).attr('href'))
.css({
'left': ($('body').width() - 360) / 2,
'top' : 100,
'width': 360,
'height': 444
})
.show()
.one('click', function() {
$closeButton.unbind('click').hide();
$enlargedCover.fadeOut();
});
$closeButton
.css({
'left': ($('body').width() - 360) / 2,
'top' : 100
})
.show()
.click(function() {
$enlargedCover.click();
});
event.preventDefault();
})
.hover(function() {
$enlargeRollover.appendTo(this).show();
}, function() {
$enlargeRollover.hide();
});
When we show the Close button, we bind a click event handler for it. All this handler needs to do, though, is to trigger the click handler we've already bound to the enlarged cover. We do need to modify that handler, though, and hide the Close button there. While we're at it, we unbind the click handler to prevent handlers from accumulating over time.
More fun with badging
Since we have the prices for
the books available to us in the HTML source, we can display this as
additional information when the book cover is enlarged. This time we'll
apply the technique we just developed for the Close button to textual content rather than an image.
Once again, we create a singleton element at the beginning of our JavaScript code:
var $priceBadge = $('<div/>')
image enlargement, image carouselsingleton element, creating.addClass('enlarged-price')
.css('opacity', 0.6)
.css('display', 'none')
.appendTo('body');
Since the price will be partially transparent, a high contrast between font color and background will work best:
.enlarged-price {
background-color: #373c40;
color: #fff;
width: 80px;
padding: 5px;
font-size: 18px;
font-weight: bold;
text-align: right;
position: absolute;
z-index: 6;
}
Before we can display the
price badge, we need to populate it with the actual price information
from the HTML. Inside the center cover's click handler, the keyword refers to the link element. Since the price is in a<span> element within the link, obtaining the text is straightforward: this
var price = $(this).find('.price').text();
Now we can display the badge when the cover is enlarged:
$priceBadge.css({
'right': ($('body').width() - 360) / 2,
'top' : 100
}).text(price).show();
This will fix the price at the top-right corner of the enlarged image:
Once we place a $priceBadge.hide(); within the cover's click handler to clean up after ourselves, we're done.
Animating the cover enlargement
When the user
clicks on the center cover, the enlarged version currently appears in
the center of the page with no flair. To improve on this, we can use
the built-in animation capabilities of jQuery to smoothly transition
between the thumbnail view of the cover and the full-size version.
To do this, we need to
know the starting coordinates of the animation; i.e. the position of
the center cover on the page. Calculating this position requires some
clever DOM traversal using plain JavaScript, but jQuery gives us a
shortcut. The .offset() method returns an object containing the left and top coordinates of
an element relative to the page. We can then insert the width and height of the image into this object, and have the position information contained in a tidy package.
var startPos = $(this).offset();
startPos.width = $(this).width();
startPos.height = $(this).height();
Our destination coordinates can now be calculated from these quite easily. We'll collect them in a similar object.
var endPos = {};
endPos.width = startPos.width * 3;
endPos.height = startPos.height * 3;
endPos.top = 100;
endPos.left = ($('body').width() - endPos.width) / 2;
We can now use these two objects as maps of CSS attributes, which can be passed to methods such as .css() and .animate().
$enlargedCover.attr('src', $(this).attr('href'))
.css(startPos)
.show()
.animate(endPos, 'normal', function() {
$enlargedCover
.one('click', function() {
$closeButton.unbind('click').hide();
$priceBadge.hide();
$enlargedCover.fadeOut();
});
$closeButton
.css({
'left': endPos.left,
'top' : endPos.top
})
.show()
.click(function() {
$enlargedCover.click();
});
$priceBadge
.css({
'right': endPos.left,
'top' : endPos.top
})
.text(price)
.show();
});
Note that the Close button and price badge can't be placed until the animation completes, so we have moved their code into the callback of the .animate() method. Also, we've taken this opportunity to simplify the .css() calls for both of these elements by reusing the positioning information we calculated for the enlarged cover.
Now we have a smooth transition from small to large cover:
Deferring animations until image loads
Our animation is smooth,
but depends on a fast connection to the site. If the enlarged cover
takes some time to download, then the first moments of the animation
might display the red X
indicating a broken image, or still display the previous image. We can
make the transition a bit more elegant by waiting until the image has
fully loaded before starting the animation:
$enlargedCover.attr('src', $(this).attr('href'))
.css(startPos)
.show();
var performAnimation = function() {
$enlargedCover.animate(endPos, 'normal', function() {
$enlargedCover.one('click', function() {
$closeButton.unbind('click').hide();
$priceBadge.hide();
$enlargedCover.fadeOut();
});
$closeButton
.css({
'left': endPos.left,
'top' : endPos.top
})
.show()
.click(function() {
$enlargedCover.click();
});
$priceBadge
.css({
'right': endPos.left,
'top' : endPos.top
})
.text(price)
.show();
});
};
if ($enlargedCover[0].complete) {
performAnimation();
}
else {
$enlargedCover.bind('load', performAnimation);
}
There are two cases we have
to consider: either the image is available nearly instantly (perhaps
due to caching), or it needs time to load. In the first situation, the
image's complete attribute will be true, so we can call our new performAnimation() function immediately. In the second case, we need to wait for the image load to complete before we call performAnimation(). This is a rare instance in which the standard DOM load event is more useful to us than jQuery's custom ready event. Since load
is triggered on a window, image, or frame when all of its contents have
fully loaded, we can observe the event to make sure that the image is
being properly displayed. Only then is the handler executed, and the
animation is performed.
We're using the .bind('load') syntax rather than the shorthand .load() method here for clarity since .load() is also an AJAX method; the two syntaxes are interchangeable.
Internet Explorer and
Firefox have different interpretations of what to do if the image is
already in the browser cache. In this case, Firefox will immediately
send the event to JavaScript, but Internet Explorer will never send the event because no load actually occurred. Our testing of the complete attribute compensates for this variance in implementations. load
Adding a loading indicator
But now, we can have an
awkward situation on slow network connections when an image takes a few
moments to load. Our page appears to do nothing while this download is
in progress. As we did when loading the news headlines, we should
provide an indication to the user that some activity is occurring by
displaying a loading indicator in the meantime.
The indicator will be another singleton image that will be displayed when appropriate:
var $waitThrobber = $('<img/>')
.attr('src', 'images/wait.gif')
.addClass('control')
.css('z-index', 4)
.hide();
For this image, we're
actually using an animated GIF, because the motion will reinforce to
the user that the activity is taking place:
It will just take two lines
to put our loading indicator in place, now that we have the element
defined. At the very beginning of our click handler for the center image, before we start doing any work, we need to display the indicator:
$waitThrobber.appendTo(this).show();
And at the beginning of the performAnimation() function, when we know the image has been loaded, we remove the indicator from view:
This is all it takes to
badge the cover being enlarged with the loading indicator. The
animation appears overlaying the top left corner of the cover:
The finished code
This chapter represents just a
small fraction of what can be done on the Web with animated image and
text rotators. Taken all together, the code for the image carousel
looks like this:
$(document).ready(function() {
image carouselcodevar spacing = 140;
function createControl(src) {
return $('<img/>')
.attr('src', src)
.addClass('control')
.css('opacity', 0.6)
.css('display', 'none');
}
var $leftRollover = createControl('images/left.gif');
var $rightRollover = createControl('images/right.gif');
var $enlargeRollover = createControl('images/enlarge.gif');
var $enlargedCover = $('<img/>')
.addClass('enlarged')
.hide()
.appendTo('body');
var $closeButton = createControl('images/close.gif')
.addClass('enlarged-control')
.appendTo('body');
var $priceBadge = $('<div/>')
.addClass('enlarged-price')
.css('opacity', 0.6)
.css('display', 'none')
.appendTo('body');
var $waitThrobber = $('<img/>')
.attr('src', 'images/wait.gif')
.addClass('control')
.css('z-index', 4)
.hide();
$('#featured-books').css({
'width': spacing * 3,
'height': '166px',
'overflow': 'hidden'
}).find('.covers a').css({
'float': 'none',
'position': 'absolute',
'left': 1000
});
var setUpCovers = function() {
var $covers = $('#featured-books .covers a');
$covers.unbind('click mouseenter mouseleave');
// Left image; scroll right (to view images on left).
$covers.eq(0)
.css('left', 0)
.click(function(event) {
$covers.eq(0).animate({'left': spacing}, 'fast');
$covers.eq(1).animate({'left': spacing * 2}, 'fast');
$covers.eq(2).animate({'left': spacing * 3}, 'fast');
$covers.eq($covers.length - 1)
.css('left', -spacing)
.animate({'left': 0}, 'fast', function() {
$(this).prependTo('#featured-books .covers');
setUpCovers();
});
event.preventDefault();
}).hover(function() {
$leftRollover.appendTo(this).show();
}, function() {
$leftRollover.hide();
});
// Right image; scroll left (to view images on right).
image carouselcode$covers.eq(2)
.css('left', spacing * 2)
.click(function(event) {
$covers.eq(0)
.animate({'left': -spacing}, 'fast', function() {
$(this).appendTo('#featured-books .covers');
setUpCovers();
});
$covers.eq(1).animate({'left': 0}, 'fast');
$covers.eq(2).animate({'left': spacing}, 'fast');
$covers.eq(3)
.css('left', spacing * 3)
.animate({'left': spacing * 2}, 'fast');
event.preventDefault();
}).hover(function() {
$rightRollover.appendTo(this).show();
}, function() {
$rightRollover.hide();
});
// Center image; enlarge cover.
$covers.eq(1)
.css('left', spacing)
.click(function(event) {
$waitThrobber.appendTo(this).show();
var price = $(this).find('.price').text();
var startPos = $(this).offset();
startPos.width = $(this).width();
startPos.height = $(this).height();
var endPos = {};
endPos.width = startPos.width * 3;
endPos.height = startPos.height * 3;
endPos.top = 100;
endPos.left = ($('body').width() - endPos.width) / 2;
$enlargedCover.attr('src', $(this).attr('href'))
.css(startPos)
.show();
var performAnimation = function() {
$waitThrobber.hide();
$enlargedCover.animate(endPos, 'normal',
function() {
$enlargedCover.one('click', function() {
$closeButton.unbind('click').hide();
$priceBadge.hide();
$enlargedCover.fadeOut();
});
$closeButton
.css({
'left': endPos.left,
'top' : endPos.top
image carouselcode})
.show()
.click(function() {
$enlargedCover.click();
});
$priceBadge
.css({
'right': endPos.left,
'top' : endPos.top
})
.text(price)
.show();
});
};
if ($enlargedCover[0].complete) {
performAnimation();
}
else {
$enlargedCover.bind('load', performAnimation);
}
event.preventDefault();
})
.hover(function() {
$enlargeRollover.appendTo(this).show();
}, function() {
$enlargeRollover.hide();
});
};
setUpCovers();
image carouselcode});