MULTIMEDIA

jQuery 1.3 : Headline rotator

10/9/2010 4:50:58 PM

For our first rotator example, we'll take a news feed and scroll the headlines, along with an excerpt of the article, into view one at a time. The stories will flow into view, pause to be read, and then slide up and off the page as if there were an infinite ribbon of information rolling over the page.

Setting up the page

At its most basic level, this feature is not very difficult to implement. But as we will soon see, making it production-ready requires a bit of finesse.

We begin, as usual, with a chunk of HTML. We'll place the news feed in the sidebar of the page:

<h3>Recent News</h3>
<div id="news-feed">
<a href="news/index.html">News Releases</a>
</div>

So far, the content area of our news feed contains only a single link to the main news page.

This is our graceful degradation scenario, in case the user does not have JavaScript enabled. The content we'll be working with will come from an actual RSS feed instead.

The CSS for this<div> is important as it will determine not only how much of each news item will be shown at a time, but also where on the page the news items will appear. Together with the style rule for the individual news items, the CSS looks like this:

#news-feed {
headline rotatorpage, setting upposition: relative;
height: 200px;
width: 17em;
overflow: hidden;
}
.headline {
position: absolute;
height: 200px;
top: 210px;
overflow: hidden;
}

Notice here that the height of both the individual news items (represented by the headline class) and their container is 200px. Also, since headline elements are absolutely positioned relative to #news-feed, we're able to line up the top of the news items with the bottom edge of their container. That way, when we set the property of #news-feed to hidden, the headlines are not displayed initially. overflow

Setting the position of the headlines to absolute is necessary for another reason as well: for any element to have its location animated on the page, it must have either or relative positioning, rather than the default static positioning. absolute

Now that we have the HTML and CSS in place, we can inject the news items from an RSS feed. To start, we'll wrap the code in a .each() method, which will act as an if statement of sorts and contain the code inside a private namespace:

$(document).ready(function() {
$('#news-feed').each(function() {
var $container = $(this);
$container.empty();
});
});

Normally when we use the .each() method, we are iterating over a possibly large set of elements. Here, though, our selector #news-feed is looking for an ID, so there are only two potential outcomes. The factory function could make a jQuery object matching one unique element with the news-feed ID, or it could find no elements on the page with that ID and produce an empty jQuery object. The .each() call takes care of executing the contained code if, and only if, the jQuery object is non-empty.

At the beginning of our .each() loop, the news feed container is emptied to make it ready for its new content.

Retrieving the feed

To retrieve the feed, we'll use the $.get() method, one of jQuery's many AJAX functions for communicating with the server. This method, as we have seen before, allows us to operate on content from a remote source by using a success handler. The content of the feed is passed to this handler as an XML structure. We can then use jQuery's selector engine to work with this data.

$(document).ready(function() {
$('#news-feed').each(function() {
var $container = $(this);
$container.empty();
$.get('news/feed.xml', function(data) {
$('rss item', data).each(function() {
// Work with the headlines here.
});
});

});
});

For more information on $.get() and other AJAX methods, see Chapter 6.


Now, we need to combine the parts of each item into a usable block of HTML markup. We can use .each() again to go through the items in the feed and build the headline links:

$(document).ready(function() {
headline rotatorfeed, retrieving$('#news-feed').each(function() {
var $container = $(this);
$container.empty();
$.get('news/feed.xml', function(data) {
$('rss item', data).each(function() {
var $link = $('<a></a>')
.attr('href', $('link', this).text())
.text($('title', this).text());
var $headline = $('<h4></h4>').append($link);
$('<div></div>')
.append($headline)
.appendTo($container);

});
});
});
});

We get the text of each item's<title> and<link> elements, and construct the<a> element from them. This link is then wrapped in an<h4> element. We put each news item into<div id="news-feed">, but for now we're omitting the headline class on each news item's containing<div> so that we can more easily see our work in progress.

In addition to the headlines, we want to display a bit of supporting information about each article. We'll grab the publication date and article summary, and display these as well.

