reptile7's JavaScript blog
Tuesday, November 22, 2011
 
The Joys of Tables
Blog Entry #233

In today's post we'll tuck into HTML Goodies' "Web Developer Class: Build Your Own JavaScript Content Rotator" tutorial, which is authored by Curtis Dicken. The "Build Your Own JavaScript Content Rotator" tutorial is an outgrowth of the "Build Your Own Image Viewer with Scrollbar" tutorial, also authored by Mr. Dicken, that we discussed in Blog Entries #228 and #229. The latter tutorial codes an "image viewer" - more specifically, a 400px-by-400px image (placeholder) flanked vertically by a caption and a description therefor - that is manipulated by an image scrollbar; the former tutorial reframes the image viewer (sans scrollbar) and animates it:

My Content Slide Show
Grasslands
This is some text about the Grasslands image above. This caption can be as long as you want it to be.
 
The "Content Rotator" tutorial doesn't provide a demo so I thought I would give you one here and now. Per the tutorial code, the demo cycles three sets of image data. Mousing over the content area will stop the rotation so that you can take a closer look at an image and its description; mousing out from the content area will restart the rotation.

We'll begin our analysis with a look at the new frame's HTML/CSS and then we'll deconstruct the rotation JavaScript in detail - as we'll see, there's plenty of room for tightening up the code in both areas.

The new frame

At the end of the Function - rotateContent section on the tutorial's second page the author says:
Tip: This technique works [the content rotator can be structured] with other elements like div, although I prefer to always use tables and cells so that I can have my rotating content in a nicely aligned and framed area of my page.
The content rotator is housed in a three-row, three-cells-per-row table, whereas each rotation "slide" itself comprises a three-row, one-cell-per-row table. Extensive style information is specified for these tables and it is not at all difficult to convert them to a corresponding div structure; doing this would allow us to shrink the volume of frame HTML. Moreover, it's debatable as to whether the table element is semantically the right choice for structuring the content rotator anyway.

But let's stick with the table framing for the time being. The outer frame table in particular could use some work: no fewer than seven of its nine cells are purely 'spacer' elements that contain no content. For example, here's the outer table's first row:

<table border="0" cellpadding="5" cellspacing="0" style="width: 410px; height: 600px;">
<tr>
<td style="width: 50px; height: 50px; background-color: black;"></td>
<td style="width: 310px; height: 50px; background-color: black; text-align: center;">
<span style="font-size: 24pt; color: #ffff66;">My Content Slide Show</span></td>
<td style="width: 50px; height: 50px; background-color: black"></td>
</tr>


We've got a 50px-by-50px spacer square followed by a 310px-by-50px "My Content Slide Show" title cell followed by another 50px-by-50px spacer square. There's no need to use three cells when one will do:

table { border: none; }
#table1 { width: 410px; height: 600px; background-color: black; }
#table1row1, #table1row3 { height: 50px; }
#titleCell { font-size: 24pt; color: #ffff66; }
...
<table id="table1" cellpadding="5" cellspacing="0">
<tr id="table1row1">
<th id="titleCell">My Content Slide Show</th>
</tr>


Style for the content rotator tables is in all cases specified at the level of inline markup - I'll be separating it out as we go along.

A generic title is not quite a "header" in my book but more resembles a header than it does data, so I've switched the title cell from a td element to a th element; doing this will allow us to subtract the cell's text-align:center; alignment. The th markup will also bold the title, which I think is appropriate.

The outer table's contentless third row - that would be the solid black bottom part of the frame - can similarly be coded as:

<tr id="table1row3"><td></td></tr>
<!-- Even this seems kind of inefficient, doesn't it? We'll get to the div thing in due course. -->


The outer table's middle row is somewhat more interesting. The middle row comprises a central 310px-by-500px content cell flanked by 50px-by-500px spacer cells. I was originally hoping to subtract the spacer cells, keep the content cell width at 310px, and then center the content cell by giving its parent tr element a text-align:center; style; in the event, Firefox and Opera gave a centered cell once I gave the cell an inline-block display whereas Safari and Chrome gave a row-spanning (width:410px;) cell no matter what I did*. Alternatively and preferably, we can let the content cell span its parent row, not worry about centering it, and transfer its width, height, and white background color to its slide table children.

*I see that HTML 4.01's definition of the width attribute of the td/th element reads, This attribute supplies user agents with a recommended cell width [emphasis added], i.e., there's no ironclad guarantee that a user agent will give you the width you want.

