Creating an Interactive Slideshow - Popular Third-Party jQuery Plugins - Web Development with jQuery (2015)

Web Development with jQuery (2015)

Part III. Popular Third-Party jQuery Plugins

Chapter 20. Creating an Interactive Slideshow

Slideshows are a common sight on homepages today. Typically, you have three or more panels set to transition automatically between the slides, one after another. When the slideshow finishes, it starts over again. These slideshows are typically used for marketing, displaying multiple banners.

In this chapter you learn how to use a slideshow plugin that I wrote for my open source PHP framework, Hot Toddy. It has no options but is a demonstration of how to create a reusable jQuery plugin that can accommodate multiple instances of the plugin on a single page.

Creating a Slideshow

In this section, you learn both how to create a slideshow using a plugin and how to code the plugin itself. The plugin that I've created provides only a fade transition between slides. Your goal in this chapter is to understand how the plugin works well enough to modify it to suit your needs, which should include how to use a different animation.

The principle of the slideshow is simple: Provide two or more frames that can transition between one another. The number of slides is variable; you can include as few or as many as you like, and the plugin automatically numbers them and transitions between them.

This plugin is designed to accommodate the possibility of multiple slideshows on the same page; each slideshow is called a collection. The code is designed to instantiate a new instance of the slideshow object for each collection so that they can operate independently of one another.

Use the following example (Example 20-1 in the source code download materials at www.wrox.com/go/webdevwithjquery) to start creating a slideshow:

<!DOCTYPE HTML>

<html lang='en'>

<head>

<meta charset='utf-8' />

<title>Slideshow</title>

<script src='../jQuery.js'></script>

<script src='../jQueryUI.js'></script>

<script src='Example 20-1.js'></script>

<link href='Example 20-1.css' rel='stylesheet' />

</head>

<body>

<div id='slides' class='slideshow'>

<div class='slide'>

<a href='#'>

<img src='images/Faces of Autumn.jpg'
alt="Faces of Autumn" />

</a>

</div>

<div class='slide'>

<a href='#'>

<img src='images/Key.png' alt="Key" />

</a>

</div>

<div class='slide'>

<a href='#'>

<img src='images/Pencil Drawing.jpg'
alt="Pencil Drawing" />

</a>

</div>

</div>

</body>

</html>

The preceding HTML is joined with the following CSS:

body {

font: 12px 'Lucida Grande', Arial, sans-serif;

background: #fff;

color: rgb(50, 50, 50);

}

div#slides {

position: relative;

border: 1px solid black;

height: 200px;

width: 500px;

}

div.slide {

position: absolute;

top: 0;

left: 0;

width: 500px;

height: 200px;

background: black;

overflow: hidden;

z-index: 1;

}

ul.slideshowControls {

list-style: none;

padding: 0;

margin: 0;

position: absolute;

z-index: 2;

}

ul.slideshowControls li {

float: left;

width: 10px;

height: 10px;

margin: 5px 0 0 5px;

border-radius: 5px;

background: black;

text-indent: -2000000px;

cursor: pointer;

border: 1px solid white;

overflow: hidden;

}

ul.slideshowControls li.slideshowControlActive {

background: white;

border: 1px solid black;

}

div#slide-1-1 img {

position: relative;

top: -200px;

}

div#slide-1-2 img {

position: relative;

top: -200px;

}

div#slide-1-3 img {

position: relative;

top: -600px;

}

The preceding style sheet and HTML are accompanied by the following JavaScript, which completes this example:

var slideshows = [];

$.fn.extend({

slideshow : function()

{

return this.each(

function()

{

var node = $(this);

if (typeof node.data('slideshow') === 'undefined')

{

var slideCollection = slideshows.length + 1;

slideshows[slideCollection] =

new slideshow(node, slideCollection);

node.data('slideshow', slideCollection);

}

}

);

}

});

// From John Resig's awesome class instantiation code.

