reptile7's JavaScript blog
Wednesday, May 02, 2012
Calendar Sprawl III
Blog Entry #249

In today's post we'll wrap up our look at the Calendar snippet of HTML Goodies' "Top 10 JavaScript Snippets for Common Tasks" tutorial by taking care of some outstanding issues that we didn't get to in the last couple of entries.

The changeBg( ) function

You may have noticed that the span.c1, span.c2, and span.c3 style rule sets begin with a cursor:hand; declaration. With IE, Chrome, and Safari, moving the mouse cursor over a calendar date changes the cursor into a little hand

The hand cursor
(go here for a page of downloadable cursor image links)

cueing the user that, à la a link, something will happen if the date is clicked: that something is a highlighting of the date by giving it a yellow background color via the if clause of the snippet's changeBg( ) function

function changeBg(id) {
    if (eval(id).style.backgroundColor != "yellow") { eval(id).style.backgroundColor = "yellow"; }
    else { eval(id).style.backgroundColor = "#ffffff"; } }

which is triggered by the onclick='changeBg(;' attribute that each tables[2] date span

text += "<td align='center'><span id='sp" + aa + "' onclick='changeBg(;'>1</span></td>";

is equipped with. Clicking the highlighted date removes the highlight (sets the date's background color to white) via the function's else clause.

There's no need for the changeBg( ) function to reach for the eval( ) function or even the getElementById( ) method in order to access a given date span and its child style object; if we were to feed (vis-à-vis to the changeBg( ) function, then the function can be recast as:

function changeBg(spanStyle) {
    spanStyle.backgroundColor = spanStyle.backgroundColor != "yellow" ? "yellow" : "white"; }

The hand value of the CSS cursor property is not standard and is not supported by the Mozilla browsers nor by Opera; the cursor: declaration can be legitimized and cross-browserized by switching the hand value to pointer. Even without a cursor:pointer;, however, the onclick='changeBg(;' event handler is operative for the Mozilla browsers and Opera, i.e., the calendar dates can still be un/highlighted as described above, although the user is unlikely to intuit that the dates are actionable.

We noted two entries ago that IE is the only major browser that will apply the CSS width and height properties to inline spans. With IE, the uniform width of the tables[2] columns is effectively set by the width:30; declaration of the various span. style rule sets; in this case clicking a calendar date highlights the entire date cell. (OK, there's a 1px-thick td padding belt that isn't yellowized, but close enough, eh?)

IE highlight

With non-IE browsers, the varying widths of the tables[2] columns are effectively set by the Sun-to-Sat day headings of the first row; in this case clicking a calendar date highlights a content-hugging span whose width is less than that of the relevant day heading.

Non-IE highlight

Doesn't look as nice, does it? (FYI: Switching the day/date font family from Arial to Courier or another monospace font equalizes the column widths but still gives a doesn't-span-the-entire-cell highlight.) Fortunately, there are two simple ways to produce a cell-spanning highlight for non-IE browsers:

(1) Add a display:inline-block; declaration to the span. styles.

.label, .c1, .c2, .c3 { display: inline-block; }

The width:30; declaration (and also the height:16; and text-align:center; declarations) will now apply to the tables[2] day/date spans.

(2) Subtract the span element markup and directly apply the width and other style properties to the tables[2] cells.

