reptile7's JavaScript blog
Wednesday, December 21, 2011
 
Introduction to the Lightbox Image Viewer
Blog Entry #236

In today's post we will take up HTML Goodies' "Web Developer Class: How To Use the JavaScript Lightbox Image Viewer" tutorial, which was authored by Scott Clark. The lightbox image viewer spotlighted by the tutorial was created by Lokesh Dhakar.

The lightbox image viewer is what we might have called a layer-based application back in the day. In response to a user action - e.g., clicking a link or thumbnail image - the lightbox image viewer displays a viewport-centered image on a page-covering overlay. The overlay and the image it surrounds seem to be above the page because of the overlay's translucent background, and they are in fact removed from the underlying document's normal rendering flow by virtue of their absolute positioning although they are still very much part of that document.

The tutorial formerly demonstrated the lightbox image viewer in a 600px-by-500px iframe just below its first paragraph; an incomplete version of the iframe's isolated src page is archived here. I must say, those were some handsome snakes in those photos!
December 2016 Update: As noted in Part 7 of this series, the tutorial demo and the ReptileClan.com site that hosted it are no longer on the live Web; as it happens, however, I downloaded those snake photos way back when, and I plan to restage the demo in a future post.

A practical example: FreeCovers.net, an archive of CD cover art, uses the lightbox image viewer for its "zoom" feature. Can you pick out Phil Collins in this photo?

Perhaps I should give you my own demo before we get rolling. Click on the thumbnail photo of Reggie the alligator:

Reggie the alligator

The lightbox/ package

The tutorial links to a downloadable lightbox/ .zip package that contains most of what you'll need to assemble the lightbox image viewer. The lightbox/ package holds five files:
(1) A lightbox.js script codes the lightbox image viewer's structure, its behavior, and some of its styles; those styles are supplemented by
(2) a lightbox.css style sheet that specifies additional styles.
(3) An 80%-opacity overlay.png image is tiled to form the overlay background.
(4) A close.gif image serves as a close button for the lightbox image viewer.
(5) The lightbox image viewer briefly displays a loading.gif animated gif while it fetches its main image.

At the top of the lightbox.js script is some metadata in which the author requests that we leave my name and link in place. Actually, there are two Lokesh Dhakar-related links in the metadata and both of them are outdated. I'm sure that Lokesh would want us to update those links:
(1) Lokesh Dhakar's Web site is no longer at http://www.huddletogether.com, it's at http://www.lokeshdhakar.com/.
(2) For more information on the script, don't visit http://huddletogether.com/projects/lightbox/, visit http://lokeshdhakar.com/projects/lightbox/.

Code-wise, the only thing missing in the package is an HTML template - let's call it lightbox.html - for providing an interface to the user; that's where the tutorial comes in...

The HTML interface

The tutorial offers two interfaces for the lightbox image viewer: a thumbnail image interface and a link interface. In both cases you will need a parent anchor element
(a) whose href attribute points to the lightbox image and
(b) with a rel="lightbox" attribute.
Optionally, (c) equipping the anchor element with a title attribute will allow you to place a caption below the lightbox image's lower-left-hand corner.

<a href="myPhoto.jpg" rel="lightbox" title="Caption for myPhoto.jpg"> ...Thumbnail image or text... </a>

The rel="lightbox" attribute does not specify a recognized link type; rather, the lightbox rel value will later serve as an identifier, a purpose for which the author should have used a class="linkClass" attribute.

Techrtr in the tutorial comment thread brings up the issue of thumbnail image creation:
I assume you have to create the thumbnail but none of the instructions I've seen that describe how to use Lightbox mention creating it.
Sure enough, for both of the tutorial's thumbnail image examples, the anchor href attribute and the img src attribute point to different images. This is totally unnecessary: per the Offering One Image section of HTML Goodies' "So You Want A Thumb-Nail Image, Huh?" tutorial, we can load the main images into the thumbnail img placeholders if the latter are equipped with suitably scaled width and height attributes*, e.g.:

<a href="punk_the_burmese.jpg" rel="lightbox" title="Baby the Albino Burmese Python">
<img width="100" height="66" src="punk_the_burmese.jpg" alt="" />
</a>


*It's odd that Scott didn't do this given that all four of the baby_full_belly/punk_the_burmese photos have a 1.5:1 width/height ratio; the main photos scale down perfectly. The examples on Lokesh's "Lightbox JS" page also feature different main/thumbnail photos; adding insult to injury, the "handstand incident" thumbnail photo links to the "Lokesh, Jess, Brian, and Steph" main photo and vice versa.

Importing lightbox.css and lightbox.js

An HTML document can import an external style sheet via either the link element or the style element; Lokesh and Scott choose the former:

<link rel="stylesheet" href="lightbox.css" type="text/css" media="screen" />