// http://ejohn.org/blog/simple-class-instantiation/

var hot = {

factory : function()

{

return function(args)

{

if (this instanceof arguments.callee)

{

if (typeof(this.init) == 'function')

{

this.init.apply(this, args && args.callee? args : arguments);

}

}

else

{

return new arguments.callee(arguments);

}

}

}

};

var slideshow = hot.factory();

slideshow.prototype.init = function(node, slideCollection)

{

this.counter = 1;

this.isInterrupted = false;

this.transitioning = false;

this.resumeTimer = null;

if (!node.find('ul.slideshowControls').length)

{

node.prepend(

$('<ul/>').addClass('slideshowControls')

);

}

node.find('ul.slideshowControls').html(");

var slideInCollection = 1;

node.find('.slide').each(

function()

{

this.id = 'slide-' + slideCollection + '-' + slideInCollection;

node.find('ul.slideshowControls')

.append(

$('<li/>')

.attr(

'id',

'slideshowControl-' + slideCollection + '-' +

slideInCollection

)

.html(

$('<span/>').text(slideInCollection)

)

);

slideInCollection++;

}

);

node.find('ul.slideshowControls li:first')

.addClass('slideshowControlActive');

node.find('ul.slideshowControls li')

.hover(

function()

{

$(this).addClass('slideshowControlOn');

},

function()

{

$(this).removeClass('slideshowControlOn');

}

)

.click(

function()

{

if (!slideshows[slideCollection].transitioning)

{

if (slideshows[slideCollection].resumeTimer)

{

clearTimeout(slideshows[slideCollection].resumeTimer);

}

slideshows[slideCollection].transitioning = true;

slideshows[slideCollection].isInterrupted = true;

var li = $(this);

node.find('ul.slideshowControls li')

.removeClass('slideshowControlActive');

node.find('.slide:visible')

.fadeOut('slow');

var slideInCollection = parseInt($(this).text());

var counter = slideInCollection + 1;

var resetCounter = (

(slideInCollection + 1) >

node.find('ul.slideshowControls li').length

);

if (resetCounter)

{

counter = 1;

}

slideshows[slideCollection].counter = counter;

$('#slide-' + slideCollection + '-' + slideInCollection)

.fadeIn(

'slow',

function()

{

li.addClass('slideshowControlActive');

slideshows[slideCollection].transitioning = false;

slideshows[slideCollection].resumeTimer = setTimeout(

'slideshows[' + slideCollection + '].resume();',

5000

);

}

);

}

}

);

this.resume = function()

{

this.isInterrupted = false;

this.transition();

};

this.transition = function()

{

if (this.isInterrupted)

{

return;

}

node.find('.slide:visible')

.fadeOut('slow');

node.find('ul.slideshowControls li')

.removeClass('slideshowControlActive');

$('#slide-' + slideCollection + '-' + this.counter).fadeIn(

'slow',

function()

{

node.find('ul.slideshowControls li').each(

function()

{

if (parseInt($(this).text()) ==

slideshows[slideCollection].counter)

{

$(this).addClass('slideshowControlActive');

}

}

);

slideshows[slideCollection].counter++;

var resetCounter = (

slideshows[slideCollection].counter >

node.find('ul.slideshowControls li').length

);

if (resetCounter)

{

slideshows[slideCollection].counter = 1;

}

setTimeout(

'slideshows[' + slideCollection + '].transition();',

5000

);

}

);

};

this.transition();

};

$(document).ready(

function()

{

if ($('.slideshow').length)

{

$('.slideshow').slideshow();

}

}

);

The preceding example results are shown in Figure 20.1.

image

Figure 20.1

The HTML in this example is designed to allow the author to specify as little as possible about the slideshow. The number of slides is automatically calculated from the plugin, and they are transitioned in the order that they appear in the document. Because each slide is a <div> element, you can have any HTML you like within each slide, including text for a marketing message. The slideshow controls are also automatically generated by the plugin.

The remainder of this chapter examines the JavaScript in this example in detail, explaining how each bit comes together to create the larger plugin.

You begin with a simple global variable declaration.

var slideshows = [];

This variable, slideshows, contains each instance of a slideshow object that has been created, making it possible to have as many slideshows as you like within the same DOM.

The next section of code creates a jQuery plugin called $.slideshow(). Unlike most of the jQuery plugins you've seen so far, this plugin has no option parameters. You could add options for pausing a slideshow, destroying a slideshow, and so on—whatever configurable parameters you want to add. Doing so would be a simple matter of adding optional arguments for the jQuery $.slideshow() method, and then deciding how those arguments should act upon the correct corresponding slideshow() object that gets instantiated for each instance of a slideshow on the page.

$.fn.extend({

slideshow : function()

{

return this.each(

function()

{

var node = $(this);

if (typeof node.data('slideshow') === 'undefined')

{

var slideCollection = slideshows.length + 1;

slideshows[slideCollection] =

new slideshow(node, slideCollection);

node.data('slideshow', slideCollection);

}

}

);

}

});

Presently, the $.slideshow() method is called on each HTML element with a class name of slideshow, and that happens automatically when the DOM is ready. When $.slideshow() is called, a variable called slideCollection is created based on the number of slideshows already created. This variable keeps track of each collection and makes it possible to go back and reference an existing collection. Each slideshow in the document is numbered offset from one, and this data is stored with each instance of the slideshow using the jQuery data API. If an instance does not have the associated slideshow data attached to it, then it hasn't been processed by the plugin. This makes it possible to call the $.slideshow() plugin method as many times as you need to call it to create a slideshow over the life of a document or application. New slideshows are created and added to the existing collection of slideshows as needed.

The next bit of code is lifted from the website of John Resig (the creator of jQuery). It provides an easy-to-reference factory method for creating prototype objects, which is to say a single object that you can instantiate again and again, creating multiple copies, each with their own properties, timers, and settings, individually intact.

// From John Resig's awesome class instantiation code.

// http://ejohn.org/blog/simple-class-instantiation/

var hot = {

factory : function()

{

return function(args)

{

if (this instanceof arguments.callee)

{

if (typeof this.init == 'function')

{

this.init.apply(this, args && args.callee? args : arguments);

}

}

else

{

return new arguments.callee(arguments);

}

}

}

};

var slideshow = hot.factory();

The next section of code begins the slideshow.prototype.init function. When the slideshow is instantiated with new slideshow(node, slideCollection), the slideshow.prototype.init function is executed to create a new copy of the slideshow object.

The names of the arguments are the same so that you can easily associate new slideshow(node, slideCollection) with slideshow.prototype.init = function(node, slideCollection). When a new slideshow object is created, some variables are created to keep track of different states.

The this.counter property keeps track of which slide is presently being displayed. The this.isInterrupted property keeps track of whether the slideshow has been interrupted by the user clicking a slide control. When a slideshow is interrupted, the slideshow pauses for 5 seconds on the slide the user clicked to see, and then the slideshow automatically resumes.

The this.transitioning property keeps track of whether an animated transition is occurring. This property prevents multiple animations from stacking up by ignoring any additional animation requests until the current one has completed.

The this.resumeTimer property keeps a reference to timers created when the user interrupts a slideshow. This timer occasionally needs to be cleared or created based on what the user does.

slideshow.prototype.init = function(node, slideCollection)

{

this.counter = 1;

this.isInterrupted = false;

this.transitioning = false;

this.resumeTimer = null;

The next line creates the slideshow controls. At this point, the element created is a <ul> element with the class name slideshowControls; it does not yet have any child elements.

if (!node.find('ul.slideshowControls').length)

{

node.prepend(

$('<ul/>').addClass('slideshowControls')

);

}

If the <ul> with the class name slideshowControls does already exist, its children are removed.

Next, you iterate over each element existing within the slideshow container element with the class name slide. This block of code begins with the declaration of a variable, slideInCollection, which is a counter to keep track of which slide in this slideshow is presently under consideration. The counter creates id names as well as creates the slideshow controls.

var slideInCollection = 1;

node.find('.slide').each(

function()

{

this.id = 'slide-' + slideCollection + '-' + slideInCollection;

node.find('ul.slideshowControls')

.append(

$('<li/>')

.attr(

'id',

'slideshowControl-' + slideCollection + '-' +

slideInCollection

)

.html(

$('<span/>').text(slideInCollection)

)

);

slideInCollection++;

}

);

First, each element with the slide class name is given an id name that identifies the collection's offset and the slide's offset. The <ul> with the class name slideshowControls is given a new <li> element for each slide. Each <li> element is populated with a <span> element, which in turn contains the numbered offset of the slide.

You can find the first <li> element within the <ul> slideshowControls with the following code, and it is given a class name slideshowControlActive.

node.find('ul.slideshowControls li:first')

.addClass('slideshowControlActive');

Next, each <li> element within <ul> slideshowControls is provided with hover and click events. The hover event simply toggles the presence of the slideshowControlOn class name.

node.find('ul.slideshowControls li')

.hover(

function()

{

$(this).addClass('slideshowControlOn');

},

function()

{

$(this).removeClass('slideshowControlOn');

}

)

The click event controls what happens when the user clicks a slideshow control, indicating that the user wants to see that particular slide again.

Within the callback function to the click event, you can use the existing slideshows global variable to refer to the correct instance of the slideshow object. You can do this in many ways—this is the method that best illustrates what is happening.

The first statement within this block of code checks whether an animation is in progress using the transitioning property. If an animation is occurring, then nothing happens and the click event is ignored.

.click(

function()

{

if (!slideshows[slideCollection].transitioning)

{

Next, you check to see if there is a resumeTimer active; the resumeTimer controls the interval between slide transitions. If a timer is active, it is cleared by calling the native clearTimeout() method.

if (slideshows[slideCollection].resumeTimer)

{

clearTimeout(slideshows[slideCollection].resumeTimer);

}

The transitioning property is set to true to indicate that a slide animation is taking place. Then the isInterrupted property is set to true to indicate that the user clicked a slide control and interrupted the slideshow.

slideshows[slideCollection].transitioning = true;

slideshows[slideCollection].isInterrupted = true;

A reference to the <li> element's jQuery object is stored in the variable named li.

var li = $(this);

All <li> elements within the current slideshow slideshowControlActive class have that class removed.

node.find('ul.slideshowControls li')

.removeClass('slideshowControlActive');

The currently visible slide identified with the class name slide and the :visible pseudo-class (proprietary to jQuery) is faded out with an animation.

node.find('.slide:visible')

.fadeOut('slow');

A reference to the current slide's offset number is retrieved from the text inside the current <li> element.

var slideInCollection = parseInt($(this).text());

A counter variable is created so that when the slideshow resumes, the counter property contains the correct reference to the correct slide.

var counter = slideInCollection + 1;

If the number contained in slideInCollection + 1 exceeds the length of slides in the collection, then the counter is reset to 1. This moves the slideshow forward from the last slide to the first slide in a loop.

var resetCounter = (

(slideInCollection + 1) >

node.find('ul.slideshowControls li').length

);

if (resetCounter)

{

counter = 1;

}

The value of the counter variable is moved to the counter property so that the slideshow can continue to function properly when it resumes automatically.

slideshows[slideCollection].counter = counter;

Then the slide the user clicked is faded in with an animation. The slide is referenced by its collection number and its slide number.

$('#slide-' + slideCollection + '-' + slideInCollection)

.fadeIn(

'slow',

function()

{

The slideshowControlActive class name is added to the li variable holding a reference to the current <li> element's jQuery object.

li.addClass('slideshowControlActive');

Now that the animation has completed, the transitioning property is set to false to indicate that there is no animation taking place right now.

slideshows[slideCollection].transitioning = false;

Because the animation has completed, the next step is to resume the timer. A call to setTimeout() triggers a slideshow transition to occur automatically after 5 seconds, as though the slideshow had never been interrupted. The next slide transition occurs after an additional 5 seconds.

slideshows[slideCollection].resumeTimer = setTimeout(

'slideshows[' + slideCollection + '].resume();',

5000

);

}

);

}

}

);

The next block of code is an API method. It resumes the slideshow if the slideshow has been interrupted.

this.resume = function()

{

this.isInterrupted = false;

this.transition();

};

This method is used only when the user clicks a control to manually flip to a slide. It resets the isInterrupted property to false and then triggers the next transition by calling the transition() method.

The transition() method is used for the normal transition of one slide to the next, repeating display of all slides on a loop, endlessly.

this.transition = function()

{

If the isInterrupted property is true, the method returns and nothing happens. This means that the process of interrupting the slideshow has not completed and should not be interfered with.

if (this.isInterrupted)

{

return;

}

Like in the block that handled clicking a slide control, the first thing you do is hide the currently visible slide with a call to fadeOut(). You can find the slide within the current slideshow by the class name slide and the :visible pseudo-class.

node.find('.slide:visible')

.fadeOut('slow');

Then, all the <li> slideshowControls within the current slideshow lose the class name slideshowControlActive.

node.find('ul.slideshowControls li')

.removeClass('slideshowControlActive');

Finally, the new slide is animated with a call to fadeIn(); it is referenced by its collection number (slideCollection) and its slide number (this.counter).

$('#slide-' + slideCollection + '-' + this.counter).fadeIn(

'slow',

function()

{

When the new slide completes the fadeIn() animation, the current slide's corresponding <li> control within <ul> slideshowControls receives the class name slideshowControlActive. You can find the current control by comparing the text of each <li> element with the current value of the counter property.

node.find('ul.slideshowControls li').each(

function()

{

if (parseInt($(this).text()) ==

slideshows[slideCollection].counter)

{

$(this).addClass('slideshowControlActive');

}

}

);

The counter property is incremented by 1 to prepare for the next slide.

slideshows[slideCollection].counter++;

If the counter property's new value is greater than the total number of slides, it is reset to 1 so that the counter property goes from referencing the last slide to the first slide, when the last slide is displayed.

var resetCounter = (

slideshows[slideCollection].counter >

node.find('ul.slideshowControls li').length

);

if (resetCounter)

{

slideshows[slideCollection].counter = 1;

}

A new timer is created that runs from this transition to the next one.

setTimeout(

'slideshows[' + slideCollection + '].transition();',

5000

);

}

);

};

A call to this.transition(); starts the slideshow running:

this.transition();

Finally, at the end of the script, a ready event fires when the DOM is ready. It checks to see whether there are any items with the class name slideshow; if there are, the $.slideshow() jQuery plugin method is called on each of those elements.

$(document).ready(

function()

{

if ($('.slideshow').length)

{

$('.slideshow').slideshow();

}

}

);

Summary

In this chapter you learned how to create and use a plugin for creating interactive slideshows, like those used to display advertising on many websites' homepages. The plugin that you created can handle one or more distinct slideshows within a document, each with two or more slides. Using a prototype style of programming, you can create distinct objects for each slideshow, each with their own properties and states of being.

Exercises

1. What is the purpose of keeping track of whether a slideshow has been interrupted by the user using the isInterrupted property?

2. What is the purpose of keeping track of whether a transition is in progress with the transitioning property?

3. Describe how the plugin automatically creates controls to click a specific slide. What information is contained in the id name of each control? What information is available in the text of each control?

Extra Credit: Create your own version of the slideshow plugin with options to

· Start, pause, or resume a slideshow.

· Destroy a slideshow.

· Set a custom amount of time between slide transitions.

· Set a custom animation.