15.8. Displaying a Countdown Timer

NN 4, IE 4

15.8.1. Problem

You want to have a running countdown timer showing on the page.

15.8.2. Solution

Countdown timers can take many forms. Look to the combination of HTML and scripts in Example 15-3 of the Discussion for inspiration for your particular timer implementation. The example application sets the turn of the new year in the user's time zone as the "zero hour" for the timer, and displays a constantly updated display of the days, hours, minutes, and seconds until that time.

An onload event handler in the page invokes the countDown( ) function by way of the setInterval( ) method:

<body onload="setInterval('countDown( )', 1000)">

The display is updated approximately every second because the setInterval( ) method repeatedly invokes the countDown( ) function until a script cancels the timer.

15.8.3. Discussion

The display of the timer can be in a text input field (all scriptable browsers), swappable images (IE 4 or later and NN 3 or later), or body text (IE 4 or later and NN 6 or later). This example uses swappable images because it lends itself to the most flexible page designs of the choices. But for other syntactic reasons, Navigator 4 is the minimum supported browser for this example. Figure 15-2 shows the output.

Figure 2. Countdown timer with swappable image

Example 15-3 shows the complete HTML document for this application, including the HTML and scripts.

Example 3. A countdown timer application

<html>
<head>
<title>Countdown Timer</title>
<style type="text/css">
table {table-collapse:collapse; border-spacing:0}
td {border:2px groove black; padding:7px; background-color:#ccffcc}
th {border:2px groove black; padding:7px; background-color:#ffffcc}
.ctr {text-align:center}
</style>
<script type="text/javascript">
if (document.images) {
    var imgArray = new Array( );
    imgArray[0] = new Image(60,120);
    imgArray[0].src = "digits/0.gif";
    imgArray[1] = new Image(60,120);
    imgArray[1].src = "digits/1.gif";
    imgArray[2] = new Image(60,120);
    imgArray[2].src = "digits/2.gif";
    imgArray[3] = new Image(60,120);
    imgArray[3].src = "digits/3.gif";
    imgArray[4] = new Image(60,120);
    imgArray[4].src = "digits/4.gif";
    imgArray[5] = new Image(60,120);
    imgArray[5].src = "digits/5.gif";
    imgArray[6] = new Image(60,120);
    imgArray[6].src = "digits/6.gif";
    imgArray[7] = new Image(60,120);
    imgArray[7].src = "digits/7.gif";
    imgArray[8] = new Image(60,120);
    imgArray[8].src = "digits/8.gif";
    imgArray[9] = new Image(60,120);
    imgArray[9].src = "digits/9.gif";
}
   
var nextYear = new Date( ).getYear( ) + 1;
nextYear += (nextYear < 1900) ? 1900 : 0;
var targetDate = new Date(nextYear,0,1);
var targetInMS = targetDate.getTime( );
var oneSec = 1000;
var oneMin = 60 * oneSec;
var oneHr = 60 * oneMin;
var oneDay = 24 * oneHr;
   
function countDown( ) {
    var nowInMS = new Date( ).getTime( );
    var diff = targetInMS - nowInMS;
    var scratchPad = diff / oneDay;
    var daysLeft = Math.floor(scratchPad);
    // hours left
    diff -= (daysLeft * oneDay);
    scratchPad = diff / oneHr;
    var hrsLeft = Math.floor(scratchPad);
    // minutes left
    diff -= (hrsLeft * oneHr);
    scratchPad = diff / oneMin;
    var minsLeft = Math.floor(scratchPad);
    // seconds left
    diff -= (minsLeft * oneMin);
    scratchPad = diff / oneSec;
    var secsLeft = Math.floor(scratchPad);
    // now adjust images
    setImages(daysLeft, hrsLeft, minsLeft, secsLeft);
}
   
function setImages(days, hrs, mins, secs) {
    var i;
    days = formatNum(days, 3); 
    for (i = 0; i < days.length; i++) {
        document.images["days" + i].src = imgArray[parseInt(days.charAt(i))].src;
    }
    hrs = formatNum(hrs, 2);
    for (i = 0; i < hrs.length; i++) {
        document.images["hours" + i].src = imgArray[parseInt(hrs.charAt(i))].src;
    }
    mins = formatNum(mins, 2);
    for (i = 0; i < mins.length; i++) {
        document.images["minutes" + i].src = imgArray[parseInt(mins.charAt(i))].src;
    }
    secs = formatNum(secs, 2);
    for (i = 0; i < secs.length; i++) {
        document.images["seconds" + i].src = imgArray[parseInt(secs.charAt(i))].src;
    }
}
   
function formatNum(num, len) {
    var numStr = "" + num;
    while (numStr.length < len) {
        numStr = "0" + numStr;
    }
    return numStr
}
   
</script>
</head>
<body style="margin-left:10%; margin-right:10%" 
<h1>New Year's Count-Down Timer</h1>
<hr /> 
   
<table cellspacing="5" cellpadding="5">
<tr>
    <td align="right">
        <img name="days0" src="digits/0.gif" height="120" width="60" alt="days">
        <img name="days1" src="digits/0.gif" height="120" width="60" alt="days">
        <img name="days2" src="digits/0.gif" height="120" width="60" alt="days">
    </td>
    <td align="left">
        <img src="digits/days.gif" height="120" width="260" alt="days">
    </td>
</tr>
<tr>
    <td align="right">
        <img name="hours0" src="digits/0.gif" height="120" width="60" alt="hours">
        <img name="hours1" src="digits/0.gif" height="120" width="60" alt="hours">
    </td>
    <td align="left">
        <img src="digits/hours.gif" height="120" width="360" alt="hours">
    </td>
</tr>
<tr>
    <td align="right">
        <img name="minutes0" src="digits/0.gif" height="120" width="60" alt="minutes">
        <img name="minutes1" src="digits/0.gif" height="120" width="60" alt="minutes">
    </td>
    <td align="left">
        <img src="digits/minutes.gif" height="120" width="450" alt="minutes">
    </td>
</tr>
<tr>
    <td align="right">
        <img name="seconds0" src="digits/0.gif" height="120" width="60" alt="seconds">
        <img name="seconds1" src="digits/0.gif" height="120" width="60" alt="seconds">
    </td>
    <td align="left">
        <img src="digits/seconds.gif" height="120" width="460" alt="seconds">
    </td>
</tr>
</table>
</body>
</html>

For this example, the target date of the countdown timer is the start of the next year in the user's local time zone. The HTML output display is a rudimentary table with place holders for the digit images. Because this application works with browsers that may experience CSS formatting problems, we use the old-fashioned, but still supported, align attribute in table cells and name attribute in img elements. The table is delivered to the browser with all of the swappable images set to the zero digit representation.

The script portion of the page begins by precaching the images that represents the numbers to prevent any delay the first time they are needed. Ten images, each the same height and width, are loaded into the browser's image cache while the page loads.

Several functions will execute repeatedly, and they benefit from the one-time predefinition of constants as global variables. Because this part of the code can run in all but first-generation browsers, it includes a fix for a Y2K problem with the getYear( ) method of the Date object (getFullYear( ) debuted in Version 4 browsers).

The function that executes repeatedly, countDown( ), performs the date math by comparing the current clock setting (read approximately every second) against the target date. Invoked by way of the window.setInterval( ) method, the countDown( ) function executes repeatedly once every second (plus or minus system latency).

Next is the setImages( ) function, which adjusts the swappable img element src properties. This function must convert the numeric values passed from countDown( ) into the URLs for the images. Calling formatNum( ) for each countdown component returns a string version of the number, including a leading zero where necessary.

This application is a good demonstration of a situation where it makes sense to use setInterval( ) to drive the repeated action. While setTimeout( ) is intended for a single invocation of a function after some delay, setInterval( ) automatically invokes a function repeatedly. If needed for older browser support (pre-dating the availability of setInterval( ) in Version 4 browsers), you can simulate the behavior by recursively invoking the countDown( ) function from a setTimeout( ) method in the last statement of the function:

setTimeout("countDown( )", 1000);

As with the timed autoscrolling of the page in Recipe 15.5, the repeated calls to the countdown timer can also exhibit less than smooth running. If you watch this code run for several seconds, you will notice an occasional and random unevenness to the flipping of the seconds. You can improve the smoothness by shortening the interval delay (to 100, for example), but this means that the function is being invoked 10 times per second, which could impact other script processing in your page. You can experiment with different settings to achieve the balance that feels best for your application.

So far, we have focused on the user's local time zone, but there may be other cases in which you want to peg the target date and time to a unique event occurring someplace on the planet, such as a corporate press conference announcing a new product release. To keep the timing accurate, you need to perform some additional calculations to get the time zone issues under control, so that users from Australia to Greenland will see the exact same countdown times leading up to the singular event.

The coding for this is simple, but the timekeeping may not be. To peg the target time and date to a universal point in the future, change the global variable declarations shown in Example 15-3 from this:

var targetDate = new Date(nextYear,0,1);
var targetInMS = targetDate.getTime( );

to this:

var targetInMS = Date.UTC(nextYear, 0, 1, 0, 0, 0);

This is for the stroke of midnight on New Year's Eve at Greenwich Mean Time (GMT). If you have another date and time, simply plug the values into the six parameters in the order of four-digit year, month (zero-based), date, hour, minute, and second, but at Greenwich Mean Time. For example, if you plan to offer a web cast of a recital at 8 P.M. in Los Angeles on March 10, 2003, you need to determine what that time is in GMT so you can provide a countdown timer on the announcement page. Los Angeles is in the Pacific Time Zone, and in that part of the year, the zone is Pacific Standard Time. The PST zone is 8 hours earlier than GMT. When it's 8 P.M. in Los Angeles, it's 4 A.M. the next day at GMT. Thus, you'd set the target time for 4 A.M. on the 11th of March (month index is 2) like this:

var targetInMS = Date.UTC(2003, 2, 11, 4, 0, 0);

There is an even easier way to figure out this GMT stuff. Set your system clock and time zone to the local time of where the event is to occur (you may already be there). Then run this little calculator page to determine the GMT date and time:

<html>
<head>
<script type="text/javascript">
function calcGMT(form) {
    var date = new Date(form.year.value, form.month.value, form.day.value, 
                        form.hour.value, form.minute.value, form.second.value);
    form.output.value = date.toUTCString( );
}
</script>
</head>
<body>
<form>
Year:<input type="text" name="year" value="0000"><br>
Month (0-11):<input type="text" name="month" value="0"><br>
Day (1-31):<input type="text" name="day" value="0"><br>
Hour (0-23):<input type="text" name="hour" value="0"><br>
Minute (0-59):<input type="text" name="minute" value="0"><br>
Second (0-59):<input type="text" name="second" value="0"><br>
<input type="button" value="Get GMT" onclick="calcGMT(this.form)"><br>
<input type="text" name="output" size="60">
</body>
</html>

If you change your system clock settings to use this calculator, be sure to change it back to your local time and time zone.

Another kind of counter is a short-term counter for applications like student quizzes or other controlled environments in which you need something to time out or navigate to another page. It's not uncommon in such cases to display, say, a 60-second counter.

The following code is a modified version of the script in Example 15-3 that displays a countdown timer for the number of seconds passed as a parameter to the primary function:

// global variables
var targetInMS, timerInterval;
var oneSec = 1000;
   
// pass the number of seconds to count down
function startTimer(secs) {
    var targetTime = new Date( );
    targetTime.setSeconds(targetTime.getSeconds( ) + secs);
    targetInMS = targetTime.getTime( );
    // display starting image
    setImages(secs);
    timerInterval = setInterval("countDown( )", 100);
}
   
function countDown( ) {
    var nowInMS = (new Date( )).getTime( );
    var diff = targetInMS - nowInMS;
    if (diff <= 0) {
        clearInterval(timerInterval);
        alert("Time is up!");
        // more processing here
    } else {
        var scratchPad = diff / oneSec;
        var secsLeft = Math.floor(scratchPad);    
        setImages(secsLeft);
    }
}
   
function setImages(secs) {
    var i;
    secs = formatNum(secs, 2);
    for (i = 0; i < secs.length; i++) {
        document.images["seconds" + i].src = imgArray[parseInt(secs.charAt(i))].src;
    }
}
   
function formatNum(num, len) {
    var numStr = "" + num;
    while (numStr.length < len) {
        numStr = "0" + numStr;
    }
    return numStr
}

This code is easily modifiable to extend the timer to minutes and seconds if you like.

15.8.4. See Also

Recipe 15.7 for displaying the number of days until Christmas; Recipe 2.9 and Recipe 2.10 for the Date object and its methods.