$(document).ready(function() {
$('#news-feed').each(function() {
var $container = $(this);
$container.empty();
$.get('news/feed.xml', function(data) {
$('rss item', data).each(function() {
var $link = $('<a></a>')
.attr('href', $('link', this).text())
.text($('title', this).text());
var $headline = $('<h4></h4>').append($link);
var pubDate = new Date(
$('pubDate', this).text());
var pubMonth = pubDate.getMonth() + 1;
var pubDay = pubDate.getDate();
var pubYear = pubDate.getFullYear();
var $publication = $('<div></div>')
.addClass('publication-date')
.text(pubMonth + '/' + pubDay + '/'
+ pubYear);
var $summary = $('<div></div>')
.addClass('summary')
.html($('description', this).text());

$('<div></div>')
.append($headline, $publication, $summary)

.appendTo($container);
});
});
});
});



The date information in an RSS feed is encoded in RFC 822 format, which includes date, time, and time zone information (for example, Sun, 28 Sep 2008 18:01:55 +0000). This format is not particularly eye-pleasing, so we use JavaScript's built-in Date object to produce a more compact representation of the date (such as. 9/28/2008)

The summary information is easier to retrieve and format. It's worth noting, though, that in our sample feed, some HTML entities may exist in the description. To make sure that these are not automatically escaped by jQuery, we need to use the .html() method to insert the description into the page, rather than the .text() method.

With these new elements created, we insert them into the document using the .append() method. Note here that we are using a new feature of the method; if more than one argument is supplied, all of them get appended in sequence.

As we can see, the title, date, link, and summary of each news item is now in place. All that's left is to add the headline class with .addClass('headline') (which will hide them from view because of the CSS we defined earlier), and we are ready to proceed with our animation.

Setting up the rotator

Since the visible news item will change over time, we'll need a way to easily keep track of which items are visible and where they are. First, we'll set two variables, one for the currently visible headline and one for the headline that has just scrolled out of view. Initially, both values will be 0.

var currentHeadline = 0, oldHeadline = 0;

Next, we'll take care of some initial positioning of the headlines. Recall that in the stylesheet we have already set the top property of the headlines to be 10 pixels greater than their container's height; because the container has an overflow property of hidden, the headlines are initially not displayed. It'll be helpful later on if we store that property in a variable, so that we can move headlines to this position when needed.

var hiddenPosition = $container.height() + 10;

We also want the first headline to be visible immediately upon page load. To achieve this, we can set its top property to 0.

$('div.headline').eq(currentHeadline).css('top', 0);

The rotator area of the page is now in the correct initial state:

Finally, we'll store the total number of headlines for later use and define a timeout variable to be used for the pause mechanism between each rotation.

var headlineCount = $('div.headline').length;
var pause;

There is no need yet to give pause a value at this time; it will be set each time the rotation occurs. Nevertheless, we must always declare local variables using var to avoid the risk of collisions with global variables of the same name.

The headline rotate function

Now we're ready to rotate the headlines, dates, and summaries. We'll define a function for this task so that we can easily repeat the action each time we need it.

First, let's take care of updating the variables that are tracking which headline is active. The modulus operator (%) will let us easily cycle through the headline numbers. We can add 1 to the currentHeadline value each time our function is called, and then take this value modulus the headlineCount value to constrain the variable to valid headline numbers.

Recall that we used this same technique to cycle through row colors when striping tables in Chapter 7.


We should also update the oldHeadline value so that we can easily manipulate the headline that is moving out of view.

var headlineRotate = function() {
headline rotatorheadline rotate functioncurrentHeadline = (oldHeadline + 1) % headlineCount;
// Animate the headline positions here.
oldHeadline = currentHeadline;
};


Now we have to fill in the gap with code that actually moves the headlines. First, we'll add an animation that moves the old headline out of the way. Then, we'll insert another animation that slides the new headline into view.
var headlineRotate = function() {
currentHeadline = (oldHeadline + 1) % headlineCount;
$('div.headline').eq(oldHeadline).animate(
{top: -hiddenPosition}, 'slow', function() {
$(this).css('top', hiddenPosition);
});
$('div.headline').eq(currentHeadline).animate(
{top: 0}, 'slow', function() {
pause = setTimeout(headlineRotate, 5000);
});
oldHeadline = currentHeadline;
};

