[ Team LiB ] Previous Section Next Section

15.9 Creating a Calendar Date Picker

NN 6, IE 5(Win)

15.9.1 Problem

You want to provide a pop-up calendar to assist users in locating and entering a date into a form.

15.9.2 Solution

You can make the user interface part of a popup calendar date picker by using a dynamic table inside an absolute-positioned div container. Scripting behind the picker must accomplish two primary tasks:

  • Populating the calendar with views of a selected month within a selected year

  • Allowing the click of a date in the calendar to be delivered back to the main document form to fill in the date field(s)

See the Discussion for an example of an HTML-based date picker, along with style sheets and the scripts that power the calendar. One script function, shown in Example 15-4, is called showCalendar( ). This function is invoked by a user interface element inside the displayed form.

To get everything initialized, an onload event handler in the body element invokes all necessary routines in the current page as well as the DHTML API library from Recipe 13.3:

onload="fillYears( ); populateTable(document.dateChooser); initDHTMLAPI( )"

15.9.3 Discussion

This solution has a lot of code, including HTML, CSS, and scripts. But there is a lot going on here: dynamically creating table content for the calendar, setting the position and visibility of the calendar, supplying data from the calendar back to the main page, and more. Figure 15-3 shows the calendar being used with a very simple date form.

Figure 15-3. Pop-up calendar picker
figs/jsdc_1503.gif

We'll start with two segments of HTML. One is for the date form that ultimately receives the data from the date picker. A click of the button-type input element invokes a function that displays the calendar:

<form name="mainForm" id="mainForm" method="POST" action="...">
<p>Enter a date:</p>
<p>mm:<input type="text" name="month" id="month" size="3" maxlength="2" value="1"> 
dd:<input type="text" name="day" id="day" size="3" maxlength="2" value="1"> 
yyyy:<input type="text" name="year" id="year" size="5" maxlength="4" value="2003">
<input type="button" id="showit" value="Pick Date  >>" onclick="showCalendar(event)">
</p>
</form>

The other important HTML part is the positioned div element that holds the calendar table. It is delivered with the page in sparse form (and hidden from view) because scripts fill out the rest during initialization. Only the days-of-the-week headers and select list of months are preset in the code. Month names in the select list get used later on to supply the name for the current month of the calendar:

<div id="calendar">
<table id="calendarTable" border=1>
<tr>
    <th id="tableHeader" colspan="7"></th>
</tr>
<tr><th>Sun</th><th>Mon</th><th>Tue</th><th>Wed</th>
<th>Thu</th><th>Fri</th><th>Sat</th></tr>
<tbody id="tableBody"></tbody>
<tr>
    <td colspan="7">
    <p>
    <form name="dateChooser">
        <select name="chooseMonth" 
        onchange="populateTable(this.form)">
            <option selected>January<option>February
            <option>March<option>April<option>May
            <option>June<option>July<option>August
            <option>September<option>October
            <option>November<option>December
    </select>
    <select name="chooseYear" onchange="populateTable(this.form)">
    </select>
    </form>
    </p>
    </td>
</tr>
</table>
</div>

The table and its components are governed by a fairly extensive style sheet that covers positioning, visibility, table cell alignment, background colors, fonts, and the like. Mouse rollover effects for the date numbers in the calendar are controlled strictly by CSS pseudoclasses of a elements:

<style type="text/css">
#calendar {position:absolute; 
           left:0px; 
           top:0px; 
           visibility:hidden
          }
table {font-family:Verdana, Arial, Helvetica, sans-serif; 
       background-color:#999999
      }
th {background-color:#ccffcc; 
    text-align:center; 
    font-size:10px; 
    width:26px
   }
#tableHeader {background-color:#ffcccc; 
              width:100%
             }
td {background-color:#ffffcc; 
    text-align:center; 
    font-size:10px
   }