The content cell holds all three rotation slides, which have id identifiers set to contentArea1, contentArea2, and contentArea3, respectively. Here are the start-tags for the content cell and the first slide:

<td style="width: 310px; height: 500px; text-align: center;" onmouseover="rotationStop( );" onmouseout="rotationStart( );">
<table id="contentArea1" border="0" cellpadding="2" cellspacing="0" style="width: 300px; display: inline; position: relative;">


• The first slide is initially given a display:inline; style - tables are normally block-level elements - that in combination with the content cell's text-align:center; style serves to horizontally center the first slide in the content cell. (This is not a large effect when the spacer cells are present: removing the display:inline; declaration shifts the slide two pixels to the left.) However, I see no point in 'inline-izing' the slide table** given that (a) there's no content on either side of the slide and (b) we can horizontally center it by setting its margin-left and margin-right properties to auto.

**There's actually a special inline-table display value for giving a table an inline-level formatting, but again, we don't need it.

• The slides have all been given a position:relative; style, which serves no purpose and should be thrown out.

In sum, my preferred CSS for the outer table's content cell and for the slide table elements is:

#contentCell { height: 500px; }
#contentArea1, #contentArea2, #contentArea3 {
width: 310px; height: 500px; background-color: white; margin-left: auto; margin-right: auto; }


The second and third slides are initially zeroed out by setting their displays to none; this can be carried out scriptically, as can the registrations of the rotationStop( ) and rotationStart( ) event listeners with the content cell:

window.onload = function ( ) {
    document.getElementById("contentArea2").style.display = "none";
    document.getElementById("contentArea3").style.display = "none";
    document.getElementById("contentCell").onmouseover = rotationStop;
    document.getElementById("contentCell").onmouseout = rotationStart; }


The slide tables