In both cases, we're animating the top property of the news item. Recall that the items are hidden because they have a top value of hiddenPosition (which is a number greater than the height of the container). Animating this property to 0 brings an item into view; further animating it to -hiddenPosition moves it out of view again.


In both cases, we also have a callback function specified to take action when the animation is complete. When the old headline has completely slid out of view, it gets its top property reset to hiddenPosition so it is ready to return later. When the new headline is finished with its animation, we want to queue up the next transition; this is done with a call to the JavaScript setTimeout() function, which registers a function to be invoked after a specified period. In this case, we're causing to be fired again in five seconds (5000 milliseconds). headlineRotate()

We now have a cycle of activity; once one animation completes, the next one is ready to activate. It remains to call the function the first time; we'll do this with another call to setTimeout(), causing the first transition to happen 5 seconds after the RSS feed has been retrieved. Now we have a functional headline rotator.

$(document).ready(function() {
$('#news-feed').each(function() {
var $container = $(this);
$container.empty();
$.get('news/feed.xml', function(data) {
$('rss item', data).each(function() {
var $link = $('<a></a>')
.attr('href', $('link', this).text())
.text($('title', this).text());
var $headline = $('<h4></h4>').append($link);
var pubDate = new Date($('pubDate', this).text());
var pubMonth = pubDate.getMonth() + 1;
var pubDay = pubDate.getDate();
var pubYear = pubDate.getFullYear();
var $publication = $('<div></div>')
.addClass('publication-date')
.text(pubMonth + '/' + pubDay + '/' + pubYear);
var $summary = $('<div></div>')
.addClass('summary')
.html($('description', this).text());
$('<div></div>')
.addClass('headline')
.append($headline, $publication, $summary)
.appendTo($container);
});
var currentHeadline = 0, oldHeadline = 0;
var hiddenPosition = $container.height() + 10;
$('div.headline').eq(currentHeadline).css('top', 0);
var headlineCount = $('div.headline').length;
var pause;
var headlineRotate = function() {
currentHeadline = (oldHeadline + 1) % headlineCount;
$('div.headline').eq(oldHeadline).animate(
{top: -hiddenPosition}, 'slow', function() {
$(this).css('top', hiddenPosition);
});
$('div.headline').eq(currentHeadline).animate(
{top: 0}, 'slow', function() {
pause = setTimeout(headlineRotate, 5000);
});
oldHeadline = currentHeadline;
};
pause = setTimeout(headlineRotate, 5000);
});
});
});


Partially through the animation, we can see one headline cropped at the top, and the next coming into view cropped at the bottom:

Pause on hover

Even though the headline rotator is now fully functioning, there is one significant usability issue that we should address&mdash;a headline might scroll out of the viewable area before a user is able to click on one of its links. This forces the user to wait until the rotator has cycled through the full set of headlines again before getting a second chance. We can reduce the likelihood of this problem by having the rotator pause when the user's mouse cursor hovers anywhere within the headline.

$container.hover(function() {
headline rotatorusability issue, addressingclearTimeout(pause);
}, function() {
pause = setTimeout(headlineRotate, 250);
});

When the mouse enters the headline area, the first .hover() handler calls JavaScript's clearTimeout() function. This cancels the timer in progress, preventing from being called. When the mouse leaves, the second .hover() handler reinstates the timer, thereby invoking headlineRotate() after a 250 millisecond delay. headlineRotate()

This simple code works fine most of the time. However, if the user moves the mouse over and back out of the<div> quickly and repeatedly, a very undesirable effect can occur. Multiple headlines will be in motion at a time, layering on top of each other in the visible area.

Unfortunately, we need to perform some serious surgery to remove this cancer. Before the headlineRotate() function, we'll introduce one more variable:

var rotateInProgress = false;

Now, on the very first line of our function, we can check if a rotation is currently in progress. Only if the value of rotateInProgress is false do we want the code to run again. Therefore, we wrap everything within the function in an if statement. Immediately inside this conditional, we set the variable to true, and then in the callback of the second .animate() method, we set it back to false.

