reptile7's JavaScript blog
Wednesday, January 30, 2008
 
Around the World in 80 Milliseconds, Part 1
Blog Entry #102

Digital clock scripts previously covered in HTML Goodies' JavaScript Script Tips:
(1) The Script Tips #25-28 Script (see Blog Entry #61)
(2) The Script Tips #84-86 Script (see Blog Entry #101)

So, what time is it in London? What time is it in Auckland, where the Kiwis are currently observing daylight-saving time? Script Tips #87, #88, #89, and #90 discuss a "World Clock" script that generates

hour:minute:second month date, year (e.g., 15:15:00 Jan 26, 2008)

time/date displays for London, Auckland, and eight other locations around the world, and can easily be extended to generate corresponding displays for all forty time zones. We'll spend the next several posts analyzing the Script Tips #87-90 Script, which can be accessed by following the Here's the Code links in all four script tips (but not by following the accompanying "Open Code in a New Window" links, which lead to a blank page) and is reproduced in the div below:

<script language="javascript">

<!--

var adjust = 0;
var zone = " New Orleans"; // This is the text that first appears denoting time zone
gmtOffset = 360; // Setting this to your offset will start with your current time
var PST = 480;
var EST = 300;
var TK = -540;
var HW = 600;
var LD = 0;
var MX = 360;
var HK = -480;
var FJ = -720;
var ND = -330;

function checkPST( ) {
clearTimeout(checkDateTime);
gmtOffset = eval(PST + adjust);
zone = " Pacific";
checkDateTime( );
}

function checkEST( ) {
clearTimeout(checkDateTime);
gmtOffset = EST + adjust;
zone = " Eastern";
checkDateTime( );
}

function checkTK( ) {
clearTimeout(checkDateTime);
gmtOffset = TK + adjust;
zone = " Tokyo";
checkDateTime( );
}

function checkHW( ) {
clearTimeout(checkDateTime);
gmtOffset = HW + adjust;
zone = " Hawaii";
checkDateTime( );
}

function checkLD( ) {
clearTimeout(checkDateTime);
gmtOffset = LD + adjust;
zone = " London(GMT)";
checkDateTime( );
}

function checkHK( ) {
clearTimeout(checkDateTime);
gmtOffset = HK + adjust;
zone = " Hong Kong";
checkDateTime( );
}

function checkFJ( ) {
clearTimeout(checkDateTime);
gmtOffset = FJ + adjust;
zone = " Auckland";
checkDateTime( );
}

function checkMX( ) {
clearTimeout(checkDateTime);
gmtOffset = MX + adjust;
zone = " Mexico City";
checkDateTime( );
}

function checkND( ) {
clearTimeout(checkDateTime);
gmtOffset = ND + adjust;
zone = " New Delhi";
checkDateTime( );
}

function checkDateTime( ) {

adjust = 0;

var today = new Date( );
var year = today.getFullYear( );
var month = today.getMonth( ) + 1;
var date = today.getDate( );
var day = today.getDay( );
var hour = today.getHours( );
var minute = today.getMinutes( );
var second = today.getSeconds( );

// This next tidbit gets the last Saturday in the month of Oct, for daylight-saving time purposes
var lastSat;
lastSat = date - (day + 1);
while (lastSat < 32) {
lastSat += 7;
}

if (lastSat > 31) lastSat += -7;

// This bit grabs the first Saturday in April for the start of daylight time
var firstSat;
firstSat = date - (day + 1);
while (firstSat > 0) {
firstSat += -7;
}

if (firstSat < 1) firstSat += 7;

// Adjust for Windows95 daylight-saving time changes
if ((((month == 4) && (date >= firstSat)) || month > 4) && (month < 11 || ((month == 10) && day <= lastSat))) { adjust += 60; }

yourOffset = (new Date( )).getTimezoneOffset( );
yourOffset = yourOffset + adjust;
ourDifference = eval(gmtOffset - yourOffset);
var half = eval(ourDifference % 60);
ourDifference = Math.round(ourDifference / 60);
hour = eval(hour - ourDifference);

var m = new Array("mm", "Jan.", "Feb.", "Mar.", "Apr.", "May ", "Jun.", "Jul.", "Aug.", "Sept.", "Oct.", "Nov.", "Dec.");

var leap = eval(year % 4); 

if ((half == -30) || (half == 30)) minute += 30;
if (minute > 59) minute -= 60, hour++;
if (minute < 0) minute += 60, hour--;
if (hour > 23) hour -= 24, date += 1;
if (((month == 4) || (month == 6) || (month == 9) || (month == 11)) && (date == 31))

date = 1, month += 1;
if (((month == 2) && (date > 28)) && (leap != 0)) date = 1, month += 1;
if ((month == 2) && (date > 29)) date = 1, month += 1;
if (hour < 0) hour += 24, date -= 1;
if ((date == 32) && (month == 12)) month = m[1], date = 1, year += 1;
if (date == 32) date = 1, month += 1;
if ((date < 1) && (month == 1)) month = m[12], date = 31, year -= 1;
if (date < 1) date = 31, month -= 1;
if (((month == 4) || (month == 6) || (month == 9) || (month == 11)) && (date == 31))

date = 30;
if ((month == 2) && (date > 28)) date = 29;
if (((month == 2) && (date > 28)) && (leap != 0)) date = 28;
for (i = 1; i < 13; i++) {
if (month == i) { month = m[i];
break;
}
}

// var dateTime = "" + ((hour >12) ? hour -12 :hour)
var dateTime = hour;
dateTime = ((dateTime < 10) ? "0" : "") + dateTime;
dateTime = " " + dateTime;
dateTime += ((minute < 10) ? ":0" : ":") + minute;
dateTime += ((second < 10) ? ":0" : ":") + second;
// dateTime += (hour >= 12) ? " pm," : " am, ";
dateTime += " " + month + " " + date + ", " + year;
document.clock.face.value = dateTime;
document.clock.locationx.value = zone;
setTimeout("checkDateTime( );", 900);
}