There are three parts to each slide table: a caption, an image, and a description; each part gets its own row/cell (that's how it was in the "Image Viewer" tutorial, so this part of the structure is not really "new"). For example, here's the contentArea1 table's caption markup:

<tr>
<td style="text-align: center;">
<span style="font-size: 16pt;"><strong>Grasslands</strong></span></td>
</tr>


The Grasslands caption is centered, given a 16pt font-size, and bolded; if we mark it up as a th element, as we should, then we can throw out the text-align:center; style*** and the strong element:

.captionCell { font-size: 16pt; }
...
<tr><th class="captionCell">Grasslands</th></tr>


***Was this style necessary in the first place? Recall that the outer table's contentCell cell was also given a text-align:center; alignment; as text-align is an inherited property, you might think that the td caption text would be centered as is. In practice, the contentCell cell's text-align:center; style is not passed on to the slide tables - at least it isn't with the browsers on my computer - so yes, it is necessary to explicitly center the caption text, assuming that you do want to center it.

The slide images (img placeholders) have a common width, height, and border (perversely specified in a non-shorthand way); their src URLs respectively point to the imagePath[0], imagePath[1], and imagePath[2] images of the demo for the "Build Your Own Image Scrollbar" tutorial on which the "Image Viewer" tutorial builds.

<!-- The Grasslands image row: -->
<tr><td>
<img height="300" src="http://cache4.asset-cache.net/xc/sb10069137p-001.jpg?v=1&c=IWSAsset&k=2&d=8A33AE939F2E01FF64B2B2573E90E1541C8B733DE2CC979414FD0B94817AE1EA491BA17D91A07709" style="border-right: 1px solid; border-top:
1px solid; border-left: 1px solid; border-bottom: 1px solid;" width="300" />
</td></tr>


If we download the images and rechristen them image1.jpg, image2.jpg, and image3.jpg, then we can alternatively write:

.slideImage { width: 300px; height: 300px; border: 1px solid; }
...
<tr><td class="imageCell"><img class="slideImage" src="image1.jpg" alt="" /></td></tr>


Lastly, the author wanted the description text to be left-justified and therefore gave the description cells

<tr>
<td class="descriptionCell" style="text-align: left;">This is some text about the Grasslands image above. This caption can be as long as you want it to be.</td>
</tr>


a text-align:left; style, which is redundant because (a) the align attribute of the td element has a left default value (for a left-to-right language) and (b) the description cells won't be inheriting the contentCell cell's text-align:center; style, if present (vide supra).

Vertical alignment

You may have noticed in the demo at the outset of the post that the slide tables are vertically centered in the contentCell cell: this is because the valign attribute of the td element has a middle default value.

The height of the original slide tables is about 400 pixels. Maximizing the slide table height to 500px - the contentCell cell height, towards the end of reducing the outer table's middle row to a single cell - redistributes the contentCell cell's excess vertical padding evenly among the slide table cells, thereby pushing apart the slide table cell content. The resulting display is still somewhat centered vertically but doesn't look as nice:

[The 'Grasslands' slide with its added padding when we give it a height of 500px]

(Because the outer table has a cellpadding="5" attribute, increasing the slide table height to a full 500px will add another 10px to the height of the display; you can keep the total display height at 600px by setting the slide table height to 490px.)

The caption, image, and description are all vertically centered in their new cells, although you wouldn't know that without giving the cells a border. Anyway, we can easily scrunch the slide table parts back to their original positions via the following style rules:

.captionCell { vertical-align: bottom; }
.imageCell { height: 306px; }
.descriptionCell { vertical-align: top; }


We're ready to go after the JavaScript part of the tutorial code, and we'll do that in the next entry.

reptile7

Friday, November 11, 2011
 
The Banner Loop, Part 3
Blog Entry #232

Is it necessary to go through the create-your-own-object rigmarole in assembling the banner animation of HTML Goodies' "Accessible JavaScript 101: Rotating Banners" tutorial? Not at all, folks...

A classical approach

The introduction for Blog Entry #230 briefly noted that we had previously coded an image-cycle animation in Blog Entry #147. Can we adapt the code for that animation to the "Rotating Banners" animation? You betcha:

Affiliate Links

Search Web with GoogleSearch Web with Yahoo!Search Web with MSN
 
The animation part (vis-à-vis the accessibility part, vide infra) of the above demo requires only one image-link placeholder: <div id="banners"> <a id="bannerLink" href="http://www.google.com"> <img id="bannerImage" src="googlelogo.png" alt="Search Web with Google" onload="window.setTimeout('animate( );', delay);" /> </a> </div> The banner images are initially accessed via the following preloading code: var delay = 1000; var sponsor = ["google", "yahoo", "msn"]; var theImages = new Array( ); for (i = 0; i < sponsor.length; i++) {     theImages[i] = new Image( );     theImages[i].src = sponsor[i] + "logo.png"; } As shown, the images can be preloaded automatedly if we pre-array the sponsor parts of their file names. (In practice for the preceding demo, the theImages[i].srcs are set discretely and not automatedly as the logo images have irregular URLs.) The animation's first frame displays the Google banner and is set by the above HTML. For subsequent frames, an imgObject.onload-triggered animate( ) function uses an imageNum index to write the bannerImage img's src and alt values and the bannerLink link's href value. var imageNum = 0; var altData = ["Google", "Yahoo!", "MSN"]; function animate( ) {     imageNum++;     document.getElementById("bannerImage").src = theImages[imageNum].src;     document.getElementById("bannerImage").alt = "Search Web with" + altData[imageNum];     document.getElementById("bannerLink").href = "http://www." + sponsor[imageNum] + ".com";     if (imageNum == 2) imageNum = -1; } The Blog Entry #147 animation is based on a Netscape classical JavaScript animation example that dates to the JavaScript 1.1 Guide/Reference. The link to the Netscape example in Blog Entry #147 is dead has recently been updated and now points to an archived copy of the original page; as far as I know, the Nihonsoft guys no longer host any of the JavaScript specifications. The DOM sector of the Mozilla Developer Network's Web site does not feature the Netscape example nor does it have anything to say more generally about the use and application of Image( ) constructors (OK, there is one small Image( ) example on this page), but maybe that'll change in the future given that the W3C has brought the Image( ) constructor into HTML5. Accessibility If you'd like all of the banners to be visible for users without JavaScript support, just follow these simple steps: (1) Add/append the other two image-links to the banners div container. ... <a href="http://www.yahoo.com"><img src="yahoologo.png" alt="Search Web with Yahoo!" /></a> <a href="http://www.msn.com"><img src="msnlogo.png" alt="Search Web with MSN"></a> </div> (2) To mimic the original ul/li formatting, give the banners anchor elements a #banners a { display: block; } style. For users with JavaScript support, zero out the added Yahoo! and MSN image-links via: window.onload = function ( ) {     var bannerLinks = document.getElementById("banners").getElementsByTagName("a");     for (i = 1; i < bannerLinks.length; i++)         bannerLinks[i].style.display = "none"; }   A(nother) DOM approach "But I want a fancy shmancy DOM way to do it." Huh? Isn't the use of id identifiers and the getElementById( ) method good enough for you? We could have used name identifiers and the document.images[ ] and document.anchors[ ] collections* to access the banners, you know. (*These collections are in the HTML DOM but they are not of the DOM: they are legacy/for-backward-compatibility features that originated in JavaScript.) Very well, then. I've come up with another DOM-based approach (actually two closely related approaches, although I see them as the same approach) to the "Rotating Banners" animation that is in fact more straightforward than the foregoing classical animation approach. We saw in Blog Entry #230 that the original "Rotating Banners" code deals with the listElement unordered list and its contents on a node-by-node basis. Gratifyingly, the Core DOM Node interface features methods via which we can cycle the banners on an element-by-element basis and not have to worry about the end-of-line Text nodes that surround the banners. Suppose that in each animation iteration we could change banners by simply picking up the currently displaying banner and moving it to the end of the listElement list, thereby displaying the next banner: the appendChild( ) method will allow us to do precisely that. var listElement = document.getElementById("banners").getElementsByTagName("ul")[0]; var liElements = listElement.getElementsByTagName("li"); var firstBanner = liElements[0]; listElement.appendChild(firstBanner);

Affiliate Links

  • Search Web with Google
  • Search Web with Yahoo!
  • Search Web with MSN
Alternatively, we can 'go in reverse' if we want to, that is, we can change banners by picking up the last banner and moving it in front of the first banner: the insertBefore( ) method will do that for us. var lastBanner = liElements[2]; listElement.insertBefore(lastBanner, firstBanner); The appendChild( ) operation gives a listElement list that begins with two end-of-line characters whereas the insertBefore( ) operation gives a listElement list that ends with two end-of-line characters, which in neither case is a problem as those end-of-line characters will be ignored when the page is rendered. The insertBefore( ) banner order (Google to MSN to Yahoo!) differs from the appendChild( ) banner order (Google to Yahoo! to MSN), not that that's a big deal, of course. Accessibility Even if we give the listElement list a specific height:80px;, leaving the list's CSS overflow property at visible (the overflow value initially assigned to it by the browser) will ensure that all of the banners are visible for users without JavaScript support. For users with JavaScript support, running a for (i = 1; i < liElements.length; i++) liElements[i].style.display = "none"; loop statement in the animation's first frame will zero out the Yahoo! and MSN banners; for subsequent frames, liElements[0].style.display = "block"; liElements[liElements.length - 1].style.display = "none"; will switch on the new banner and switch off the old banner if you're cycling the banners via the appendChild( ) method whereas liElements[0].style.display = "block"; liElements[1].style.display = "none"; will do so if you're cycling the banners via the insertBefore( ) method.
We'll continue the 'rotation' theme in the following entry by taking up "Web Developer Class: Build Your Own JavaScript Content Rotator", the next Beyond HTML : JavaScript sector tutorial.

reptile7

Wednesday, November 02, 2011
 
The Banner Loop, Part 2
Blog Entry #231

We continue today our analysis of HTML Goodies' "Accessible JavaScript 101: Rotating Banners" tutorial. We are at present discussing the JavaScript part of the tutorial code. At the conclusion of our last episode, we had triggered the Rotator( ) constructor function, removed the banner listElement's end-of-line Text nodes, and zeroed out the banner listElement's second and third li element children:

function Rotator(listElement, delay) {
    for (var i = listElement.childNodes.length - 1; i >= 0; i--)
        if (!/LI/.test(listElement.childNodes[i].nodeName))
            listElement.removeChild(listElement.childNodes[i]);
    for (var i = 1; i < listElement.childNodes.length; i++)
        listElement.childNodes[i].style.display = "none";


At this point - this point counting as the first frame of the banner animation - the Google logo is all we have for a display:

[The Google logo]

The Rotator( ) function next defines a currentLI property for representing the listElement list item currently on display.

this.currentLI = listElement.firstChild;

currentLI is bound to the custom object we are constructing via the this keyword.
currentLI is initialized to the listElement's firstChild, that is, it initially points to the li element holding the Google banner; firstChild is yet another attribute of the Core DOM Node interface.

Subsequently the Rotator( ) function defines a showNext method for manipulating the currentLI property.

this.showNext = function ( ) { ... }

The showNext method name, the showNext functionality, and this, which again acts as a proxy for the custom object, are associated à la the displayCar method example appearing in the Defining Methods subsection of the "Working with Objects" chapter in the Mozilla JavaScript Guide. Note that Mozilla formulates the displayCar functionality as a named displayCar( ) function placed outside the Car( ) constructor function, but there's no reason why you can't assign an anonymous function expression to this.methodName inside the constructor, as is done in this case.

Each frame of the banner animation maps onto a run of the showNext function. Here's what the function does in the second frame:

this.currentLI.style.display = "none";

The li element holding the Google banner is zeroed out by setting its CSS display property to none.

this.currentLI = this.currentLI.nextSibling ? this.currentLI.nextSibling : this.currentLI.parentNode.firstChild;
/* nextSibling and parentNode are also Node interface attributes. */


Read about the ?: conditional operator here. The this.currentLI.nextSibling condition returns true - the li element holding the Google banner does have a nextSibling, namely, the li element holding the Yahoo! banner - and consequently this.currentLI is switched to this.currentLI.nextSibling.

this.currentLI.style.display = "block";

The li element holding the Yahoo! banner is visibilized by setting its display property to block.

In the third animation frame, the function similarly zeroes out the li element holding the Yahoo! banner and visibilizes the li element holding the MSN banner.

In the fourth animation frame, the this.currentLI.nextSibling condition returns false - the li element holding the MSN banner doesn't have a nextSibling as it's the last list item in the listElement list - and consequently this.currentLI is switched (back) to this.currentLI.parentNode.firstChild, that is, to the li element holding the Google banner.

The mechanics of the animation are now in place: all that remains for us to do is to set the animation in motion by iteratively calling the showNext method. Had we variabilized the custom object, say, with a bannerRotator identifier

window.onload = function ( ) {
    var bannerRotator = new Rotator(document.getElementById("banners").getElementsByTagName("ul")[0]); }


then starting the animation would be a simple matter of adding a

window.setInterval("bannerRotator.showNext( );", 1000);

command to the preceding function. But for whatever reason, the tutorial author did not give the custom object a name.

Is it necessary to call the showNext method outside of the Rotator( ) constructor function? Why not put the setInterval( ) command in the constructor and reference the custom object with this? The author briefly alludes to this coding possibility in a the dynamic class method cannot be passed to setInterval( ) remark. In the event, a

window.setInterval("this.showNext( );", delay);

constructor command throws a repeating "this.showNext is not a function" error. However, the source of the error is not the showNext method but rather the this reference:
Code executed by setTimeout( ) or setInterval( ) is called from a separate execution context vis-à-vis that for the function from which setTimeout( ) or setInterval( ) was called. The usual rules for setting the this keyword for the called function apply, and if you have not set this in the call or with the bind( ) method, it will default to the global (or window) object in non–strict mode, or be undefined in strict mode. It will not be the same as the this value for the function that called setTimeout( ) or setInterval( ).
In the Questions sector of stackoverflow.com, moderator SLaks offers a solution to this problem:
You need to save a reference to this in a local variable, then pass a function to [setInterval( )] that uses the variable. For example:

var bannerRotator = this;
window.setInterval(function ( ) { bannerRotator.showNext( ); }, delay);
I've tried the above code. Works beautifully. Thanks, SLaks!

The tutorial author also uses a find-another-way-to-reference-the-custom-object approach to solving the this/setInterval( ) problem - an approach that is somewhat more circuitous than SLaks's approach. Here's how it goes:

(1) Outside of the constructor function, Rotator( ) is given an instances property that is set to an empty array.

Rotator.instances = new Array( );

instances is not a property of objects created by the Rotator( ) constructor but rather a property of the Rotator( ) Function object itself.

(2) Back inside the Rotator( ) constructor, the custom object is given an id property that is set to the length of the instances array.

this.id = Rotator.instances.length;

For a first run through the Rotator( ) function, this.id/Rotator.instances.length is 0.

(3) The custom object is loaded into the instances array via the push( ) method of the Array object.

Rotator.instances.push(this);

We are now able to reference the custom object as an element of the instances array, i.e., Rotator.instances[0].

(4) Finally, the animation is started with:

window.setInterval("Rotator.instances[" + this.id + "].showNext( );", delay);

This approach works but does seem to come out of left field if you are unaware of the this/setInterval( ) problem, which gets no mention in the aforementioned Mozilla "Working with Objects" documentation.

We'll look at two other ways to code the banner animation in the following entry.

reptile7


Powered by Blogger

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