var headlineRotate = function() {
if (!rotateInProgress) {
rotateInProgress = true;

currentHeadline = (oldHeadline + 1)
% headlineCount;
$('div.headline').eq(oldHeadline).animate(
{top: -hiddenPosition}, 'slow', function() {
$(this).css('top', hiddenPosition);
});
$('div.headline').eq(currentHeadline).animate(
{top: 0}, 'slow', function() {
rotateInProgress = false;

pause = setTimeout(headlineRotate, 5000);
});
oldHeadline = currentHeadline;
}
};

These few additional lines improve our headline rotator substantially. Rapid, repeated hovering no longer causes the headlines to pile up on top of each other. Yet this user behavior still leaves us with one nagging problem: the rhythm of the rotator is thrown off with two or three animations immediately following each other, rather than all of them evenly spaced out at five-second intervals.

The problem is that more than one timer can become active concurrently if a user mouses out of the<div> before the existing timer completes. We therefore need to put one more safeguard into place, using our pause variable as a flag indicating whether another animation is imminent. To do this, we set the variable to false when the timeout is cleared or when one completes. Now we can test the variable's value to make sure there is no timeout active before we put a new one in place.

var headlineRotate = function() {
if (!rotateInProgress) {
rotateInProgress = true;
pause = false;

currentHeadline = (oldHeadline + 1)
% headlineCount;
$('div.headline').eq(oldHeadline).animate(
{top: -hiddenPosition}, 'slow', function() {
$(this).css('top', hiddenPosition);
});
$('div.headline').eq(currentHeadline).animate(
{top: 0}, 'slow', function() {
rotateInProgress = false;
if (!pause) {

pause = setTimeout(headlineRotate, 5000);
}
});
oldHeadline = currentHeadline;
}
};
if (!pause) {

pause = setTimeout(headlineRotate, 5000);
}
$container.hover(function() {
clearTimeout(pause);
pause = false;

}, function() {
if (!pause) {

pause = setTimeout(headlineRotate, 250);
}
});


At last, our headline rotator can withstand all manner of attempts by the user to thwart it.

Retrieving a feed from a different domain

The news feed that we've been using for our example is a local file, but we might want to retrieve a feed from another site altogether. As we saw in Chapter 6, AJAX requests cannot, as a rule, be made to a different site than the one hosting the page being viewed. There, we discussed the JSONP data format as a method for circumventing this limitation. Here, though, we'll assume we cannot modify the data source, so we need a different solution.

To allow AJAX to fetch this file, we'll use some server-side code as a proxy for the request, so that JavaScript believes the XML file is on our server even though it actually resides on a different one. We will write a short PHP script to pull the content of the news feed to our server, and relay that data to the requesting jQuery script. This script, which we'll call feed.php, can be called in the same way feed.xml was fetched previously:

$.get('news/feed.php', function(data) {
// ...
});

Inside the feed.php file, we pull in the content of the news feed from the remote site, then print the content as the output of the script.

<?php
header('Content-Type: text/xml');
print file_get_contents('http://jquery.com/blog/feed');
?>

Note here that we need to explicitly set the content type of the page to text/xml so that jQuery can fetch it and parse it as if it were a normal, static XML document.

Some web-hosting providers may not allow the use of the PHP file_get_contents() function to fetch remote files because of security concerns. In these cases, alternative solutions, such as using the cURL library, may be available. More information on this library can be found at http://wiki.dreamhost.com/CURL.


Adding a loading indicator

Pulling in a remote file like this might take some time, depending on a number of factors, so we should inform the user that loading is in progress. To do this, we'll add a loading indicator image to the page before we issue our AJAX request.

var $loadingIndicator = $('<img/>')
.attr({
'src': 'images/loading.gif',
'alt': 'Loading. Please wait.'
})
.addClass('news-wait')
.appendTo($container);

Then, as the first line of our $.get() function's success callback, we can remove the image from the page with a simple command:

$loadingIndicator.remove();

Now, when the page first loads, if there is a delay in retrieving the headline content, we'll see a loading image rather than an empty area.

This image is an animated GIF, and in a web browser will spin to signify that activity is taking place.

We can easily create new animated GIF images for use as AJAX loading indicators by using the service at http://ajaxload.info/.


Gradient fade effect