.label, .c1, .c2, .c3 { width: 30px; height: 16px; background: white; font: bold 13px Arial, sans-serif; }
.c1, .c2, .c3 { cursor: pointer; text-align: center; }
.label, .c1 { color: black; }
.c2 { color: red; }
.c3 { color: #b0b0b0; }
for (ii = 0; ii <= 6; ii++) { text += "<th class='label'>" + arrD[ii] + "</th>"; }
for (ii = 0; ii <= 6; ii++) { text += "<td id='sp" + aa + "' onclick='changeBg(;'></td>"; aa += 1; }

29 February

The snippet's maxDays( ) function contains the following statement for determining the last date in non-leap year and leap year Februaries:

if (mm == 1) {
    if (yyyy/4 - parseInt(yyyy/4) != 0) { mDay = 28; }
    else { mDay = 29; } }

The (yyyy/4 - parseInt(yyyy/4) != 0) if condition tests if the yyyy year value is not a multiple of 4: the condition returns true if the yyyy/4 division gives a floating-point number. The statement can be written more simply via the % arithmetic operator and by taking into account that 0 converts to false in a logical context:

if (mm == 1) mDay = yyyy % 4 ? 28 : 29;

In the tutorial comment thread, leap points out that the maxDays( ) leap year code does not catch all non-leap years:
If you're going to include a leap day calculation, get the formula correct:
mDay = (year mod 4 != 0 or year mod 100 == 0) ? 28 : 29;
The code below allows Feb 29 1900 which is incorrect.
if (mm == 1) { if (yyyy/4 - parseInt(yyyy/4) != 0) { mDay = 28; } else { mDay = 29; } }
leap is correct that 1900 was not a leap year; however, leap's proposed (year mod 4 != 0 or year mod 100 == 0) condition for flagging a non-leap year would misidentify years that are a multiple of 400 (e.g., 1600, 2000) as non-leap years when they are in fact leap years. Here's the statement we really want:

if (mm == 1) mDay = (yyyy % 4) || (!(yyyy % 100) && (yyyy % 400)) ? 28 : 29;

If (a) the yyyy year value is not a multiple of 4, OR
(b) the yyyy year value is a multiple of 100 AND is not a multiple of 400 (e.g., 1800, 2100),
then yyyy is a non-leap year; otherwise it's a leap year. See Wikipedia's "Leap year" entry for more on this.

Place it

At the end of the Calendar section, Scott tells the reader to place the writeCalendar( ) function trigger in the body of your document ... where you want the calendar to show up. This sort of vague instruction is OK for the Date Display and IP Address snippets as these snippets output #PCDATA text, which can indeed be plopped just about anywhere on a Web page (or written to the browser window's title/status bars); in contrast, the Calendar snippet gives us an actual structure to deal with.

As noted at the outset of our Calendar analysis, the calendar code's outermost element is the calForm form: the form element is a block-level element whose rendered width spans the width of the viewport. If we throw out the calForm form, then the tables[0] table becomes the outermost element: the table element has a shrink-to-fit rendered width but is also a block-level element. So if you're content with the calendar occupying its own block on the page, then you're good to go - no changes to the calendar's default rendering are needed. N.B. The body element has a (%block;|SCRIPT)+ +(INS|DEL) content model, so you can validly deploy the <script type="text/javascript">writeCalendar( );</script> script element as a child of the body element.

If for whatever reason you would like the calendar to have an inline rendering, then the easiest route to that end is to hold on to the calForm form and give it a display:inline-block; style. Flanking text will appear at the baseline of the calendar; giving the inline-block form a vertical-align:middle; style will lift the text to the vertical center of the calendar.

Alternatively, you might want to override* the calendar's default rendering in the opposite sense, i.e., perhaps you would rather put the second script element in a floated or absolutely positioned div container in order to separate the calendar from the rest of the page's content - that's what I would probably do.

*Actually, floats and absolutely positioned elements themselves establish new block formatting contexts.

A final comment on design

I'm not entirely comfortable with the Calendar snippet's non-separation of structure and behavior, but I can see why Alexander wrote it that way: after all, the calendar is not meant to be a major part of the page but rather a small, decorative widget. Moreover, I'm not happy with the snippet's extensive use of document.write( ) commands (all those HTML end-tags that need to be escaped). The JavaScript Source page on which I found the Calendar snippet says that the snippet was written in 2005; this being the case, it is striking that there are no DOM method calls - e.g., getElementById( ), createElement( ), appendChild( ) - in the snippet.

Nevertheless, it does make sense to use JavaScript to assemble some parts of the calender, in particular the tables[2] table and also the selMonth and selYear selection lists.

for (ii = 0; ii <= 11; ii++) {
    monthOption = document.createElement("option");
    monthOption.value = ii;
    monthOption.text = arrM[ii];
    if (ii == mm) document.getElementById("select1").options[ii].selected = true; }
document.getElementById("select1").onchange = changeCal;
<select id="select1" name="selMonth"></select>

IMO the rest of the calendar is better written out as normal HTML, but to each his own.

Working through scripts like the Calendar snippet (warts and all) is definitely the best way to learn HTML/CSS/JavaScript - good job, Alexander!

In the following entry we'll move on to the next Beyond HTML : JavaScript sector tutorial, "How Can I Set A Cookie Based On A User's Selection On A Form?".

Comments: Post a Comment

<< Home

Powered by Blogger

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