#tableBody tr td {width:26px}
#today {background-color:#ffcc33}
a:link {color:#000000; text-decoration:none}
a:active {color:#000000; text-decoration:none}
a:visited {color:#000000; text-decoration:none}
a:hover {color:#990033; text-decoration:underline}
</style>

Example 15-4 shows the script portion of this recipe, including the linked-in DHTML API library from Recipe 13.3. The calendar script is divided into four sections.

Example 15-4. Scripts for the pop-up date picker
<script type="text/javascript" src="DHTMLapi.js"></script>
<script type="text/javascript">

/*******************
  Utility Functions
********************/
// day of week of month's first day
function getFirstDay(theYear, theMonth){
    var firstDate = new Date(theYear,theMonth,1);
    return firstDate.getDay( );
}
   
// number of days in the month
function getMonthLen(theYear, theMonth) {
    var nextMonth = new Date(theYear, theMonth + 1, 1);
    nextMonth.setHours(nextMonth.getHours( ) - 3);
    return nextMonth.getDate( );
}
   
// read position of an element in regular document flow
function getElementPosition(elemID) {
    var offsetTrail = document.getElementById(elemID);
    var offsetLeft = 0;
    var offsetTop = 0;
    while (offsetTrail) {
        offsetLeft += offsetTrail.offsetLeft;
        offsetTop += offsetTrail.offsetTop;
        offsetTrail = offsetTrail.offsetParent;
    }
    if (navigator.userAgent.indexOf("Mac") != -1 && 
        typeof document.body.leftMargin != "undefined") {
        offsetLeft += document.body.leftMargin;
        offsetTop += document.body.topMargin;
    }
    return {left:offsetLeft, top:offsetTop};
}
   
// position and show calendar
function showCalendar(evt) {
    evt = (evt) ? evt : event;
    if (evt) {
        if (document.getElementById("calendar").style.visibility != "visible") {
            var elem = (evt.target) ? evt.target : evt.srcElement;
            var position = getElementPosition(elem.id);
            shiftTo("calendar", position.left + elem.offsetWidth, position.top);
            show("calendar");
        } else {
            hide("calendar");
        }
    }
}
   
/************************
  Draw Calendar Contents
*************************/
// clear and re-populate table based on form's selections
function populateTable(form) {
    // pick up date form choices
    var theMonth = form.chooseMonth.selectedIndex;
    var theYear = parseInt(form.chooseYear.options[form.chooseYear.selectedIndex].text);
    // initialize date-dependent variables
    var firstDay = getFirstDay(theYear, theMonth);
    var howMany = getMonthLen(theYear, theMonth);
    var today = new Date( );
    
    // fill in month/year in table header
    document.getElementById("tableHeader").innerHTML = 
        form.chooseMonth.options[theMonth].text + " " + theYear;
    
    // initialize vars for table creation
    var dayCounter = 1;
    var TBody = document.getElementById("tableBody");
    // clear any existing rows
    while (TBody.rows.length > 0) {
        TBody.deleteRow(0);
    }
    var newR, newC, dateNum;
    var done=false;
    while (!done) {
        // create new row at end
        newR = TBody.insertRow(TBody.rows.length);
        if (newR) {
            for (var i = 0; i < 7; i++) {
                // create new cell at end of row
                newC = newR.insertCell(newR.cells.length);
                if (TBody.rows.length =  = 1 && i < firstDay) {
                    // empty boxes before first day
                    newC.innerHTML = "&nbsp;";
                    continue;
                }
                if (dayCounter =  = howMany) {
                    // no more rows after this one
                    done = true;
                }
                // plug in link/date (or empty for boxes after last day)
                if (dayCounter <= howMany) {
                    if (today.getFullYear( ) =  = theYear &&
                        today.getMonth( ) =  = form.chooseMonth.selectedIndex &&
                        today.getDate( ) =  = dayCounter) {
                        newC.id = "today";
                    }
                    newC.innerHTML = "<a href='#'onclick='chooseDate(" +
                        dayCounter + "," + theMonth + "," + theYear + 
                        "); return false;'>" + dayCounter + "</a>";
                     dayCounter++;
               } else {
                    newC.innerHTML = "&nbsp;";
                }
            }
        } else {
            done = true;
        }
    }
}
   
/*******************
  Initializations
********************/
// create dynamic list of year choices
function fillYears( ) {
    var today = new Date( );
    var thisYear = today.getFullYear( );
    var yearChooser = document.dateChooser.chooseYear;
    for (i = thisYear; i < thisYear + 5; i++) {
        yearChooser.options[yearChooser.options.length] = new Option(i, i);
    }
    setCurrMonth(today);
}
// set month choice to current month
function setCurrMonth(today) {
    document.dateChooser.chooseMonth.selectedIndex = today.getMonth( );
}
   
/*******************
   Process Choice
********************/
function chooseDate(date, month, year) {
    document.mainForm.date.value = date;
    document.mainForm.month.value = month + 1;
    document.mainForm.year.value = year;
    hide("calendar");
}
   
</script>

The first script section contains several utility functions that support others to come. First is a pair of functions, getFirstDay( ) and getMonthLen( ), that the calendar-creation routines use to find which day of the week the first day of a month falls on, and then the length of the month. The three-hour correction in getMonthLen( ) takes care of date calculation anomalies that can occur when the month includes a transition from summer to winter. The goal is to obtain a valid date of the day before the first of the next month.

When it's time to display the calendar, the next pair of functions come into play. The getElementPosition( ) function (Recipe 13.8) determines the position of a body element (the "Pick Date" button in our example), which the following showCalendar( ) function uses to position the calendar just to the right of the button before showing the calendar (positioning and visibility are controlled from DHTML API functions).

The core routine of this application, populateTable( ), calculates the date data and assembles the HTML for the table body portion of the calendar pop-up window. It begins by gathering important bearings for the calculations from select lists at the bottom of the calendar. Then it places the month and year text in the table's headers. After some DOM-oriented preparations, the script removes any previous table body content. At the heart of the script is a while loop that keeps adding rows to the table as needed. For each row, a for loop generates cells for each of the seven columns, filling cells with date numbers surrounded by links that pass the values back to the main form. A little extra touch is labeling the ID of the current day's cell so that it picks up one of the style sheet rules to make it stand out from the rest of the cells.

Two initialization routines, fillYears( ) and setCurrMonth( ), prepare the select elements in the calendar so that the years in the list constantly move forward as time marches on. Also, the lists are set to the current month and year as a starting point for the user the first time the calendar appears.

When the user clicks on one of the dates in the calendar, the links for each date invoke the chooseDate( ) function, passing parameters for the date, month, and year. The parameters are assigned to the event handlers of the calendar date links while the calendar month's HTML is assembled back in populateTable( ). The chooseDate( ) function in this example distributes the values to the three date fields in the original form.

This pop-up calendar works in Internet Explorer 5 or later for Windows and Netscape 6 or later. Unfortunately, table modification bugs in IE 5 for the Mac prevent it from working in that environment.

Netscape (at least through Version 7) exhibits a lingering oddity when the calendar div overlaps form controls, especially text-oriented fields. A rendering conflict causes the text field to supersede the positioned div such that you cannot click on calendar date links or activate the select lists if any of them are on top of a field. Therefore, you should endeavor to design your page and the position of the calendar div such that the div does not come into contact with form controls. IE does not have this problem.

Most of the visible, fun part of this application is governed by style sheets for the calendar table. You have wide flexibility in designing your calendar by using the HTML tags and IDs of the skeletal calendar table as a guide. If you adhere to those naming conventions, the calendar-generating and modifying code will work without any problems.

Another potential modification that might appeal to you is to make the calendar draggable by its titlebar. You can adapt the element-dragging code from Recipe 13.11 to add that functionality here, as well.

15.9.4 See Also

Recipe 2.9 and Recipe 2.10 for details about using date objects; Recipe 12.7 for hiding and showing elements; Recipe 13.3 for details on the DHTML API library; Recipe 13.8 for obtaining the position of a body element.

    [ Team LiB ] Previous Section Next Section