Before we put away our headline rotator example, let's give it a finishing touch, by making the headline text appear to fade in from the bottom of its container. The effect will be a gradient fade, appearing as if the text is opaque at the top of the effect area and transparent at the bottom.

A single text element cannot have multiple opacities simultaneously, however. To simulate this, we'll actually cover up the effect area with a series of elements, each of which has a different opacity. These slices with be<div> elements with a few style properties in common, which we can declare in our stylesheet:

.fade-slice {
position: absolute;
width: 20em;
height: 2px;
background: #efd;
z-index: 3;
}

They all have the same width and background-color properties as their containing element,<div id="news-feed">. This will fool the user's eye into thinking the text is fading away, rather than being covered up by another element.

Now we can create the<div class="fade-slice"> elements. To make sure we have the right number of them, first we'll determine a height in pixels for the entire effect area. In this case, we're choosing 25 percent of the<div id="news-feed"> height. We'll use a for loop to iterate across the height of this area, creating a new slice element for each 2-pixel segment of the gradient:

$(document).ready(function() {
$('#news-feed').each(function() {
var $container = $(this);
$container.empty();
var fadeHeight = $container.height() / 4;
for (var yPos = 0; yPos < fadeHeight; yPos += 2) {
$('<div></div>')
.addClass('fade-slice')
.appendTo($container);
}
});
});

Now we have 25 slices (one for each 2-pixel segment of the 50-pixel gradient area), but they are all piled up at the top of the container. For our trick to work, we need each one to have a different position and opacity. We can use the iteration variable yPos to mathematically determine each element's opacity and top properties:

$(document).ready(function() {
$('#news-feed').each(function() {
var $container = $(this);
$container.empty();
var fadeHeight = $container.height() / 4;
for (var yPos = 0; yPos < fadeHeight; yPos += 2) {
$('<div></div>').css({
opacity: yPos / fadeHeight,
top: $container.height() - fadeHeight + yPos
}).addClass('fade-slice').appendTo($container);

}
});
});

These calculations can be a bit tricky to visualize, so we'll lay out the numbers in a table. The opacity values step up incrementally from transparent to opaque, as the top values begin at the top of the fade area (150) and grow to the container's height:

yPos opacity top
0 0 150
2 0.04 152
4 0.08 154
6 0.12 156
8 0.16 158
  
40 0.80 190
42 0.84 192
44 0.88 194
46 0.92 196
48 0.96 198

Keep in mind that since the top position of the final<div class="fade-slice"> is 198, its 2-pixel height will neatly overlay the bottom two pixels of the 200-pixel-tall containing<div>.

With our code in place, the text in the headline area of the page now blends beautifully from transparent to opaque as it overlaps the bottom of the container:

The finished code

Our first rotator is now complete. The news items are now fetched from a remote server, formatted, animated in and out of view on schedule, and beautifully styled:

$(document).ready(function() {
$('#news-feed').each(function() {
var $container = $(this);
$container.empty();
var fadeHeight = $container.height() / 4;
for (var yPos = 0; yPos < fadeHeight; yPos += 2) {
$('<div></div>').css({
opacity: yPos / fadeHeight,
top: $container.height() - fadeHeight + yPos
}).addClass('fade-slice').appendTo($container);
}
var $loadingIndicator = $('<img/>')
.attr({
'src': 'images/loading.gif',
'alt': 'Loading. Please wait.'
})
.addClass('news-wait')
.appendTo($container);
$.get('news/feed.php', function(data) {
$loadingIndicator.remove();
$('rss item', data).each(function() {
var $link = $('<a></a>')
.attr('href', $('link', this).text())
.text($('title', this).text());
var $headline = $('<h4></h4>').append($link);
var pubDate = new Date($('pubDate', this).text());
var pubMonth = pubDate.getMonth() + 1;
var pubDay = pubDate.getDate();
var pubYear = pubDate.getFullYear();
var $publication = $('<div></div>')
.addClass('publication-date')
.text(pubMonth + '/' + pubDay + '/' + pubYear);
var $summary = $('<div></div>')
.addClass('summary')
.html($('description', this).text());
$('<div></div>')
.addClass('headline')
headline rotatorcode.append($headline, $publication, $summary)
.appendTo($container);
});
var currentHeadline = 0, oldHeadline = 0;
var hiddenPosition = $container.height() + 10;
$('div.headline').eq(currentHeadline).css('top', 0);
var headlineCount = $('div.headline').length;
var pause;
var rotateInProgress = false;
var headlineRotate = function() {
if (!rotateInProgress) {
rotateInProgress = true;
pause = false;
currentHeadline = (oldHeadline + 1)
% headlineCount;
$('div.headline').eq(oldHeadline).animate(
{top: -hiddenPosition}, 'slow', function() {
$(this).css('top', hiddenPosition);
});
$('div.headline').eq(currentHeadline).animate(
{top: 0}, 'slow', function() {
rotateInProgress = false;
if (!pause) {
pause = setTimeout(headlineRotate, 5000);
}
});
oldHeadline = currentHeadline;
}
};
if (!pause) {
pause = setTimeout(headlineRotate, 5000);
headline rotatorcode}
$container.hover(function() {
clearTimeout(pause);
pause = false;
}, function() {
if (!pause) {
pause = setTimeout(headlineRotate, 250);
}
});
});
});
});