• The rel="stylesheet" attribute and the absence of a title attribute stipulate that lightbox.css is a persistent style sheet whose rules "must apply" unless the user disables it.
• The media="screen" attribute is unnecessary as screen is the default value of the media attribute.
• The equivalent style element formulation would be:
<style type="text/css">@import "lightbox.css";</style>

We will discuss the lightbox.css rules when we work through the lightbox.js showLightbox( ) function.

To the best of my knowledge, an external script can only be imported via the script element:

<script type="text/javascript" src="lightbox.js"></script>

Pre-load

Before lightbox.html loads, the lightbox.js script variabilizes the loading.gif and close.gif file names and calls an addLoadEvent( ) function:

var loadingImage = "loading.gif";
var closeButton = "close.gif";
...
addLoadEvent(initLightbox);


(In the aforementioned lightbox.js metadata, Lokesh lists all of the lightbox.js functions and identifies the lightbox.js function, addLoadEvent( ), whose top-level call sets up the rest of the script action. If only more authors did this!)

Here's the addLoadEvent( ) function:

function addLoadEvent(func) {
    var oldonload = window.onload;
    if (typeof window.onload != "function") {
        window.onload = func; }
    else {
        window.onload = function( ) {
            oldonload( );
            func( ); } } }


The above function is the original version** of the "Executing JavaScript on page load" addLoadEvent( ) function that was crafted by Simon Willison and is highlighted by HTML Goodies' "Using Multiple JavaScript Onload Functions" tutorial. In brief, the addLoadEvent( ) function is a cross-browser, IE 5 for Mac-accommodating way to assign a function reference - in our case, a reference to an initLightbox( ) function - to the onload property (event handler) of the window object without overwriting a previous window.onload = some_other_function; assignment.

**The current addLoadEvent( ) conditionalizes the oldonload( ) call in the else clause:

if (oldonload) { oldonload( ); }

this quashes a runtime error when using IE 7 according to the Update 28th May 2006 on the "Executing JavaScript on page load" page.