// End -->

</script>

<!-- The HTML Portion Starts Here -->

<body text="#000000" bgcolor="#000000" onload="checkDateTime( );"> 

<center><b><font color="#ffffff" size="+2">World Clock</font></b>
<form name="clock">
<center>
<input type="text" name="face" size="31" value="" /><br /><br />
<font color="#ffffff">
<b>Current Time Zone</b>
</font>
<br /><input type="text" name="locationx" size="21" value="" />
</center><br />
<table border="1" cellpadding="5"><tr>
<td><input type="button" name="reset" value="Pacific" onclick="checkPST( );" /></td>
<td><input type="button" name="reset" value="Eastern" onclick="checkEST( );" /></td>
<td><input type="button" name="reset" value="Auckland" onclick="checkFJ( );" /></td></td><td>
<td><input type="button" name="reset" value="Tokyo " onclick="checkTK( );" /></td>
<td><input type="button" name="reset" value="London" onclick="checkLD( );" /></td>
<td><input type="button" name="reset" value="Hong Kong" onclick="checkHK( );" /></td></tr><tr>
<td><input type="button" name="reset" value="Hawaii" onclick="checkHW( );" /></td>
<td><input type="button" name="reset" value="Mexico" onclick="checkMX( );" /></td>
<td><input type="button" name="reset" value="New Delhi" onclick="checkND( );" /></td>

</tr></table> 

</form>

</center>

Joe provides a demo page for the Script Tips #87-90 Script here.

The script display

Let's briefly run through the Script Tips #87-90 Script document body; we'll first consider the body's structure and then we'll replace its presentational aspects with a style sheet.

At the top of the script's display is a large "World Clock" string, which is not but should be marked up as a heading:

<h2>World Clock</h2>
<!-- Don't like skipping heading levels? Mark it up as an h1 element if you prefer. -->

The rest of the script's display is wrapped in a <form name="clock"> ... </form> container*. The clock form begins with a text field, named face (not text, as incorrectly stated in Script Tip #87), to which the above time/date string will be written. Below the face field is a "Current Time Zone" string, which serves as a label for a second text field, named locationx, that will hold a string indicating a time zone location (Pacific, Eastern, etc.) in which the time is that shown in the face field. For example, the script is set up to initially display U.S. Central Time in the face field; the corresponding locationx string is New Orleans.

*As noted in previous entries, for backward-compatibility and validation reasons you might want to:
(a) Remove the form's name="clock" attribute, which is deprecated by XHTML 1.0;
(b) Give the form an action="" attribute - the action attribute has a #REQUIRED default value designation; and then
(c) Use document.forms[0] to reference the form in the script's script element.

Because the "Current Time Zone" string is a control label, let's mark it up that way, shall we?

<label for="ctz">Current Time Zone</label><br />
<input id="ctz" name="locationx" size="21" />

The type="text" and value="" attributes of the face and locationx input elements are unnecessary and can be removed, but there's nothing 'tref' about them, either.

Below the locationx field is a set of nine push buttons that is laid out via a three-row table. Each button is labeled, via its value attribute, with a time zone location. When clicked, each button triggers functions that generate face and locationx field values appropriate for that button.

Validation note: Each button is also equipped with a name=reset attribute that never comes into play and would seem to be removable; however, the W3C's (X)HTML DTDs state (here, e.g.), [For the input element,] the name attribute is required for all but [type=]submit & reset, so perhaps we should let the button name attributes be (for XHTML-compliance, however, their values and the other unquoted attribute values should be quoted, per the div above).

There's no geographical relationship between the buttons of a given table row or column, and because the W3C holds that [t]ables should not be used purely as a means to layout document content as this may present problems when rendering to non-visual media, I thought, "Maybe we should replace the script's table with a div element container." But of course, there's nothing stopping us from grouping the buttons geographically - we could have a 'U.S.' row (Pacific, Mountain, Central, Eastern), a 'European' row (London, Berlin, Athens, Moscow), etc. - and I am now inclined to keep the table scaffolding in place.

One more point before moving on: On the "Here's the Code" page, the first table row's tr element end-tag and the second table row's tr element start-tag are mistyped as </td> and <td>, respectively; the correct </tr> and <tr> tags appear in the demo page's source, however.

And now for some style...

The script's body element is equipped with deprecated text="#000000" and bgcolor="#000000" attributes that produce black text on a black background - how's that for a color scheme, huh? On the other hand, the script document's two 'text nodes' - the "World Clock" and "Current Time Zone" strings - are wrapped in deprecated <font color="#ffffff"> ... </font> containers. Accordingly, let's remove the text/bgcolor/color attributes and begin our style sheet with:

body { background-color: black; color: white; }

The body content is centered by, curiously, two nested center elements, which are deprecated and can be replaced as follows:
(1) For centering the pre-table content:
(a) Wrap the face field, the "Current Time Zone" label, and the locationx field in a div element:

<div>
<input name="face" size="31" /><br /><br />
<label for="ctz">Current Time Zone</label><br />
<input id="ctz" name="locationx" size="21" />
</div>

(b) Give the div element and the "World Clock" h# element (vide supra) a common class="cent" attribute; and
(c) Add the following rule to the style sheet:

.cent { text-align: center; }

