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:
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.
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 = " ";
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 = " ";
}
}
} 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.
|