Other  
  •  Silverlight : Print a Document
  •  Silverlight : Capture a Webcam
  •  Silverlight : Make Your Application Run out of the Browser
  •  Silverlight : Put Content into a 3D Perspective
  •  Silverlight : Response to Timer Events on the UI Thread
  •  Silverlight : Build a Download and Playback Progress Bar
  •  Silverlight : Play a Video
  •  C# 4.0 : Add a Static Constructor and Initialization
  •  C# 4.0 : Add a Constructor
  •  .NET Compact Framework : Font Selection
  •  .NET Compact Framework : Drawing Text
  •  Programming the Service Bus
  •  WCF Services : Generics
  •  WCF Services : Delegates and Data Contracts
  •  WCF Services : Enumerations
  •  WCF Services : Versioning
  •  WCF Services : Data Contract - Equivalence
  •  WCF Services : Data Contract - Hierarchy
  •  WCF Services : Data Contract - Attributes
  •  Executing Work on a Background Thread with Updates
  •  
    Most View
    The Roundup Of 120mm Fans: 1,350RPM Speed And More (Part 1)
    Windows 8 Hybrids, Tablets And Laptops (Part 5)
    Essential Mobile-Commerce Technology (part 3) - MOBILE COMMERCE PAYMENT METHODS
    Deconstructed - Five Classic Bass Music Tunes And Discover Some Key Ideas (Part 2)
    Arcam FMJ A19 Integrated Amplifier - Jumping Jack (Part 2)
    Kindle Fire HD - Most Advanced 7" Tablet
    Power To The People (Part 1)
    ASP.NET 4 : Web Site Navigation (part 3) - Trapping the SiteMapResolve Event, Defining Custom Attributes for Each Node
    Olympus M.Zuiko Digital ED 75-300mm f/4.8-6.7 II Lens Review
    The Summary Of Six Mini-ITX Mainboard Based On Intel Z77 Chipset (Part 10)
    Top 10
    Managing Windows Server 2012 (part 13) - Using Remote Desktop - Supporting Remote Desktop Connection clients
    Managing Windows Server 2012 (part 12) - Using Remote Desktop - Remote Desktop essentials, Configuring Remote Desktop
    Managing Windows Server 2012 (part 11) - Optimizing toolbars
    Managing Windows Server 2012 (part 10) - Customizing the desktop and the taskbar - Using Auto Hide and locking, Controlling programs in the notification area
    Managing Windows Server 2012 (part 9) - Customizing the desktop and the taskbar - Configuring desktop items, Configuring the taskbar
    Managing Windows Server 2012 (part 8) - Using the System console
    Managing Windows Server 2012 (part 7) - Using Control Panel
    Managing Windows Server 2012 (part 6) - Working with Computer Management
    Managing Windows Server 2012 (part 5) - Working with Server Manager - Adding servers for management, Creating server groups, Enabling remote management
    Managing Windows Server 2012 (part 4) - Working with Server Manager - Getting to know Server Manager