reptile7's JavaScript blog
Friday, December 02, 2011
 
The Content Cycle
Blog Entry #234

We continue today our discussion of HTML Goodies' "Build Your Own JavaScript Content Rotator" tutorial. In this post we turn our attention to the JavaScript part of the tutorial code. The tutorial JavaScript carries out the content rotator animation in a fairly conventional manner; in brief:
(1) In each animation iteration the new slide is turned on and the old slide is turned off via an index that tracks the animation.
(2) The animation goes from one iteration to the next via a window.setTimeout( )-delayed recursive function call.
On the downside, the author's approach to starting and stopping the animation is more involved than it needs to be and gives rise to a setTimeout( )-related complication that we should and will get sorted out.

Pre-animation

Five top-level variables are declared before the animation action gets under way:

var contentIndex = 1;
var contentAreas = 3;
var changeDelay = 2000;
var continueRotation = 0;
var rotationInProgress = 0;


(1) contentIndex, the aforementioned index, maps onto the slide currently on display; it is initialized to 1 and will proceed in a 1, 2, 3, 1, 2, 3, ... cycle. Recall that the slide table elements have ordinalized ids: contentArea1, contentArea2, and contentArea3.

(2) contentAreas is set to the total number of animation slides - currently 3 but increasable if we want to add more slides.

(3) changeDelay holds the delay for the setTimeout( ) command - a 2000-ms display time for each slide seems about right to me.

The other two variables are less straightforward:

(4) The tutorial's description of continueRotation as a flag that tells us when content rotation is on and off is at best half right. Initialized to 0, continueRotation is toggled to 1 when we start the animation and is kept at 1 as long as we want the animation to keep going. If we decide to stop the animation, then continueRotation is returned to 0 at the time we make that decision; a continueRotation value of 0 does not mean that the animation has well and truly ground to a halt, it merely means that we want it to do so.

(5) The tutorial's description of rotationInProgress - The rotationInProgress variable keeps track of when we are within the delay window after setTimeout( ) is used and before the rotateContent( ) function is actually executed [i.e., before the next slide is displayed] - is also misleading. Initialized to 0, rotationInProgress is toggled to 1 when we start the animation and is kept at 1 as long as the animation continues. If we decide to stop the animation, then rotationInProgress is returned to 0 at the time the animation stops, and thus there is a time gap (as long as two seconds) between setting continueRotation to 0 and setting rotationInProgress to 0. It's true that rotationInProgress is 1 during the setTimeout( ) delay but it is not switched to 0 when the delay is over.

In sum, the tutorial's continueRotation description really applies to rotationInProgress. We'll see later that neither of these variables is necessary to track the state of the content rotator.

Animation

The first frame

The first animation frame displays the contentArea1 slide table holding the Grasslands caption, image, and description as specified by the tutorial code's HTML, which we went through in the previous post.

When the page loads, the animation is set in motion by calling a rotationStart( ) function:

function rotationStart( ) { ... }
...
<body onload="javascript:rotationStart( );">
<!-- Is the use of a JavaScript URL necessary here? Nope. -->


The rotationStart( ) function first toggles the continueRotation flag to 1

continueRotation = 1;

and then
(a) toggles the rotationInProgress flag to 1 and
(b) calls a rotateContent( ) function after a two-second delay
if rotationInProgress is 0, which it is:

if (rotationInProgress == 0) {
    rotationInProgress = 1;
    window.setTimeout("rotateContent( );", changeDelay); }


We'll discuss the role of the rotationInProgress == 0 test in the Delay mischief section at the end of the post.

Subsequent frames

Subsequent animation frames are handled by the rotateContent( ) function, which begins by checking if continueRotation is 1:

function rotateContent( ) {
    if (continueRotation == 1) {
        ... }
    else { rotationInProgress = 0; } }


Per the preceding Pre-animation section, the if condition effectively asks, "Do we want the animation to continue?" That we do, and we're ready to change slides. In the first animation frame, contentIndex, 1, maps onto the contentArea1 slide. The if (continueRotation == 1) clause first turns off the contentArea1 slide by concatenating a contentArea string with contentIndex

var contentAreaID = "contentArea" + contentIndex;

and then plugging the resulting contentAreaID string into a command that gets the contentArea1 slide table element and sets its CSS display property to none:

document.getElementById(contentAreaID).setAttribute("style", "display: none;");

In the second animation frame, contentIndex will map onto the contentArea2 slide holding the Tree Canopy caption/image/description. Accordingly, contentIndex is next incremented to 2 via the else clause of the following conditional:

if (contentIndex == contentAreas) { contentIndex = 1; }
else { contentIndex = contentIndex + 1; }


Subsequently the contentArea2 slide is turned on by setting its display to inline:

contentAreaID = "contentArea" + contentIndex;
document.getElementById(contentAreaID).setAttribute("style", "display: inline;");