(BTW, if you were to also equip the body element with an inline onload attribute -
<body onload="yet_another_function( );"> -
then that attribute will overwrite the addLoadEvent( ) window.onload assignment, so don't do that.)

The state-of-the-art way to do this sort of thing is via the addEventListener( ) method

window.addEventListener("load", some_other_function, false);
window.addEventListener("load", initLightbox, false);


whose IE support began with IE 9.

Of course, if initLightbox( ) is the only function that is 'listening' for window load events, then the addLoadEvent( ) function is superfluous and a simple window.onload = initLightbox; statement is all we need.

Load

We're ready to put the initLightbox( ) function under the microscope, and that could take a while, so let's save it for the next entry.

reptile7

Sunday, December 11, 2011
 
Divising a New Content Rotator
Blog Entry #235

In today's post I'll tell you how I would code the content rotator of HTML Goodies' "Build Your Own JavaScript Content Rotator" tutorial.

The div thing

As detailed in Blog Entry #233, the original content rotator employs a table-inside-a-table approach to laying out the rotator's frame and content - I wouldn't call this a 'complicated' approach but it is more involved than it needs to be. I for my part will use a corresponding div structure to lay out the rotator.

#divOuter { width: 410px; height: 600px; background-color: black; }
#divTitle { height: 50px; text-align: center; color: #ffff66; font-weight: bold; font-size: 24pt; line-height: 1.6; }
#divContent { width: 310px; height: 500px; background-color: white; position: relative; left: 50px; }
#divCaption { font-size: 16pt; font-weight: bold; }
#slideImage { width: 300px; height: 300px; border: 1px solid; }
#divCaption, #divImage, #divDescription { position: relative; top: 50px; }
#divCaption, #divImage { text-align: center; }
#divContent, #divCaption, #divDescription { padding: 4px; }
...
<div id="divOuter">
<div id="divTitle">My Content Slide Show</div>
<div id="divContent">
<div id="divCaption">Grasslands</div>
<div id="divImage"><img id="slideImage" src="image1.jpg" alt="" /></div>
<div id="divDescription">This is some text about the Grasslands image above. This description can be as long as you want it to be.</div>
</div></div>


(1) A divOuter div handles the rotator's frame.

(2) A divTitle div contains the "My Content Slide Show" title at the top of the display. The line-height:1.6; style declaration serves to vertically center the title in the div (unlike vertical-align, line-height can be applied to block-level elements).

(3) A divContent div plays the role of the outer frame table's central cell; it is horizontally centered in the divOuter div by a position:relative;left:50px; positioning.

(4-6) A slide's caption, image, and description are respectively held by divCaption, divImage, and divDescription divs, which collectively are approximately centered vertically in the divContent div by a position:relative;top:50px; positioning*. Like the display title, the caption and image are horizontally centered in their divs by a text-align:center; style. Lastly, the slide parts are slightly pushed apart from each other and from the frame by a padding:4px; style.

*Applying display:table-cell;vertical-align:middle; to the divContent div will also vertically center the caption/image/description content; however, I find that the Mozilla browsers (Firefox, Camino, Netscape 9) will not relatively position a display:table-cell; element. Moreover, display:table-cell; is not supported by pre-IE 8 versions of Internet Explorer.

A set of six simple divs: that's all we need, folks. Ah, but what about the Tree Canopy and Mountain Clouds slides? Does the rotator HTML really need to include them? This is something I see as an accessibility issue...

Accessibility

Regarding the original code, users without JavaScript support have no access to the contentArea2 and contentArea3 slides, which is not such a big deal if those slides show photos of trees and clouds but would clearly not be a good thing if we were to use the slides for complex layouts of product information as suggested by the Conclusion on the tutorial's second page. Do we want all of the slides to be visible simultaneously for these users? This can be done - the contentArea2/contentArea3 display:none; settings can be moved to a script for users with JavaScript support - and might be OK if we were dealing with smaller slides, although it would run counter to the space-saving solution aspect of the rotator touted by the author in the tutorial's Introduction to the JavaScript Content Rotator section.

Alternatively, we could supplement the rotator with links that open the second and third slides in new windows

<p><a href="slide2.html" target="_blank">Click here to see Slide 2 in a new window.</a></p>
<p><a href="slide3.html" target="_blank">Click here to see Slide 3 in a new window.</a></p>


and if we do that, and that's what I think we should do, then there is no point in coding all three slides in the rotator HTML, whether we use a table- or div-based structure.

Script retool

If the second and third slides are externalized for users without JavaScript support, then the main document still needs to be able to access the caption/image/description data for those slides so that JavaScript-enabled browsers can generate the slides on the fly; this data can be placed in the JavaScript part of the rotator code. We can use arrays to index the data for use in the rotator loop:

var captionArray = ["Grasslands", "Tree Canopy", "Mountain Clouds"];

var imageArray = new Array(3);
for (i = 0; i < 3; i++) {
    imageArray[i] = new Image( );
    imageArray[i].src = "image" + (i + 1) + ".jpg"; }

var descriptionArray = ["This is some text about the Grasslands image above. This description can be as long as you want it to be.",
    "This is some text about the Tree Canopy image above. This description can be as long as you want it to be.",
    "This is some text about the Mountain Clouds image above. This description can be as long as you want it to be."];


In each rotator iteration,
(1) a captionArray string will be written to the divCaption div (or captionCell td, if you want to stick with a table-based layout),
(2) an imageArray image will be loaded into the slideImage img placeholder, and
(3) a descriptionArray string will be written to the divDescription div (or descriptionCell td).
These operations can be carried out automatedly via the classical animation code presented in Blog Entry #232, which lends itself very nicely to the present situation:

var slideNum = 0;
function rotateContent( ) {
    slideNum++;
    document.getElementById("divCaption").innerHTML = captionArray[slideNum];
    document.getElementById("slideImage").src = imageArray[slideNum].src;
    document.getElementById("divDescription").innerHTML = descriptionArray[slideNum];
    if (slideNum == 2) slideNum = -1; }


The above rotateContent( ) function does not contain a recursive function call; per my preference, the next section's demo uses a setInterval( )/clearInterval( ) mechanism for starting and stopping the rotator.

var timer1;
window.onload = function ( ) {
    timer1 = window.setInterval("rotateContent( );", changeDelay);
    document.getElementById("divContent").onmouseout = function ( ) { timer1 = window.setInterval("rotateContent( );", changeDelay); }
    document.getElementById("divContent").onmouseover = function ( ) { window.clearInterval(timer1); } }


Demo

The demo below is based on the code of the preceding sections.

My Content Slide Show
Grasslands
This is some text about the Grasslands image above. This description can be as long as you want it to be.
 
Click here to see Slide 2 in a new window.
 
Click here to see Slide 3 in a new window.
 
The demo links point to the slides below:

Slide 2

My Content Slide Show
Tree Canopy
This is some text about the Tree Canopy image above. This description can be as long as you want it to be.

Slide 3

My Content Slide Show
Mountain Clouds
This is some text about the Mountain Clouds image above. This description can be as long as you want it to be.

Next up in the Beyond HTML : JavaScript sector is "Object Oriented JavaScript Class for Developers", which discusses what JavaScript does and does not have in common with more advanced programming languages and is not suitable for a post. The sector tutorial after that, "How To Use the JavaScript Lightbox Image Viewer", showcases an effect that is less interesting than the code that produces it, but going through that code will give us a good workout and we'll get stuck into it in the following entry.

reptile7

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


Powered by Blogger

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