reptile7's JavaScript blog
Monday, April 23, 2012
Approaching iCal
Blog Entry #248

We return now to our discussion of the changeCal( ) function of the Calendar snippet of HTML Goodies' "Top 10 JavaScript Snippets for Common Tasks" tutorial. At this point in our journey, here's what we've got on the page:

The writeCalendar( ) function output

In this post we'll replace those 1s with a display suitable for the current month/year, April 2012 at this writing. More specifically as regards the tables[2] table:
(1) We'll put a week's worth of grayed-out dates for March 2012 in the second row.
(2) We'll run the 30 days of April 2012 from the first cell of the third row to the second cell of the seventh row in a normal manner, and highlight the current date while we're at it.
(3) We'll finish out the seventh row with five grayed-out dates for May 2012.

The April 2012 calendar display

Our first step is to respectively get the current month's getMonth( ) index and the current year from the selMonth and selYear selection lists that were created in the writeCalendar( ) function.

var currM = parseInt(document.calForm.selMonth.value);
var currY = parseInt(document.calForm.selYear.value);

As for all other control types, the value property of the select object (first implemented by Microsoft, now standard) has a string data type. Use of the parseInt( ) function in the preceding statements is in fact not necessary as the JavaScript engine will carry out string-to-number type conversion on the fly for the code to come, but there's no harm in leaving the parseInt( ) calls in place.

The first date of the current month/year - 1 April 2012, which falls on a Sunday - will serve as a base for setting the dates of the tables[2] table, so let's go get that date:

var mmyyyy = new Date( );
var day1 = mmyyyy.getDay( );
if (day1 == 0) { day1 = 7; }

The setFullYear( ) method of the Date object can optionally take arguments specifying a month and a date

mmyyyy.setFullYear(currY, currM, 1);

and thus the above setMonth( ) and setDate( ) commands are unnecessary.

As detailed below, the day1 value - initially 0, set to 7 by the if (day1 == 0) day1 = 7; statement - is used to delimit the March/April/May zones of the display. The day1 value also determines the number of March days that lead up to 1 April; commenting out the if statement will remove the March zone, push the April zone up a row, and give a 1 May to 12 May zone in the last two rows of the tables[2] table.

The changeCal( ) function next creates an arrN array

var arrN = new Array(41); // That should be 42, Alexander

of date numbers (a) that will be respectively loaded into the date spans (the span element children of the td cells of the lower six rows) of the tables[2] table and (b) whose arrN indexes will map onto the ids of those spans. The arrN array can be divided into a March part, an April part, and a May part; the March part is populated by:

var prevM;
if (currM != 0) { prevM = currM - 1; }
else { prevM = 11; }
for (ii = 0; ii < day1; ii++) { arrN[ii] = maxDays(prevM, currY) - day1 + ii + 1; }

An if...else statement assigns the getMonth( ) index of the previous month - that would be 2 for March - to a prevM variable. The statement can be written more concisely via the ?: conditional operator:

var prevM = currM ? currM - 1 : 11;

Subsequently a for loop calls the snippet's maxDays( ) function

function maxDays(mm, yyyy) {
    var mDay;
    if ((mm == 3) || (mm == 5) || (mm == 8) || (mm == 10)) { mDay = 30; }
    else {
        mDay = 31;
        if (mm == 1) {
            if (yyyy/4 - parseInt(yyyy/4) != 0) { mDay = 28; }
            else { mDay = 29; } } }
    return mDay; }

and passes thereto the prevM index and the currY year in order to get the mDay date number for the last day of the prevM month, 31 for March. The mDay return is adjusted (- day1 + 1) to give the date number that belongs in the first cell of the second row, 25: let's call this number row2Sun. The row2Sun value is incremented by the loop to generate the remaining March date numbers for the second row; the 25-to-31 numbers are respectively assigned to the first seven 'boxes' of the arrN array, as though we had coded arrN = [25, 26, 27, 28, 29, 30, 31];.

Each loop iteration runs the maxDays( ) function and determines the row2Sun date number; these operations can and should be 'factored out':

var row2Sun = maxDays(prevM, currY) - day1 + 1;
for (ii = 0; ii < day1; ii++) arrN[ii] = row2Sun + ii;

The leap year part of the maxDays( ) function is flawed - we'll get it sorted out later. For now let's move to the arrN array's April part, which is populated by:

var aa = 1;
for (ii = day1; ii <= day1 + maxDays(currM, currY) - 1; ii++) {
    arrN[ii] = aa;
    aa += 1; }

The 1-to-30 date numbers of April are sequentially represented by an aa variable; these numbers are respectively loaded into the next thirty boxes of the arrN array (arrN[7] through arrN[36]) by the above loop. The loop's upper boundary, ii = 36, is determined by calling the maxDays( ) function with currM and currY inputs and adjusting (+ day1 - 1) the mDay = 30 return; these operations should again be carried out prior to the loop and not iteratively, e.g., by a var endofcurrM = day1 + maxDays(currM, currY) - 1; statement.

Lastly, the May part of the arrN array is populated by:

aa = 1;
for (ii = day1 + maxDays(currM, currY); ii <= 41; ii++) {
    arrN[ii] = aa;
    aa += 1; }

aa is reset to 1 for the 1-to-5 May date numbers, which are respectively loaded into the arrN[37]-to-arrN[41] boxes of the arrN array. Using the above endofcurrM variable, the May loop's initialExpression can and should be reformulated as ii = endofcurrM + 1. (The initialExpression is not executed iteratively but if we've already done the day1 + maxDays(currM, currY) calculation then there's no reason to do it again.)

With the arrN array now complete, the changeCal( ) function then gives a white background color to all of the tables[2] date spans:

for (ii = 0; ii <= 41; ii++) { eval("sp" + ii).style.backgroundColor = "#ffffff"; }

At first glance this statement may seem unnecessary as the span.c1, span.c2, and span.c3 rule sets in the snippet's style block will impart a white background color to these spans, but it does have a purpose, namely, it clears any highlights effected by the changeBg( ) function (which we'll cover in the next post) as well as the current date highlight if the user changes the calendar month or year. Note the use of the eval( ) function to convert the "sp" + ii strings to id-value object references; actually, Microsoft itself would recommend that we alternatively access the date spans via the getElementById( ) method, i.e.:

for (ii = 0; ii <= 41; ii++) { document.getElementById("sp" + ii).style.backgroundColor = "#ffffff"; }

The remainder of the changeCal( ) function consists of a for loop that
(a) loads the arrN date numbers into the tables[2] date spans,
(b) indirectly applies various styles to the tables[2] date spans, and
(c) highlights the current date.
The loop uses a revolving dCount variable to flag the Sun and Sat columns in order to redden the April dates in those columns; dCount's values run in a 0-to-6 cycle and map onto the 0-to-6 returns of the Date object's getDay( ) method.

Just before the loop, dCount is initialized to 0; the loop then kicks off with an if block that handles the March and May dates of the display:

var dCount = 0;
for (ii = 0; ii <= 41; ii++) {
    if (((ii < 7) && (arrN[ii] > 20)) || ((ii > 27) && (arrN[ii] < 20))) {
        eval("sp" + ii).innerHTML = arrN[ii];
        eval("sp" + ii).className = "c3"; }

The previous/current month boundary and the current/next month boundary are each set via a combination of arrN index and value; these combinations can be determined by considering extreme cases involving a non-leap year February.

• We know that the tables[2] table will contain at least one and as many as seven dates for the previous month via the day1 variable. Suppose that the current month is a March whose first day falls on a Sunday. If the preceding month is a non-leap year February, then the row2Sun value will be 22; for all other cases - for all other month boundaries, be it a leap year or not - the row2Sun value will be higher. It follows that a given arrN value will belong to the previous month if the value's arrN index is 6 or lower AND the value itself is 22 or higher.

• Suppose that the current month is a non-leap year February whose first day falls on a Monday. The 28 days of that February will run from the second cell of the second row to the first cell of the sixth row and will be followed by 13 March dates; for all other cases there will be fewer next month dates (the minimum is four, for a current month that begins on a Sunday and has 31 days) and those dates will begin at a later position in the table. It follows that a given arrN value will belong to the next month if the value's arrN index is 29 or higher AND the value itself is 13 or lower.

In sum, the precise previous/current month boundary is
ii <= 6 && arrN[ii] >= 22
and the precise current/next month boundary is
ii >= 29 && arrN[ii] <= 13.
The above if condition unnecessarily provides a bit of margin for error but is serviceable.

Via the ii counter/index and the innerHTML property, the March and May date numbers are written to their corresponding tables[2] date spans; the className of those spans is then set to c3 in order to gray out the March/May date numbers via the span.c3 style rule set:

span.c3 { cursor: hand; color: #b0b0b0; width: 30; height: 16; text-align: center; margin-top: 0; background: #fff; font: bold 12px Arial; }

The if block is followed by an else clause that handles the April dates of the display.

else {
    eval("sp" + ii).innerHTML = arrN[ii];
    if ((dCount == 0) || (dCount == 6)) { eval("sp" + ii).className = "c2"; }
    else { eval("sp" + ii).className = "c1"; }
    if ((arrN[ii] == dd) && (mm == currM) && (yyyy == currY)) { eval("sp" + ii).style.backgroundColor = "#90ee90"; } }

The April date numbers are loaded into their corresponding tables[2] date spans à la the March/May date numbers. If dCount is 0 or 6, i.e., if arrN[ii] is a Sunday or Saturday date number, then the number is reddened via the span.c2 style rule set:

span.c2 { cursor: hand; color: red; width: 30; height: 16; text-align: center; margin-top: 0; background: #fff; font: bold 13px Arial; }

Otherwise the date numbers are rendered in a customary black color via the span.c1 style rule set:

span.c1 { cursor: hand; color: black; width: 30; height: 16; text-align: center; margin-top: 0; background: #fff; font: bold 13px Arial; }
/* Prior to going through the code I didn't notice that the April date numbers have a slightly larger font size (13px) than do the March/May date numbers (12px) - I myself would equalize them. */

The dCount if...else statement can be condensed via the ?: operator:
eval("sp" + ii).className = dCount == 0 || dCount == 6 ? "c2" : "c1";

A concluding if statement imparts a #90ee90 (light green) highlight to the date span for the current (mm dd, yyyy) date.

At the bottom of the loop, dCount is incremented or reset as necessary:

dCount += 1;
if (dCount > 6) { dCount = 0; }
/* Alternatively: dCount = dCount == 6 ? 0 : dCount + 1; */
} }

A new month/year

As noted in the previous post, the selMonth and selYear selection lists are bound to the changeCal( ) function via an onchange='changeCal( );' attribute. Changing the calendar's month or year re-calls the changeCal( ) function and we go through the whole shebang all over again: we go get the currM month index and the currY year from the selection lists, we create a new mmyyyy Date object set to the first date of the chosen month/year, we repopulate the arrN array via mmyyyy's day1 value, etc.

In the following entry we'll discuss the snippet's changeBg( ) function, clear up the leap year conditional in the maxDays( ) function, and address the placement of the calendar.

Comments: Post a Comment

<< Home

Powered by Blogger

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