(It's not entirely clear whether the CSS text-align property is meant to be applicable to non-text elements, but the text-align definition - This property describes how inline content of a block is aligned - would certainly seem to apply to the input and label elements, which are inline elements.)

(2) Center the table with the following style rule set:

table { margin-left: auto; margin-right: auto; }

Style-wise, the b element/size="+2" markup that surrounds the "World Clock" string in the original script is reproduced by the h# container. To bold(en) the "Current Time Zone" label, you can add to the style sheet:

label { font-weight: bold; }

Finally, the HTML 4 Index of Attributes shows that neither the border="1" attribute nor the cellpadding="5" attribute of the table element is deprecated, but it is simple enough to replace these attributes with the style rules below:

table, td { border: 1px solid white; }
td { padding: 5px; }

Deconstruction intro

When the script document has loaded, the body element's onload="checkDateTime( );" attribute calls the large checkDateTime( ) function in the script element in the document head.

function checkDateTime( ) {
adjust = 0;

The first checkDateTime( ) statement assigns 0 to the variable adjust, which will come into play during the daylight-saving time part of the script; this statement is unnecessary because adjust is globally initialized with a value of 0 at the top of the script element:

<script type="text/javascript">
<!--
var adjust = 0;

Back to the checkDateTime( ) function - we next grab some date and time values:

var today = new Date( );
var year = today.getFullYear( );
var month = today.getMonth( ) + 1;
var date = today.getDate( );
var day = today.getDay( );
var hour = today.getHours( );
var minute = today.getMinutes( );
var second = today.getSeconds( );

(I gave you the following references in the previous post, but let me give them to you again: Mozilla discusses the Date object here and collects links to its pages for the above Date object methods here.)

As noted earlier, the Script Tips #87-90 Script initially displays U.S. Central Time in the face text box; however, the preceding block of code returns local date and time information for the user's machine, whether the user is in Seattle, Sarajevo, or Sydney. This is a good time to bring up a general caveat about clock scripts that Joe raised at the beginning of Script Tip #85: As long as the viewer has his or her browser [clock] set correctly, you're good to go. If they don't, well, you can't help that.

Next we have three blocks of code dealing with daylight-saving time. Ah, daylight-saving time...any objective reader of Wikipedia's DST entry would conclude that DST's costs are definite and its benefits are questionable...anyway, the script's DST code deserves its own entry and we'll get to it next time.

reptile7

Monday, January 21, 2008
 
The Ticking Image
Blog Entry #101

Back in Blog Entry #61, we discussed HTML Goodies' JavaScript Script Tips #25-28, whose script codes a basic digital clock:

Script Tips #84, #85, and #86 offer a script that codes a similar but fancier image-based digital clock:
 

(Through a combination of incorrect image URL paths and wrongly escaped [ and ] characters, the demos at the 'current' Script Tips #84-86 pages do not work. However, the demos at the 'legacy' Script Tips #84-86 pages (e.g., here) work fine.)

In today's entry we'll dissect the Script Tips #84-86 Script, which can be accessed by following the Here's the Code links in all three script tips and is reproduced in the div below:

<html>
<head>
<script type="text/javascript">
<!--

var d = new Array( );
for (i = 0; i < 10; i++) {
d[i] = new Image( );
d[i].src = "dgt" + i + ".gif";
}
var pm = new Image( );
pm.src = "dgtp.gif";
var am = new Image( );
am.src = "dgta.gif";
var dates, min, sec, hour;
var amPM = "am";

function clock( ) {
dates = new Date( );
hour = dates.getHours( );
min = dates.getMinutes( );
sec = dates.getSeconds( );
if (hour < 12) {
amPM = am.src;
}
if (hour > 11) {
amPM = pm.src;
hour = hour - 12;
}
if (hour == 0) {
hour = 12;
}
if (hour < 10) {
document["tensHour"].src = "dgtbl.gif";
document["Hour"].src = d[hour].src;
}
if (hour > 9) {
document["tensHour"].src = d[1].src;
document["Hour"].src = d[hour - 10].src;
}
if (min < 10) {
document["tensMin"].src = d[0].src;
}
if (min > 9) {
document["tensMin"].src = d[parseInt(min / 10, 10)].src;
}
document["Min"].src = d[min % 10].src;
if (sec < 10) {
document["tensSec"].src = d[0].src;
}
if (sec > 9) {
document["tensSec"].src = d[parseInt(sec / 10, 10)].src;
}
document["Sec"].src = d[sec % 10].src;
document["amPM"].src = amPM;
setTimeout("clock( );", 100);
}
//-->
</script>
</head>
<body bgcolor="#ffffff" onload="clock( );">
<center>

<img src="dgtbl.gif" name="tensHour" /><img src="dgtbl.gif" name="Hour" /><img src="dgtcol.gif" /><img src="dgtbl.gif" name="tensMin" /><img src="dgtbl.gif" name="Min" /><img src="dgtcol.gif" /><img src="dgtbl.gif" name="tensSec" /><img src="dgtbl.gif" name="Sec" /><img src="dgtbl.gif" /><img src="dgtbl.gif" name="amPM" />

</center>

</body></html>

The clock images

The Script Tips #84-86 Script's clock makes use of fourteen images for its display:
(1-10) Ten dgt#.gif images of the 0-9 digits for the hour:minute:second parts of the clock;
(11-12) dgta.gif and dgtp.gif images of the letters A and P for AM and PM, respectively;
(13) A dgtcol.gif image of a colon for delimiting the hour:minute:second parts of the clock; and
(14) A default, 'blank' dgtbl.gif image.
These images are collected at the beginning of Script Tip #84 and can be downloaded in the usual manner (on each image, right-click and then on the contextual menu go to Download Image to Disk or whatever on your computer). Dimensions-wise, all of these images are 16px by 21px except for the dgtcol.gif image, which is 7px by 21px.

In the script document body, the display itself comprises a 'string' of ten back-to-back img elements. In source order, and as shown above:
(1-2) The first (name="tensHour") img element displays the 'tens place' of the hour part of the clock; the second (name="Hour") img element displays the 'ones place' of the hour part of the clock.
(3) The third img element uniformly displays the clock's hour:minute colon.
(4-5) The fourth (name="tensMin") and fifth (name="Min") img elements respectively display the tens and ones places of the minute part of the clock.
(6) The sixth img element uniformly displays the clock's minute:second colon.
(7-8) The seventh (name="tensSec") and eighth (name="Sec") img elements respectively display the tens and ones places of the second part of the clock.
(9) The ninth img element uniformly displays the space between the second and AM/PM parts of the clock.
(10) The tenth (name="amPM") img element displays the AM/PM part of the clock.

Validation notes: XHTML 1.0 deprecates the name attribute of the img element. In addition, an appropriate alt attribute should be added to each img element; the img alt attribute has a #REQUIRED designation even in the HTML 4.01 Transitional DTD.

Like the Script Tips #25-28 clock, the Script Tips #84-86 clock displays a 1-to-12 hour value, a 2-digit 00-to-59 minute value, and a 2-digit 00-to-59 second value. In building the Script Tips #25-28 clock, we showed in Blog Entry #61 that we could use the normal returns of the getHours( ), getMinutes( ), and getSeconds( ) methods of the core JavaScript Date object 'as is' most of the time. In contrast, the Script Tips #84-86 clock display has at its disposal a much smaller set of 'values' (images) and thus must deal separately with the tens and ones places of its hour:minute:second parts as intimated above. (Yes, you could create a set of sixty images corresponding to the getHours( )/getMinutes( )/getSeconds( ) returns, but I for my part would rather not do that.) As a result, the Script Tips #84-86 Script is somewhat more complicated than is the Script Tips #25-28 Script, but it's nothing we can't handle and maybe improve on a bit...

Deconstruction of the Script Tips #84-86 Script

Before the script document body is rendered

<script type="text/javascript">

var d = new Array( );
for (i = 0; i < 10; i++) {
d[i] = new Image( );
d[i].src = "dgt" + i + ".gif"; }

Validation note: Scripts containing one or more < characters should be externalized for XHTML-compliance.

We initially preload the dgt#.gif images, automating the process via a for loop. We first preloaded images in HTML Goodies' JavaScript Primers #27; more relevantly, we previously used a for loop to preload images automatedly in the "Other code possibilities" section of Blog Entry #64. The dgt#.gif image URLs are assigned to a d array.

var pm = new Image( ); pm.src = "dgtp.gif";
var am = new Image( ); am.src = "dgta.gif";

These lines similarly preload the dgtp.gif and dgta.gif images, whose URLs are respectively assigned to pm.src and am.src.

Two more points before moving on:
• Joe evidently didn't find it necessary to preload the dgtcol.gif and dgtbl.gif images, although you could certainly preload them if you wanted to:

var colon = new Image( ); colon.src = "dgtcol.gif";
var blank = new Image( ); blank.src = "dgtbl.gif";

• I can confirm that (at least on my computer) the Script Tips #84-86 clock doesn't run smoothly if the image preloading code is commented out.

var dates, min, sec, hour, amPM = "am";

The variables dates, min, sec, and hour are declared but not initialized. Also, the variable amPM (not the best choice for a variable name given that the document's last img element is named amPM) is declared and given an am string value.

Loading the document body HTML

All of the document's img elements are initially loaded with the dgtbl.gif image except for the third- and sixth-in-source-order img elements, which are loaded with the dgtcol.gif image.

For validating the script document against the XHTML 1.0 Strict DTD, wrap the images in a <div id="imageString"> ... </div> container and replace the presentational markup (the body element's bgcolor attribute and the center element) with the following style rules:

body { background-color: white; }
#imageString { margin-left: auto; margin-right: auto; width: 144px; }

When the script document has loaded, the body element's onload="clock( );" attribute triggers the clock( ) function in the script's script element.

The clock( ) function

We begin by grabbing the current time:

function clock( ) {
dates = new Date( );
hour = dates.getHours( );
min = dates.getMinutes( );
sec = dates.getSeconds( );

This is code that we know like the backs of our hands at this point; Mozilla discusses the Date object on this page and the above Date object methods on this page if you could use refreshers for these guys.

if (hour < 12) amPM = am.src;
if (hour > 11) {
amPM = pm.src;
hour = hour - 12; }
if (hour == 0) hour = 12;
if (hour < 10) {
document["tensHour"].src = "dgtbl.gif";
document["Hour"].src = d[hour].src; }
if (hour > 9) {
document["tensHour"].src = d[1].src;
document["Hour"].src = d[hour - 10].src; }

A series of five if conditionals sets the hour part of the clock and lays some groundwork for setting later the AM/PM part of the clock; for now, let's focus on the clock's hour part.

The second if conditional adjusts the 12-23 hour (dates.getHours( )) values to a 0-11 range.
The third if conditional adjusts the 0 hour values (for the midnight-to-1AM and noon-to-1PM hours) to 12.
It occurred to me that the abs( ) method of the core JavaScript Math object would allow us to carry out these adjustments via one conditional:

if (hour > 12 || hour == 0) hour = Math.abs(hour - 12);

With a 1-12 hour range now in hand, we're ready to load some images into the tensHour and Hour img element placeholders for the hour part of the clock.
For the 1-9 hour values, the fourth if conditional loads the dgtbl.gif image into the tensHour placeholder and the d[hour].src (dgt+hour+.gif) image into the Hour placeholder.
Because we don't have images of the numerals 10, 11, and 12, the 10-12 hour values must for display be split into separate digits; accordingly for these values, the fifth if conditional loads the d[1].src (dgt1.gif) image into the tensHour placeholder and the d[hour-10].src (dgt0.gif, dgt1.gif, or dgt2.gif) image into the Hour placeholder.

You are probably wondering, "That document["imageName"] syntax for referencing an image...that's not standard, is it?" Indeed, it is not - before the fact, you would expect it to throw errors, but it doesn't (at least on my computer) - document.imageName or document.images["imageName"] references would be legit, but given that XHTML deprecates the img name attribute (vide supra), I encourage you to instead use ordinal document.images[#] references per my alternate code samples below.

JavaScript's* ternary ?: conditional operator allows a more concise formulation of the fourth and fifth if conditionals:

document.images[0].src = (hour < 10) ? "dgtbl.gif" : d[1].src;
document.images[1].src = (hour < 10) ? d[hour].src : d[hour - 10].src;

The ?: operator is somewhat like a simple if...else statement. The ?: syntax is:

condition ? expression1 : expression2

If the condition is true, then expression1 is evaluated; if it's false, then expression2 is evaluated. In my ?: code above, different image URLs are assigned to the src properties of the zeroth (tensHour) and first (Hour) img placeholders depending on whether hour is or is not less than 10. The parentheses surrounding the hour<10 condition(s) are actually unnecessary (they make the code a bit more readable, IMO), because the < comparison operator has a higher priority than does the ?: operator.

(*FYI: The ?: operator is not specific to JavaScript but, according to its Wikipedia entry, is part of at least twelve other programming languages.)

Let's move on to the minute part of the clock. The 0-59 min (dates.getMinutes( )) return is converted to a 00-59 display via two if conditionals that set the tens place and a subsequent statement that sets the ones place:

if (min < 10) document["tensMin"].src = d[0].src;
if (min > 9) document["tensMin"].src = d[parseInt(min / 10, 10)].src;
document["Min"].src = d[min % 10].src;

For the 0-9 min values, the first if conditional loads the d[0].src (dgt0.gif) image into the tensMin img placeholder.
The 10-59 min values must for display be split into separate digits; for these values, the second if conditional extracts the min tens-place digit via
(a) dividing min by 10 and then
(b) lopping off the .# part of the quotient with the top-level parseInt( ) function**
within the square brackets of a d[ ].src expression. The resulting d[#].src URL is assigned to document["tensMin"].src, loading the dgt1.gif, dgt2.gif, dgt3.gif, dgt4.gif, or dgt5.gif image into the tensMin placeholder. For example, if min is 49, then 49/10 gives 4.9 and parseInt(4.9) gives 4, and consequently the tensMin placeholder is loaded with the dgt4.gif image.
(**The radix=10 second parseInt( ) parameter is unnecessary and can be removed.)

I can think of two other ways to extract the min tens-place digit.
(1) Divide min by 10 and then round down via the floor( ) method of the Math object;

if (min > 9) document.images[3].src = d[Math.floor(min / 10)].src;

(2) Stringify min and then use the charAt( ) method of the core JavaScript String object as follows:

var minstring = min.toString( );
/* Mozilla discusses the toString( ) method of the core JavaScript Number object here. */
if (min > 9) document.images[3].src = d[minstring.charAt(0)].src;
/* String-to-number conversion occurs automatically in the right side of the preceding statement. */

As for the Min img element, a min%10 operation yields the correct ones-place digit for all min values. If min is 49 per the above example, then 49%10 gives 9 and consequently the Min placeholder is loaded with the d[9].src (dgt9.gif) image. (We similarly could have used an hour%10 operation in filling the Hour placeholder.) Alternatively, if min has been stringified, then we can use the charAt( ) method to extract the ones-place digit:

document.images[4].src = d[minstring.charAt(1)].src;

Other than its different identifiers, the code for setting the second part of the clock is identical to that for the clock's minute part:

if (sec < 10) document["tensSec"].src = d[0].src;
if (sec > 9) document["tensSec"].src = d[parseInt(sec / 10, 10)].src;
document["Sec"].src = d[sec % 10].src;

The clock( ) function's minute and second blocks can also be condensed via the ?: operator:

document.images[3].src = (min > 9) ? d[parseInt(min / 10)].src : d[0].src;
document.images[4].src = d[min % 10].src;
document.images[6].src = (sec > 9) ? d[parseInt(sec / 10)].src : d[0].src;
document.images[7].src = d[sec % 10].src;

We next turn to the AM/PM part of the clock, which is set with:

document["amPM"].src = amPM;

This statement loads the amPM image - dgta.gif or dgtp.gif, depending on the time of day - into the amPM img placeholder; in the clock( ) function's hour block, the am.src (dgta.gif) URL was assigned to amPM if hour<12 and the pm.src (dgtp.gif) URL was assigned to amPM if hour>11. More efficiently, we can use the ?: operator to ditch the amPM variable and put all of the AM/PM code on one command line:

document.images[9].src = (hour < 12) ? am.src : pm.src;

Finally, a setTimeout( ) command recursively calls the clock( ) function after a 100-millisecond delay, updating the clock display:

window.setTimeout("clock( );", 100);

In the next post, we'll begin a look at a more ambitious digital clock script - one that takes time zones and daylight-saving time into account - covered by Script Tips #87-90.

reptile7

Friday, January 11, 2008
 
Coloring by Elements
Blog Entry #100

We continue today our discussion of the rainbow text script of HTML Goodies' JavaScript Script Tips #81-83. We noted in the previous post that the Script Tips #81-83 Script commingles its document body HTML with some of its JavaScript, which raises a portability issue: for a separate document that, unlike the script document, has some real structure, can we apply the script and its coloring effects to specific text strings therein? Gratifyingly, with a little help from the Document Object Model (DOM), we can indeed transform the Script Tips #81-83 Script from an ad hoc generator of colored strings into a targeting color machine; in this post, we'll modify the script's ColoredText( ) function so that it adds color to text elements on the basis of
(a) id attribute value,
(b) tag name, and
(c) class attribute value.
A demo illustrating our handiwork will follow.

We'll apply our modified JavaScript to the following document body:

<body>
<h1>A sample with shades of green</h1>
<p>
<span class="rainbow">This is a text sample written using the function ColoredText.</span>
<br />
<span id="span1">Another text sample, this time only with red and orange repeatedly</span>
</p>
<h1>This is another H1 heading</h1>
<p id="p1" class="rainbow">Try refreshing the page a couple of times!</p>
</body>

setColorsById( )

We begin with the one-off coloring of a unique element. Consider the string Another text sample, this time only with red and orange repeatedly, whose characters are alternately colored red and orange by the Script Tips #81-83 Script and which my sample document wraps in a <span id="span1"> ... </span> container. This string can now be accessed and overwritten by the ColoredText( ) function via

document.getElementById("span1").innerHTML

expressions. (As noted in Blog Entry #70, innerHTML is not a standard (W3C-approved) DOM property. The DOM Level 3 Core Specification introduces a textContent property that is similar to innerHTML and that I would use below if the browsers on my computer supported it, which they do not.)

By combining the document.getElementById("span1").innerHTML expressions with parts of the Script Tips #81-83 Script and with some statements analogous to those appearing in the "To return or not to return" section of the previous post, I came up with the following JavaScript module that successfully colors the Another text sample, this time only with red and orange repeatedly string à la the Script Tips #81-83 Script:

var redorangeArray = new Array("red", "orange");
var myString = document.getElementById("span1").innerHTML;
var redorangeString = "";
for (i = 0; i < myString.length; i++) {
var charColor = myString.charAt(i);
colorCode = i % redorangeArray.length;
tempStr = charColor.fontcolor(redorangeArray[colorCode]);
redorangeString += tempStr; }
document.getElementById("span1").innerHTML = redorangeString;

The span1 string is replaced by a redorangeString string that is assembled on a character-by-character basis. The red and orange color keyword values are assigned on the first line to a redorangeArray array; their respective redorangeArray indexes, 0 and 1, match the colorCode % returns, which thus do not need to be incremented as was necessary in the else block of the original script.

setColorsByTagName( )

We set our sights higher in this section as we apply the sequence of green shades in the Script Tips #81-83 Script's third ColoredText( ) function call to both of the h1 elements in the above sample document. The A sample with shades of green and This is another H1 heading strings are smoothly colored with the #00x000 sequence via the following JavaScript module:

var headings1 = document.getElementsByTagName("h1");
var greenArray = new Array("#006000", "#007000", "#008000", "#009000", "#00a000", "#00b000");
var greenString = new Array( );
for (i = 0; i < headings1.length; i++) {
greenString[i] = "";
var myString = headings1[i].innerHTML;
for (j = 0; j < myString.length; j++) {
var charColor = myString.charAt(j);
colorCode = j % greenArray.length;
tempStr = charColor.fontcolor(greenArray[colorCode]);
greenString[i] += tempStr; }
headings1[i].innerHTML = greenString[i]; }

In brief:
(1) The h1 elements are scooped up and arrayed via the DOM's getElementsByTagName( ) method.
(2) Separate arrays are set up for the
(a) #00x000 green hex codes and
(b) greened variants of the h1 element strings.
(3) A parent for loop sequentially accesses the h1 element strings and then overwrites them with their greened variants.
(4) A nested for loop creates the greened strings à la the previous section.

setColorsByClass( )

Finally, in this section we'll randomly color the text strings of the sample document's first span element and second p element, which have a common class="rainbow" identifier, with the color values of the defClrsArray array. Unfortunately, the DOM does not have a "getElementsByClass( )" method that would allow us to directly array these elements and thus color them via the "setColorsByTagName( )" approach above. Fortunately, a Google search led me to a script posted at The JavaScript Source and authored by Dustin Diaz that uses the getElementsByTagName( ) method and the DOM className property to indirectly (more circuitously) array elements with a given class attribute value. A watered-down version of Mr. Diaz's script that is suitable for our present purpose is given below:

var classElements = new Array( );
var els = document.getElementsByTagName("*");
var elsLen = els.length;
for (i = 0, j = 0; i < elsLen; i++) {
if (els[i].className == "rainbow") {
classElements[j] = els[i];
j++; } }

Much like a document.all expression, the document.getElementsByTagName("*") expression returns an array of all of a document's elements, which is assigned in the code above to the variable els. We then loop through the els array and use an

if (els[i].className == "rainbow") { classElements[j] = els[i]; j++; }

conditional to fish out those elements whose className value is rainbow; the class="rainbow" elements are sequentially assigned to a classElements array.

The strings contained by the classElements elements can now be accessed and colored per the previous section:

var defClrsArray = new Array("red", "purple", "cyan", "green", "blue", "magenta");
var rainbowString = new Array( );
for (i = 0; i < classElements.length; i++) {
rainbowString[i] = "";
var myString = classElements[i].innerHTML;
for (j = 0; j < myString.length; j++) {
var charColor = myString.charAt(j);
colorCode = Math.floor(Math.random( ) * defClrsArray.length);
/* For periodic coloring, replace the preceding line with: colorCode = j % defClrsArray.length; */
tempStr = charColor.fontcolor(defClrsArray[colorCode]);
rainbowString[i] += tempStr; }
classElements[i].innerHTML = rainbowString[i]; }

FYI: Mr. Diaz's script addresses the complication that an element can belong to more than one class, e.g.,

<p class="class1 class2 class3">This is a paragraph.</p>
<!-- The HTML class attribute is detailed here. -->

by comparing the els[i].className expression to (simplifying once again) a (^|\s)classValue(\s|$)-like regular expression.

Demo

Here's what it all looks like:

A sample with shades of green

This is a text sample written using the function ColoredText.
Another text sample, this time only with red and orange repeatedly

This is another H1 heading

Try refreshing the page a couple of times!


Relevant demo CSS:

div { background-color: #ffffcc; font-size: 16px; }
#p1 { font-family: Courier, monospace; font-size: 24px; text-align: center; }

A couple of points:
#ffffcc is a member of the set of so-called Web-safe colors, which are discussed by Wikipedia here.
• In the Script Tips #81-83 Script, the Try refreshing the page a couple of times! string is marked up with a size="+2" attribute that should in this case be reproducible by a font-size:x-large; declaration, which in practice (without getting into the details) performed as expected with Netscape but not with MSIE on my computer, so I decided to use <length> font-size values for the div and p1 elements instead.

In the following entry, we'll look at the Script Tips #84-86 Script, which codes an image-based digital clock.

reptile7

Tuesday, January 01, 2008
 
Strings in Living Technicolor
Blog Entry #99

Today we return to the topic of color as we take up the "Rainbow Text" script discussed in HTML Goodies' JavaScript Script Tips #81, #82, and #83. As you know, the 'traditional'* HTML tool for adding color to inline text is the now-deprecated font element and its color attribute. The core JavaScript String object has a corresponding fontcolor( ) method that in effect wraps a string in a <font color="color"> ... </font> container. (Although not deprecated, the fontcolor( ) method is nonetheless "non-standard" in the sense that it is not part of ECMAScript.) The Script Tips #81-83 Script uses the fontcolor( ) method in conjunction with other JavaScript tools to create randomly and periodically 'heterocolored' text strings, which are displayed here.
(*I hesitate to use the word 'traditional' because the font element does not appear in early versions of HTML, e.g., HTML 2.0.)

The Script Tips #81-83 Script can be accessed by following the Here's the Code links in all three script tips and is reproduced in the div below:

<html>
<head>

<title>Put Some Colors Into The Text</title>

<script language="javascript" type="text/javascript">
<!-- Start hiding
var i;

function ColoredText( ) {
var argLen = ColoredText.arguments.length;
if (argLen == 0)
{ argLen = 1; }

var text = ColoredText.arguments[0]; 
var textLen = ColoredText.arguments[0].length;
var defClrsArray = new Array("red", "purple", "cyan", "green", "blue", "magenta"); /* default colors, change as needed */

for (i = 0; i < textLen; i++) {
charColor = text.charAt(i);

if (argLen == 1) 
{
colorCode = Math.floor(Math.random( ) * defClrsArray.length);
tempStr = charColor.fontcolor(defClrsArray[colorCode]);
}

else
{
colorCode = i % (argLen - 1);
tempStr = charColor.fontcolor(ColoredText.arguments[colorCode + 1]);
}
document.write(tempStr);
}
}
// Stop hiding -->

</script>
</head>

<body bgcolor="#ffffcc">

<script language="javascript" type="text/javascript">

<!-- Start hiding

ColoredText("This is a text sample written using the function ColoredText");

document.write("<br />");

ColoredText("Another text sample, this time only with red and orange repeatedly", "red", "orange");

document.write("<br /><h1>");

ColoredText("Another sample with shades of green", "#006000", "#007000", "#008000", "#009000", "#00a000", "#00b000");

document.write("</h1><br /><br /><center><font face='courier' size='+2'>");

ColoredText("Try refreshing the page a couple of times!");

document.write("</font></center>");

// Stop hiding -->

</script>

</body>

</html>

Overview

We begin our analysis, as we often do, with the script document body. The body element has one child, a script element. The body script element contains four calls for the ColoredText( ) function, which sits in a separate script element in the document head.
(1,4) The first and fourth ColoredText( ) calls will generate strings whose characters are randomly colored with the color values of the defClrsArray array.
(2) The second ColoredText( ) call will generate a string whose characters are alternately colored red and orange.
(3) The third ColoredText( ) call will generate a string whose characters are periodically (i.e., in a repeated cycle) colored with different shades of green.

Otherwise, the script document body has no real structure to speak of. The ColoredText( ) calls are flanked by document.write( ) commands whose parameters contain some perfunctory markup, all of which ultimately serves a presentational purpose. Is it worth it to separate the HTML and JavaScript in this document? Not really, IMO - I'm not sure it's even worth it to replace the center and font elements surrounding the fourth ColoredText( ) call with a style rule set. However, in the "Setting heterocolor by identifier" section below, we'll see that all of the script's JavaScript can easily be 'modularized' (isolated) for application to other documents.

Random heterocolor

In this section, we'll deconstruct what happens when the browser hits the first ColoredText( ) call:

ColoredText("This is a text sample written using the function ColoredText");

This line triggers the ColoredText( ) function and passes thereto the string This is a text sample written using the function ColoredText.

function ColoredText( ) {
var argLen = ColoredText.arguments.length;

The length (size) of the arguments[ ] object (i.e., the number of ColoredText( ) arguments) - 1 in this case - is assigned to the variable argLen.

Comments
• Every JavaScript function has a local arguments[ ] object for accessing/referencing the arguments passed to that function. The individual arguments of the arguments[ ] object are referenced ordinally à la a client-side document collection: arguments[0] is the first argument, arguments[1] is the second argument, etc.; however, the arguments[ ] object is technically not an array because the properties and methods of the core JavaScript Array object do not apply to it.
• The arguments[ ] object and the composite arguments.length property were once properties of the core JavaScript Function object, but these 'relationships' were deprecated by JavaScript 1.4; consequently, in the ColoredText( ) function, neither arguments.length, arguments[0], nor arguments[colorCode + 1] should be prefaced with a ColoredText reference, e.g., the first ColoredText( ) statement should be recast as:
var argLen = arguments.length;
• Contra Script Tips #81-83, the This is a text sample written using the function ColoredText string is indeed a ColoredText( ) argument; per the first comment above, it's arguments[0].

if (argLen == 0) { argLen = 1; }

None of the ColoredText( ) calls in the document body has no arguments; this conditional never comes into play and can be removed.

var text = arguments[0];
var textLen = arguments[0].length;

The arguments[0] string is assigned to the variable text; the length of the arguments[0] string, 60, is assigned to the variable textLen.

var defClrsArray = new Array("red", "purple", "cyan", "green", "blue", "magenta");
/* FYI: the W3C uses the color keywords aqua and fuchsia for the colors cyan (#00ffff) and magenta (#ff00ff), respectively. IMO, cyan is a nice background color but an awful text color, and should not have been included. */

An array of six color keyword strings is created and assigned to the variable defClrsArray.

for (i = 0; i < textLen; i++) {

A 60-iteration for loop will run through and color the arguments[0] string characters.

charColor = text.charAt(i);

For the for loop's first iteration, text.charAt(0) returns T, which is assigned to the variable charColor.

if (argLen == 1) {
colorCode = Math.floor(Math.random( ) * defClrsArray.length);

The if condition returns true. The right side of the subsequent statement generates a random integer in the range 0 to 5, inclusive, which is assigned to the variable colorCode. Here's what's going on:
(1) Math.random( ) returns a random number between 0 and 1, and running many digits past the decimal point (contra Script Tip #82, it does not generate a random number between 000 and 999).
(2) Multiplying the Math.random( ) return by defClrsArray.length, 6, gives a random number between 0 and 6, and still running many digits past the decimal point.
(3) The Math.random( ) * defClrsArray.length return is rounded down by Math.floor( ) to the 0-5 integer range.
The colorCode range matches the range of defClrsArray index numbers, bringing us to...

tempStr = charColor.fontcolor(defClrsArray[colorCode]); }

We plug colorCode into the square brackets of a defClrsArray[ ] reference; the resulting defClrsArray[colorCode] expression evaluates to a random defClrsArray color value, which is applied to charColor by the fontcolor( ) method to give a colored character that is assigned to the variable tempStr. For example, if colorCode is 5, then defClrsArray[colorCode] evaluates to magenta and charColor=T is thus converted to tempStr=T.

The browser skips over the else block and moves to...

document.write(tempStr); }

The colored tempStr character (T, T, whatever) is written to the page, concluding the first loop iteration. And so it goes: the loop's subsequent iterations randomly color and then print the rest of the arguments[0] string characters.

The preceding discussion also applies to the fourth ColoredText( ) call, for which argLen is also 1.

Periodic heterocolor

In this section, let's look at what happens when the browser hits the third ColoredText( ) call:

ColoredText("Another sample with shades of green", "#006000", "#007000", "#008000", "#009000", "#00a000", "#00b000");

This code triggers the ColoredText( ) function and passes thereto a set of seven arguments.

function ColoredText( ) {
var argLen = arguments.length;
var text = arguments[0];
var textLen = arguments[0].length;
var defClrsArray = new Array("red", "purple", "cyan", "green", "blue", "magenta");
for (i = 0; i < textLen; i++) {
charColor = text.charAt(i);
if (argLen == 1) {

In order:
(1) arguments.length, 7, is assigned to argLen.
(2) arguments[0], the Another sample with shades of green text string, is assigned to text.
(3) arguments[0].length, 35, is assigned to textLen.
(4) The defClrsArray array is again created, although we won't be using it this time.
(5) A 35-iteration for loop kicks off.
(6) text.charAt(0), A, is assigned to charColor in the loop's first iteration.
(7) The (argLen == 1) if condition returns false, so the browser moves on to...

else {
colorCode = i % (argLen - 1);

As the loop proceeds, the right side of the above statement cyclically generates the numbers 0, 1, 2, 3, 4, and 5, which individually are assigned to colorCode:
• For i = 0, 6, 12, 18, 24, 30: i % 6 = 0.
• For i = 1, 7, 13, 19, 25, 31: i % 6 = 1.
• For i = 2, 8, 14, 20, 26, 32: i % 6 = 2; etc.
(We previously discussed the % arithmetic operator in Blog Entry #36. Contra Script Tip #83, 6 % 5 does not equal 1.2 and does not return 2; rather, 6 % 5 equals and returns 1.)

tempStr = charColor.fontcolor(arguments[colorCode + 1]); }
document.write(tempStr); }

The 0-5 colorCode range is incremented so that it matches the range of the index numbers of the ColoredText( ) arguments specifying shades of green (arguments[1] through arguments[6]); otherwise, the first line above is analogous to the fontcolor( ) command of the previous section. For the loop's first iteration, colorCode is 0 and arguments[1] evaluates to the color #006000, which is applied to charColor to give A, which is in turn assigned to tempStr, which is then written to the page.

For the loop's second iteration, text.charAt(1), n, is given a #007000 color (arguments[2]); for the third iteration, text.charAt(2), o, is given a #008000 color (arguments[3]); and so on. The sequence of green shades begins again with the loop's seventh iteration, in which text.charAt(6), r, is given the #006000 color (arguments[1]). In total, the loop colors the arguments[0] string characters with the arguments[1]-arguments[6] color cycle not quite six times.

For the second ColoredText( ) call, the ColoredText( ) function similarly applies a arguments[1]-arguments[2] color cycle to the arguments[0] string; a detailed deconstruction of this case is left to you.

To return or not to return

On my computer and regardless of browser, the ColoredText( ) function is somehow able to direct its output to specific points on the page without a return statement, e.g., the Another sample with shades of green string is in practice rendered as an h1 element. Before the fact, one might think that the colored arguments[0] strings would be written one after the other at the beginning of the document body, without any markup, if they are not returned, and maybe that does happen with some browsers. If need be, here's how you can ensure that the outputted strings are sent to their intended destinations:

(1) First, nest the ColoredText( ) calls in document.write( ) commands, e.g.,

document.write(ColoredText("Try refreshing the page a couple of times!"));

For each call, we'll want to return a completed string and not just one character; towards this end:

(2) Begin the ColoredText( ) function with:

function ColoredText( ) {
var newString = "";

(3) Conclude the if and else blocks with newString += tempStr statements, e.g.,

else {
colorCode = i % (argLen - 1);
tempStr = charColor.fontcolor(arguments[colorCode + 1]);
newString += tempStr; }

(4) Finally, outside of the for loop, conclude the ColoredText( ) function with:

return newString; }

Steps (2)-(4) utilize a "start with an empty string, and build the new string character by character" methodology that we previously encountered in the multiple search engine script of Script Tips #42-44.

It's been a while since we've used the += shorthand assignment operator; Mozilla defines it on this page if you could use a refresher thereon.

Setting heterocolor by identifier

This topic deserves its own entry and we'll get to it next time.

reptile7


Powered by Blogger

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