/* Depending on what styles have been set for the slide table elements and their td element parent, the display can also be set to inline-table or to block. */

John H. in the tutorial comment thread correctly points out that setAttribute( ) is an IE-unfriendly method in the present context and that corresponding style.display commands should be used instead:

document.getElementById(contentAreaID).style.display = "none";
...
document.getElementById(contentAreaID).style.display = "inline";


The if (continueRotation == 1) clause lastly schedules another call to the rotateContent( ) function and unnecessarily resets rotationInProgress to 1:

window.setTimeout("rotateContent( );", changeDelay);
rotationInProgress = 1;


In the next rotateContent( ) run, the contentArea2 slide is turned off, contentIndex is incremented to 3, and the Mountain Clouds contentArea3 slide is turned on as detailed above; upon re-calling rotateContent( ), the contentArea3 slide is turned off, contentIndex is reset to 1 by the if (contentIndex == contentAreas) contentIndex = 1; statement, and the Grasslands contentArea1 slide is turned on; and so on.

Stopping and restarting the animation

The animation can be stopped by mousing anywhere over the rotator's content area, more specifically, by mousing over the outer frame table's central cell, whose onmouseover attribute then calls a rotationStop( ) function that sets the continueRotation flag to 0:

function rotationStop( ) { continueRotation = 0; }
...
<td id="contentCell" style="width: 310px; height: 500px; text-align: center;" onmouseover="rotationStop( );" onmouseout="rotationStart( );">


When the rotateContent( ) function is next called, the function's if (continueRotation == 1) clause is not executed as the clause's condition now returns false; as a result, the current slide is not turned off but remains in place. The browser moves to the function's concluding else clause (vide supra), switches the rotationInProgress flag back to 0, and exits the function.

Once stopped, the animation can be restarted by mousing out from the outer frame table's central cell, whose onmouseout attribute then re-calls the rotationStart( ) function.

The event flow associated with these operations is complicated by the following factors:

The W3C notes, In the case of nested elements, mouse events are always targeted at the most deeply nested element. In mousemoving into the content area, the most deeply nested element could be a slideImage img element, an imageCell td element, a caption's strong element parentNode, a captionCell td element, a descriptionCell td element, or the contentCell td element itself, depending on where and how quickly we move the mouse cursor.

• The mouseover and mouseout events both "bubble", that is, they propagate through the ancestors of the most deeply nested element to the contentCell td element (and beyond).

• A mousemove that is entirely within an element X but that ends on a descendant element Y of element X also constitutes a mouseout event with respect to element X.

Suppose we mousemove into the contentCell space above a slide caption - a mouseover event that stops the animation - and then mousemove onto the caption itself; the latter mousemove generates up to four mouseover/mouseout events, in order:
(1) a mouseout event fired by the contentCell td element;
(2) a mouseover event fired by the caption's captionCell td element ancestor;
(3) a mouseout event fired by the caption's captionCell td element ancestor; and
(4) a mouseover event fired by the caption's strong element parent.
Events (2), (3), and (4) all bubble up to the contentCell td element. The sequence ends with a mouseover event that keeps the animation stopped, but would you predict that before the fact? It's somewhat remarkable that the script works as well as it does.

Delay mischief

As intimated above, there is a delay between a call to the rotationStop( ) function and the time that the animation actually stops. If we were to call the rotationStart( ) function during that delay,
(a) we would undo the rotationStop( ) action - the rotationStart( ) call would toggle continueRotation back to 1 before the end of the delay - and
(b) we would set up a second rotateContent( ) cycle that is staggered with respect to the original rotateContent( ) cycle,
and the resulting display could/can be pretty spastic (or not so spastic, depending on when exactly during the delay rotationStart( ) is called).

To forestall this situation, the author wrapped the rotationStart( ) function's rotateContent( ) call in an if (rotationInProgress == 0) { ... } statement - a statement that is only executed if the animation has completely stopped; he explains:
The reason this is so necessary is so that we don't stack a bunch of delayed calls to the rotateContent( ) function as the mouse moves around the content area. If you want to see what I mean remove the if statement and watch the rotator freak out as you mouse around it.
Of course, it would be better if we could stop the animation immediately and not have to wait for the delay to run its course; gratifyingly, this is easily accomplished via a window.clearTimeout( ) command:

var timer1;
function rotationStart( ) { timer1 = window.setTimeout("rotateContent( );", changeDelay); }
function rotationStop( ) { window.clearTimeout(timer1); }


The clearTimeout( ) command obviates the need for the continueRotation and rotationInProgress flags - throw them out.

I'll present an alternative coding for the content rotator and address its accessibility in the following entry.

reptile7

Comments: Post a Comment

<< Home

Powered by Blogger

Actually, reptile7's JavaScript blog is powered by Café La Llave. ;-)