update
[openemr.git] / library / js / jquery-calendar.js
blob4d70d3d55f40923bc984fce6d4db440c4bc2d730
1 /* jQuery Calendar v2.7
2    Written by Marc Grabanski (m@marcgrabanski.com) and enhanced by Keith Wood (kbwood@iprimus.com.au).
4    Copyright (c) 2007 Marc Grabanski (http://marcgrabanski.com/code/jquery-calendar)
5    Dual licensed under the GPL (http://www.gnu.org/licenses/gpl-3.0.txt) and 
6    CC (http://creativecommons.org/licenses/by/3.0/) licenses. "Share or Remix it but please Attribute the authors."
7    Date: 09-03-2007  */
9 /* PopUp Calendar manager.
10    Use the singleton instance of this class, popUpCal, to interact with the calendar.
11    Settings for (groups of) calendars are maintained in an instance object
12    (PopUpCalInstance), allowing multiple different settings on the same page. */
13 function PopUpCal() {
14         this._nextId = 0; // Next ID for a calendar instance
15         this._inst = []; // List of instances indexed by ID
16         this._curInst = null; // The current instance in use
17         this._disabledInputs = []; // List of calendar inputs that have been disabled
18         this._popUpShowing = false; // True if the popup calendar is showing , false if not
19         this._inDialog = false; // True if showing within a "dialog", false if not
20         this.regional = []; // Available regional settings, indexed by language code
21         this.regional[''] = { // Default regional settings
22                 clearText: 'Clear', // Display text for clear link
23                 closeText: 'Close', // Display text for close link
24                 prevText: '<Prev', // Display text for previous month link
25                 nextText: 'Next>', // Display text for next month link
26                 currentText: 'Today', // Display text for current month link
27                 dayNames: ['Su','Mo','Tu','We','Th','Fr','Sa'], // Names of days starting at Sunday
28                 monthNames: ['January','February','March','April','May','June',
29                         'July','August','September','October','November','December'], // Names of months
30                 dateFormat: 'DMY/' // First three are day, month, year in the required order,
31                         // fourth (optional) is the separator, e.g. US would be 'MDY/', ISO would be 'YMD-'
32         };
33         this._defaults = { // Global defaults for all the calendar instances
34                 autoPopUp: 'focus', // 'focus' for popup on focus,
35                         // 'button' for trigger button, or 'both' for either
36                 appendText: '', // Display text following the input box, e.g. showing the format
37                 buttonText: '...', // Text for trigger button
38                 buttonImage: '', // URL for trigger button image
39                 buttonImageOnly: false, // True if the image appears alone, false if it appears on a button
40                 closeAtTop: true, // True to have the clear/close at the top,
41                         // false to have them at the bottom
42                 hideIfNoPrevNext: false, // True to hide next/previous month links
43                         // if not applicable, false to just disable them
44                 changeMonth: true, // True if month can be selected directly, false if only prev/next
45                 changeYear: true, // True if year can be selected directly, false if only prev/next
46                 yearRange: '-10:+10', // Range of years to display in drop-down,
47                         // either relative to current year (-nn:+nn) or absolute (nnnn:nnnn)
48                 firstDay: 0, // The first day of the week, Sun = 0, Mon = 1, ...
49                 changeFirstDay: true, // True to click on day name to change, false to remain as set
50                 showOtherMonths: false, // True to show dates in other months, false to leave blank
51                 minDate: null, // The earliest selectable date, or null for no limit
52                 maxDate: null, // The latest selectable date, or null for no limit
53                 speed: 'medium', // Speed of display/closure
54                 customDate: null, // Function that takes a date and returns an array with
55                         // [0] = true if selectable, false if not,
56                         // [1] = custom CSS class name(s) or '', e.g. popUpCal.noWeekends
57                 fieldSettings: null, // Function that takes an input field and
58                         // returns a set of custom settings for the calendar
59                 onSelect: null // Define a callback function when a date is selected
60         };
61         $.extend(this._defaults, this.regional['']);
62         this._calendarDiv = $('<div id="calendar_div"></div>');
63         $(document.body).append(this._calendarDiv);
64         $(document.body).mousedown(this._checkExternalClick);
67 $.extend(PopUpCal.prototype, {
68         /* Register a new calendar instance - with custom settings. */
69         _register: function(inst) {
70                 var id = this._nextId++;
71                 this._inst[id] = inst;
72                 return id;
73         },
75         /* Retrieve a particular calendar instance based on its ID. */
76         _getInst: function(id) {
77                 return this._inst[id] || id;
78         },
80         /* Override the default settings for all instances of the calendar. 
81            @param  settings  object - the new settings to use as defaults (anonymous object)
82            @return void */
83         setDefaults: function(settings) {
84                 $.extend(this._defaults, settings || {});
85         },
87         /* Handle keystrokes. */
88         _doKeyDown: function(e) {
89                 var inst = popUpCal._getInst(this._calId);
90                 if (popUpCal._popUpShowing) {
91                         switch (e.keyCode) {
92                                 case 9:  popUpCal.hideCalendar(inst, '');
93                                                 break; // hide on tab out
94                                 case 13: popUpCal._selectDate(inst);
95                                                 break; // select the value on enter
96                                 case 27: popUpCal.hideCalendar(inst, inst._get('speed'));
97                                                 break; // hide on escape
98                                 case 33: popUpCal._adjustDate(inst, -1, (e.ctrlKey ? 'Y' : 'M'));
99                                                 break; // previous month/year on page up/+ ctrl
100                                 case 34: popUpCal._adjustDate(inst, +1, (e.ctrlKey ? 'Y' : 'M'));
101                                                 break; // next month/year on page down/+ ctrl
102                                 case 35: if (e.ctrlKey) popUpCal._clearDate(inst);
103                                                 break; // clear on ctrl+end
104                                 case 36: if (e.ctrlKey) popUpCal._gotoToday(inst);
105                                                 break; // current on ctrl+home
106                                 case 37: if (e.ctrlKey) popUpCal._adjustDate(inst, -1, 'D');
107                                                 break; // -1 day on ctrl+left
108                                 case 38: if (e.ctrlKey) popUpCal._adjustDate(inst, -7, 'D');
109                                                 break; // -1 week on ctrl+up
110                                 case 39: if (e.ctrlKey) popUpCal._adjustDate(inst, +1, 'D');
111                                                 break; // +1 day on ctrl+right
112                                 case 40: if (e.ctrlKey) popUpCal._adjustDate(inst, +7, 'D');
113                                                 break; // +1 week on ctrl+down
114                         }
115                 }
116                 else if (e.keyCode == 36 && e.ctrlKey) { // display the calendar on ctrl+home
117                         popUpCal.showFor(this);
118                 }
119         },
121         /* Filter entered characters. */
122         _doKeyPress: function(e) {
123                 var inst = popUpCal._getInst(this._calId);
124                 var chr = String.fromCharCode(e.charCode == undefined ? e.keyCode : e.charCode);
125                 return (chr < ' ' || chr == inst._get('dateFormat').charAt(3) ||
126                         (chr >= '0' && chr <= '9')); // only allow numbers and separator
127         },
129         /* Attach the calendar to an input field. */
130         _connectCalendar: function(target, inst) {
131                 var $input = $(target);
132                 var appendText = inst._get('appendText');
133                 if (appendText) {
134                         $input.after('<span class="calendar_append">' + appendText + '</span>');
135                 }
136                 var autoPopUp = inst._get('autoPopUp');
137                 if (autoPopUp == 'focus' || autoPopUp == 'both') { // pop-up calendar when in the marked field
138                         $input.focus(this.showFor);
139                 }
140                 if (autoPopUp == 'button' || autoPopUp == 'both') { // pop-up calendar when button clicked
141                         var buttonText = inst._get('buttonText');
142                         var buttonImage = inst._get('buttonImage');
143                         var buttonImageOnly = inst._get('buttonImageOnly');
144                         var trigger = $(buttonImageOnly ? '<img class="calendar_trigger" src="' +
145                                 buttonImage + '" alt="' + buttonText + '" title="' + buttonText + '"/>' :
146                                 '<button type="button" class="calendar_trigger">' + (buttonImage != '' ?
147                                 '<img src="' + buttonImage + '" alt="' + buttonText + '" title="' + buttonText + '"/>' :
148                                 buttonText) + '</button>');
149                         $input.wrap('<span class="calendar_wrap"></span>').after(trigger);
150                         trigger.click(this.showFor);
151                 }
152                 $input.keydown(this._doKeyDown).keypress(this._doKeyPress);
153                 $input[0]._calId = inst._id;
154         },
156         /* Attach an inline calendar to a div. */
157         _inlineCalendar: function(target, inst) {
158                 $(target).append(inst._calendarDiv);
159                 target._calId = inst._id;
160                 var date = new Date();
161                 inst._selectedDay = date.getDate();
162                 inst._selectedMonth = date.getMonth();
163                 inst._selectedYear = date.getFullYear();
164                 popUpCal._adjustDate(inst);
165         },
167         /* Pop-up the calendar in a "dialog" box.
168            @param  dateText  string - the initial date to display (in the current format)
169            @param  onSelect  function - the function(dateText) to call when a date is selected
170            @param  settings  object - update the dialog calendar instance's settings (anonymous object)
171            @param  pos       int[2] - coordinates for the dialog's position within the screen
172                         leave empty for default (screen centre)
173            @return void */
174         dialogCalendar: function(dateText, onSelect, settings, pos) {
175                 var inst = this._dialogInst; // internal instance
176                 if (!inst) {
177                         inst = this._dialogInst = new PopUpCalInstance({}, false);
178                         this._dialogInput = $('<input type="text" size="1" style="position: absolute; top: -100px;"/>');
179                         this._dialogInput.keydown(this._doKeyDown);
180                         $('body').append(this._dialogInput);
181                         this._dialogInput[0]._calId = inst._id;
182                 }
183                 $.extend(inst._settings, settings || {});
184                 this._dialogInput.val(dateText);
185                 
186                 /*      Cross Browser Positioning */
187                 if (self.innerHeight) { // all except Explorer
188                         windowWidth = self.innerWidth;
189                         windowHeight = self.innerHeight;
190                 } else if (document.documentElement && document.documentElement.clientHeight) { // Explorer 6 Strict Mode
191                         windowWidth = document.documentElement.clientWidth;
192                         windowHeight = document.documentElement.clientHeight;
193                 } else if (document.body) { // other Explorers
194                         windowWidth = document.body.clientWidth;
195                         windowHeight = document.body.clientHeight;
196                 } 
197                 this._pos = pos || // should use actual width/height below
198                         [(windowWidth / 2) - 100, (windowHeight / 2) - 100];
200                 // move input on screen for focus, but hidden behind dialog
201                 this._dialogInput.css('left', this._pos[0] + 'px').css('top', this._pos[1] + 'px');
202                 inst._settings.onSelect = onSelect;
203                 this._inDialog = true;
204                 this._calendarDiv.addClass('calendar_dialog');
205                 this.showFor(this._dialogInput[0]);
206                 if ($.blockUI) {
207                         $.blockUI(this._calendarDiv);
208                 }
209         },
211         /* Enable the input field(s) for entry.
212            @param  inputs  element/object - single input field or jQuery collection of input fields
213            @return void */
214         enableFor: function(inputs) {
215                 inputs = (inputs.jquery ? inputs : $(inputs));
216                 inputs.each(function() {
217                         this.disabled = false;
218                         $('../button.calendar_trigger', this).each(function() { this.disabled = false; });
219                         $('../img.calendar_trigger',
220 this).css({opacity:'1.0',cursor:''});
221                         var $this = this;
222                         popUpCal._disabledInputs = $.map(popUpCal._disabledInputs,
223                                 function(value) { return (value == $this ? null : value); }); // delete entry
224                 });
225         },
227         /* Disable the input field(s) from entry.
228            @param  inputs  element/object - single input field or jQuery collection of input fields
229            @return void */
230         disableFor: function(inputs) {
231                 inputs = (inputs.jquery ? inputs : $(inputs));
232                 inputs.each(function() {
233                         this.disabled = true;
234                         $('../button.calendar_trigger', this).each(function() { this.disabled = true; });
235                         $('../img.calendar_trigger', this).css({opacity:'0.5',cursor:'default'});
236                         var $this = this;
237                         popUpCal._disabledInputs = $.map(popUpCal._disabledInputs,
238                                 function(value) { return (value == $this ? null : value); }); // delete entry
239                         popUpCal._disabledInputs[popUpCal._disabledInputs.length] = this;
240                 });
241         },
243         /* Update the settings for a calendar attached to an input field or division.
244            @param  control   element - the input field or div/span attached to the calendar
245            @param  settings  object - the new settings to update
246            @return void */
247         reconfigureFor: function(control, settings) {
248                 var inst = this._getInst(control._calId);
249                 if (inst) {
250                         $.extend(inst._settings, settings || {});
251                         this._updateCalendar(inst);
252                 }
253         },
255         /* Set the date for a calendar attached to an input field or division.
256            @param  control  element - the input field or div/span attached to the calendar
257            @param  date     Date - the new date
258            @return void */
259         setDateFor: function(control, date) {
260                 var inst = this._getInst(control._calId);
261                 if (inst) {
262                         inst._setDate(date);
263                 }
264         },
266         /* Retrieve the date for a calendar attached to an input field or division.
267            @param  control  element - the input field or div/span attached to the calendar
268            @return Date - the current date */
269         getDateFor: function(control) {
270                 var inst = this._getInst(control._calId);
271                 return (inst ? inst._getDate() : null);
272         },
274         /* Pop-up the calendar for a given input field.
275            @param  target  element - the input field attached to the calendar
276            @return void */
277         showFor: function(target) {
278                 var input = (target.nodeName && target.nodeName.toLowerCase() == 'input' ? target : this);
279                 if (input.nodeName.toLowerCase() != 'input') { // find from button/image trigger
280                         input = $('input', input.parentNode)[0];
281                 }
282                 if (popUpCal._lastInput == input) { // already here
283                         return;
284                 }
285                 for (var i = 0; i < popUpCal._disabledInputs.length; i++) {  // check not disabled
286                         if (popUpCal._disabledInputs[i] == input) {
287                                 return;
288                         }
289                 }
290                 var inst = popUpCal._getInst(input._calId);
291                 popUpCal.hideCalendar(inst, '');
292                 popUpCal._lastInput = input;
293                 inst._setDateFromField(input);
294                 if (popUpCal._inDialog) { // hide cursor
295                         input.value = '';
296                 }
297                 if (!popUpCal._pos) { // position below input
298                         popUpCal._pos = popUpCal._findPos(input);
299                         popUpCal._pos[1] += input.offsetHeight;
300                 }
301                 inst._calendarDiv.css('position', (popUpCal._inDialog && $.blockUI ? 'static' : 'absolute')).
302                         css('left', popUpCal._pos[0] + 'px').css('top', popUpCal._pos[1] + 'px');
303                 popUpCal._pos = null;
304                 var fieldSettings = inst._get('fieldSettings');
305                 $.extend(inst._settings, (fieldSettings ? fieldSettings(input) : {}));
306                 popUpCal._showCalendar(inst);
307         },
309         /* Construct and display the calendar. */
310         _showCalendar: function(id) {
311                 var inst = this._getInst(id);
312                 popUpCal._updateCalendar(inst);
313                 if (!inst._inline) {
314                         var speed = inst._get('speed');
315                         inst._calendarDiv.show(speed, function() {
316                                 popUpCal._popUpShowing = true;
317                                 popUpCal._afterShow(inst);
318                         });
319                         if (speed == '') {
320                                 popUpCal._popUpShowing = true;
321                                 popUpCal._afterShow(inst);
322                         }
323                         if (inst._input[0].type != 'hidden') {
324                                 inst._input[0].focus();
325                         }
326                         this._curInst = inst;
327                 }
328         },
330         /* Generate the calendar content. */
331         _updateCalendar: function(inst) {
332                 inst._calendarDiv.empty().append(inst._generateCalendar());
333                 if (inst._input && inst._input != 'hidden') {
334                         inst._input[0].focus();
335                 }
336         },
338         /* Tidy up after displaying the calendar. */
339         _afterShow: function(inst) {
340                 if ($.browser.msie) { // fix IE < 7 select problems
341                         $('#calendar_cover').css({width: inst._calendarDiv[0].offsetWidth + 4,
342                                 height: inst._calendarDiv[0].offsetHeight + 4});
343                 }
344                 /*// re-position on screen if necessary
345                 var calDiv = inst._calendarDiv[0];
346                 var pos = popUpCal._findPos(inst._input[0]);
347                 if ((calDiv.offsetLeft + calDiv.offsetWidth) >
348                                 (document.body.clientWidth + document.body.scrollLeft)) {
349                         inst._calendarDiv.css('left', (pos[0] + inst._input[0].offsetWidth - calDiv.offsetWidth) + 'px');
350                 }
351                 if ((calDiv.offsetTop + calDiv.offsetHeight) >
352                                 (document.body.clientHeight + document.body.scrollTop)) {
353                         inst._calendarDiv.css('top', (pos[1] - calDiv.offsetHeight) + 'px');
354                 }*/
355         },
357         /* Hide the calendar from view.
358            @param  id     string/object - the ID of the current calendar instance,
359                         or the instance itself
360            @param  speed  string - the speed at which to close the calendar
361            @return void */
362         hideCalendar: function(id, speed) {
363                 var inst = this._getInst(id);
364                 if (popUpCal._popUpShowing) {
365                         speed = (speed != null ? speed : inst._get('speed'));
366                         inst._calendarDiv.hide(speed, function() {
367                                 popUpCal._tidyDialog(inst);
368                         });
369                         if (speed == '') {
370                                 popUpCal._tidyDialog(inst);
371                         }
372                         popUpCal._popUpShowing = false;
373                         popUpCal._lastInput = null;
374                         inst._settings.prompt = null;
375                         if (popUpCal._inDialog) {
376                                 popUpCal._dialogInput.css('position', 'absolute').
377                                         css('left', '0px').css('top', '-100px');
378                                 if ($.blockUI) {
379                                         $.unblockUI();
380                                         $('body').append(this._calendarDiv);
381                                 }
382                         }
383                         popUpCal._inDialog = false;
384                 }
385                 popUpCal._curInst = null;
386         },
388         /* Tidy up after a dialog display. */
389         _tidyDialog: function(inst) {
390                 inst._calendarDiv.removeClass('calendar_dialog');
391                 $('.calendar_prompt', inst._calendarDiv).remove();
392         },
394         /* Close calendar if clicked elsewhere. */
395         _checkExternalClick: function(event) {
396                 if (!popUpCal._curInst) {
397                         return;
398                 }
399                 var target = $(event.target);
400                 if( (target.parents("#calendar_div").length == 0)
401                         && (target.attr('class') != 'calendar_trigger')
402                         && popUpCal._popUpShowing 
403                         && !(popUpCal._inDialog && $.blockUI) )
404                 {
405                         popUpCal.hideCalendar(popUpCal._curInst, '');
406                 }
407         },
409         /* Adjust one of the date sub-fields. */
410         _adjustDate: function(id, offset, period) {
411                 var inst = this._getInst(id);
412                 inst._adjustDate(offset, period);
413                 this._updateCalendar(inst);
414         },
416         /* Action for current link. */
417         _gotoToday: function(id) {
418                 var date = new Date();
419                 var inst = this._getInst(id);
420                 inst._selectedDay = date.getDate();
421                 inst._selectedMonth = date.getMonth();
422                 inst._selectedYear = date.getFullYear();
423                 this._adjustDate(inst);
424         },
426         /* Action for selecting a new month/year. */
427         _selectMonthYear: function(id, select, period) {
428                 var inst = this._getInst(id);
429                 inst._selectingMonthYear = false;
430                 inst[period == 'M' ? '_selectedMonth' : '_selectedYear'] =
431                         select.options[select.selectedIndex].value - 0;
432                 this._adjustDate(inst);
433         },
435         /* Restore input focus after not changing month/year. */
436         _clickMonthYear: function(id) {
437                 var inst = this._getInst(id);
438                 if (inst._input && inst._selectingMonthYear && !$.browser.msie) {
439                         inst._input[0].focus();
440                 }
441                 inst._selectingMonthYear = !inst._selectingMonthYear;
442         },
444         /* Action for changing the first week day. */
445         _changeFirstDay: function(id, a) {
446                 var inst = this._getInst(id);
447                 var dayNames = inst._get('dayNames');
448                 var value = a.firstChild.nodeValue;
449                 for (var i = 0; i < 7; i++) {
450                         if (dayNames[i] == value) {
451                                 inst._settings.firstDay = i;
452                                 break;
453                         }
454                 }
455                 this._updateCalendar(inst);
456         },
458         /* Action for selecting a day. */
459         _selectDay: function(id, td) {
460                 var inst = this._getInst(id);
461                 inst._selectedDay = $("a", td).html();
462                 this._selectDate(id);
463         },
465         /* Erase the input field and hide the calendar. */
466         _clearDate: function(id) {
467                 this._selectDate(id, '');
468         },
470         /* Update the input field with the selected date. */
471         _selectDate: function(id, dateStr) {
472                 var inst = this._getInst(id);
473                 dateStr = (dateStr != null ? dateStr : inst._formatDate());
474                 if (inst._input) {
475                         inst._input.val(dateStr);
476                 }
477                 var onSelect = inst._get('onSelect');
478                 if (onSelect) {
479                         onSelect(dateStr);  // trigger custom callback
480                 }
481                 else {
482                         inst._input.trigger('change'); // fire the change event
483                 }
484                 if (inst._inline) {
485                         this._updateCalendar(inst);
486                 }
487                 else {
488                         this.hideCalendar(inst, inst._get('speed'));
489                 }
490         },
492         /* Set as customDate function to prevent selection of weekends.
493            @param  date  Date - the date to customise
494            @return [boolean, string] - is this date selectable?, what is its CSS class? */
495         noWeekends: function(date) {
496                 var day = date.getDay();
497                 return [(day > 0 && day < 6), ''];
498         },
500         /* Find an object's position on the screen. */
501         _findPos: function(obj) {
502                 if (obj.type == 'hidden') {
503                         obj = obj.nextSibling;
504                 }
505                 var curleft = curtop = 0;
506                 if (obj.offsetParent) {
507                         curleft = obj.offsetLeft;
508                         curtop = obj.offsetTop;
509                         while (obj = obj.offsetParent) {
510                                 var origcurleft = curleft;
511                                 curleft += obj.offsetLeft;
512                                 if (curleft < 0) {
513                                         curleft = origcurleft;
514                                 }
515                                 curtop += obj.offsetTop;
516                         }
517                 }
518                 return [curleft,curtop];
519         }
522 /* Individualised settings for calendars applied to one or more related inputs.
523    Instances are managed and manipulated through the PopUpCal manager. */
524 function PopUpCalInstance(settings, inline) {
525         this._id = popUpCal._register(this);
526         this._selectedDay = 0;
527         this._selectedMonth = 0; // 0-11
528         this._selectedYear = 0; // 4-digit year
529         this._input = null; // The attached input field
530         this._inline = inline; // True if showing inline, false if used in a popup
531         this._calendarDiv = (!inline ? popUpCal._calendarDiv :
532                 $('<div id="calendar_div_' + this._id + '" class="calendar_inline"></div>'));
533         if (inline) {
534                 var date = new Date();
535                 this._currentDay = date.getDate();
536                 this._currentMonth = date.getMonth();
537                 this._currentYear = date.getFullYear();
538         }
539         // customise the calendar object - uses manager defaults if not overridden
540         this._settings = $.extend({}, settings || {}); // clone
543 $.extend(PopUpCalInstance.prototype, {
544         /* Get a setting value, defaulting if necessary. */
545         _get: function(name) {
546                 return (this._settings[name] != null ? this._settings[name] : popUpCal._defaults[name]);
547         },
549         /* Parse existing date and initialise calendar. */
550         _setDateFromField: function(input) {
551                 this._input = $(input);
552                 var dateFormat = this._get('dateFormat');
553                 var currentDate = this._input.val().split(dateFormat.charAt(3));
554                 if (currentDate.length == 3) {
555                         this._currentDay = parseInt(currentDate[dateFormat.indexOf('D')], 10);
556                         this._currentMonth = parseInt(currentDate[dateFormat.indexOf('M')], 10) - 1;
557                         this._currentYear = parseInt(currentDate[dateFormat.indexOf('Y')], 10);
558                 }
559                 else {
560                         var date = new Date();
561                         this._currentDay = date.getDate();
562                         this._currentMonth = date.getMonth();
563                         this._currentYear = date.getFullYear();
564                 }
565                 this._selectedDay = this._currentDay;
566                 this._selectedMonth = this._currentMonth;
567                 this._selectedYear = this._currentYear;
568                 this._adjustDate();
569         },
571         /* Set the date directly. */
572         _setDate: function(date) {
573                 this._selectedDay = this._currentDay = date.getDate();
574                 this._selectedMonth = this._currentMonth = date.getMonth();
575                 this._selectedYear = this._currentYear = date.getFullYear();
576                 this._adjustDate();
577         },
579         /* Retrieve the date directly. */
580         _getDate: function() {
581                 return new Date(this._currentYear, this._currentMonth, this._currentDay);
582         },
584         /* Generate the HTML for the current state of the calendar. */
585         _generateCalendar: function() {
586                 var today = new Date();
587                 today = new Date(today.getFullYear(), today.getMonth(), today.getDate()); // clear time
588                 // build the calendar HTML
589                 var controls = '<div class="calendar_control">' +
590                         '<a class="calendar_clear" onclick="popUpCal._clearDate(' + this._id + ');">' +
591                         this._get('clearText') + '</a>' +
592                         '<a class="calendar_close" onclick="popUpCal.hideCalendar(' + this._id + ');">' +
593                         this._get('closeText') + '</a></div>';
594                 var prompt = this._get('prompt');
595                 var closeAtTop = this._get('closeAtTop');
596                 var hideIfNoPrevNext = this._get('hideIfNoPrevNext');
597                 // controls and links
598                 var html = (prompt ? '<div class="calendar_prompt">' + prompt + '</div>' : '') +
599                         (closeAtTop && !this._inline ? controls : '') + '<div class="calendar_links">' +
600                         (this._canAdjustMonth(-1) ? '<a class="calendar_prev" ' +
601                         'onclick="popUpCal._adjustDate(' + this._id + ', -1, \'M\');">' + this._get('prevText') + '</a>' :
602                         (hideIfNoPrevNext ? '' : '<label class="calendar_prev">' + this._get('prevText') + '</label>')) +
603                         (this._isInRange(today) ? '<a class="calendar_current" ' +
604                         'onclick="popUpCal._gotoToday(' + this._id + ');">' + this._get('currentText') + '</a>' : '') +
605                         (this._canAdjustMonth(+1) ? '<a class="calendar_next" ' +
606                         'onclick="popUpCal._adjustDate(' + this._id + ', +1, \'M\');">' + this._get('nextText') + '</a>' :
607                         (hideIfNoPrevNext ? '' : '<label class="calendar_next">' + this._get('nextText') + '</label>')) +
608                         '</div><div class="calendar_header">';
609                 var minDate = this._get('minDate');
610                 var maxDate = this._get('maxDate');
611                 // month selection
612                 var monthNames = this._get('monthNames');
613                 if (!this._get('changeMonth')) {
614                         html += monthNames[this._selectedMonth] + '&nbsp;';
615                 }
616                 else {
617                         var inMinYear = (minDate && minDate.getFullYear() == this._selectedYear);
618                         var inMaxYear = (maxDate && maxDate.getFullYear() == this._selectedYear);
619                         html += '<select class="calendar_newMonth" ' +
620                                 'onchange="popUpCal._selectMonthYear(' + this._id + ', this, \'M\');" ' +
621                                 'onclick="popUpCal._clickMonthYear(' + this._id + ');">';
622                         for (var month = 0; month < 12; month++) {
623                                 if ((!inMinYear || month >= minDate.getMonth()) &&
624                                                 (!inMaxYear || month <= maxDate.getMonth())) {
625                                         html += '<option value="' + month + '"' +
626                                                 (month == this._selectedMonth ? ' selected="selected"' : '') +
627                                                 '>' + monthNames[month] + '</option>';
628                                 }
629                         }
630                         html += '</select>';
631                 }
632                 // year selection
633                 if (!this._get('changeYear')) {
634                         html += this._selectedYear;
635                 }
636                 else {
637                         // determine range of years to display
638                         var years = this._get('yearRange').split(':');
639                         var year = 0;
640                         var endYear = 0;
641                         if (years.length != 2) {
642                                 year = this._selectedYear - 10;
643                                 endYear = this._selectedYear + 10;
644                         }
645                         else if (years[0].charAt(0) == '+' || years[0].charAt(0) == '-') {
646                                 year = this._selectedYear + parseInt(years[0], 10);
647                                 endYear = this._selectedYear + parseInt(years[1], 10);
648                         }
649                         else {
650                                 year = parseInt(years[0], 10);
651                                 endYear = parseInt(years[1], 10);
652                         }
653                         year = (minDate ? Math.max(year, minDate.getFullYear()) : year);
654                         endYear = (maxDate ? Math.min(endYear, maxDate.getFullYear()) : endYear);
655                         html += '<select class="calendar_newYear" onchange="popUpCal._selectMonthYear(' +
656                                 this._id + ', this, \'Y\');" ' + 'onclick="popUpCal._clickMonthYear(' +
657                                 this._id + ');">';
658                         for (; year <= endYear; year++) {
659                                 html += '<option value="' + year + '"' +
660                                         (year == this._selectedYear ? ' selected="selected"' : '') +
661                                         '>' + year + '</option>';
662                         }
663                         html += '</select>';
664                 }
665                 html += '</div><table class="calendar" cellpadding="0" cellspacing="0"><thead>' +
666                         '<tr class="calendar_titleRow">';
667                 var firstDay = this._get('firstDay');
668                 var changeFirstDay = this._get('changeFirstDay');
669                 var dayNames = this._get('dayNames');
670                 for (var dow = 0; dow < 7; dow++) { // days of the week
671                         html += '<td>' + (!changeFirstDay ? '' : '<a onclick="popUpCal._changeFirstDay(' +
672                                 this._id + ', this);">') + dayNames[(dow + firstDay) % 7] +
673                                 (changeFirstDay ? '</a>' : '') + '</td>';
674                 }
675                 html += '</tr></thead><tbody>';
676                 var daysInMonth = this._getDaysInMonth(this._selectedYear, this._selectedMonth);
677                 this._selectedDay = Math.min(this._selectedDay, daysInMonth);
678                 var leadDays = (this._getFirstDayOfMonth(this._selectedYear, this._selectedMonth) - firstDay + 7) % 7;
679                 var currentDate = new Date(this._currentYear, this._currentMonth, this._currentDay);
680                 var selectedDate = new Date(this._selectedYear, this._selectedMonth, this._selectedDay);
681                 var printDate = new Date(this._selectedYear, this._selectedMonth, 1 - leadDays);
682                 var numRows = Math.ceil((leadDays + daysInMonth) / 7); // calculate the number of rows to generate
683                 var customDate = this._get('customDate');
684                 var showOtherMonths = this._get('showOtherMonths');
685                 for (var row = 0; row < numRows; row++) { // create calendar rows
686                         html += '<tr class="calendar_daysRow">';
687                         for (var dow = 0; dow < 7; dow++) { // create calendar days
688                                 var customSettings = (customDate ? customDate(printDate) : [true, '']);
689                                 var otherMonth = (printDate.getMonth() != this._selectedMonth);
690                                 var unselectable = otherMonth || !customSettings[0] ||
691                                         (minDate && printDate < minDate) || (maxDate && printDate > maxDate);
692                                 html += '<td class="calendar_daysCell' +
693                                         ((dow + firstDay + 6) % 7 >= 5 ? ' calendar_weekEndCell' : '') + // highlight weekends
694                                         (otherMonth ? ' calendar_otherMonth' : '') + // highlight days from other months
695                                         (printDate.getTime() == selectedDate.getTime() ? ' calendar_daysCellOver' : '') + // highlight selected day
696                                         (unselectable ? ' calendar_unselectable' : '') +  // highlight unselectable days
697                                         (!otherMonth || showOtherMonths ? ' ' + customSettings[1] : '') + // highlight custom dates
698                                         (printDate.getTime() == currentDate.getTime() ? ' calendar_currentDay' : // highlight current day
699                                         (printDate.getTime() == today.getTime() ? ' calendar_today' : '')) + '"' + // highlight today (if different)
700                                         (unselectable ? '' : ' onmouseover="$(this).addClass(\'calendar_daysCellOver\');"' +
701                                         ' onmouseout="$(this).removeClass(\'calendar_daysCellOver\');"' +
702                                         ' onclick="popUpCal._selectDay(' + this._id + ', this);"') + '>' + // actions
703                                         (otherMonth ? (showOtherMonths ? printDate.getDate() : '&nbsp;') : // display for other months
704                                         (unselectable ? printDate.getDate() : '<a>' + printDate.getDate() + '</a>')) + '</td>'; // display for this month
705                                 printDate.setDate(printDate.getDate() + 1);
706                         }
707                         html += '</tr>';
708                 }
709                 html += '</tbody></table>' + (!closeAtTop && !this._inline ? controls : '') +
710                         '<div style="clear: both;"></div>' + (!$.browser.msie ? '' :
711                         '<!--[if lte IE 6.5]><iframe src="javascript:false;" class="calendar_cover"></iframe><![endif]-->');
712                 return html;
713         },
715         /* Adjust one of the date sub-fields. */
716         _adjustDate: function(offset, period) {
717                 var date = new Date(this._selectedYear + (period == 'Y' ? offset : 0),
718                         this._selectedMonth + (period == 'M' ? offset : 0),
719                         this._selectedDay + (period == 'D' ? offset : 0));
720                 // ensure it is within the bounds set
721                 var minDate = this._get('minDate');
722                 var maxDate = this._get('maxDate');
723                 date = (minDate && date < minDate ? minDate : date);
724                 date = (maxDate && date > maxDate ? maxDate : date);
725                 this._selectedDay = date.getDate();
726                 this._selectedMonth = date.getMonth();
727                 this._selectedYear = date.getFullYear();
728         },
730         /* Find the number of days in a given month. */
731         _getDaysInMonth: function(year, month) {
732                 return 32 - new Date(year, month, 32).getDate();
733         },
735         /* Find the day of the week of the first of a month. */
736         _getFirstDayOfMonth: function(year, month) {
737                 return new Date(year, month, 1).getDay();
738         },
740         /* Determines if we should allow a "next/prev" month display change. */
741         _canAdjustMonth: function(offset) {
742                 var date = new Date(this._selectedYear, this._selectedMonth + offset, 1);
743                 if (offset < 0) {
744                         date.setDate(this._getDaysInMonth(date.getFullYear(), date.getMonth()));
745                 }
746                 return this._isInRange(date);
747         },
749         /* Is the given date in the accepted range? */
750         _isInRange: function(date) {
751                 var minDate = this._get('minDate');
752                 var maxDate = this._get('maxDate');
753                 return ((!minDate || date >= minDate) && (!maxDate || date <= maxDate));
754         },
756         /* Format the given date for display. */
757         _formatDate: function() {
758                 var day = this._currentDay = this._selectedDay;
759                 var month = this._currentMonth = this._selectedMonth;
760                 var year = this._currentYear = this._selectedYear;
761                 month++; // adjust javascript month
762                 var dateFormat = this._get('dateFormat');
763                 var dateString = '';
764                 for (var i = 0; i < 3; i++) {
765                         dateString += dateFormat.charAt(3) +
766                                 (dateFormat.charAt(i) == 'D' ? (day < 10 ? '0' : '') + day :
767                                 (dateFormat.charAt(i) == 'M' ? (month < 10 ? '0' : '') + month :
768                                 (dateFormat.charAt(i) == 'Y' ? year : '?')));
769                 }
770                 return dateString.substring(dateFormat.charAt(3) ? 1 : 0);
771         }
774 /* Attach the calendar to a jQuery selection.
775    @param  settings  object - the new settings to use for this calendar instance (anonymous)
776    @return jQuery object - for chaining further calls */
777 $.fn.calendar = function(settings) {
778         return this.each(function() {
779                 // check for settings on the control itself - in namespace 'cal:'
780                 var inlineSettings = null;
781                 for (attrName in popUpCal._defaults) {
782                         var attrValue = this.getAttribute('cal:' + attrName);
783                         if (attrValue) {
784                                 inlineSettings = inlineSettings || {};
785                                 try {
786                                         inlineSettings[attrName] = eval(attrValue);
787                                 }
788                                 catch (err) {
789                                         inlineSettings[attrName] = attrValue;
790                                 }
791                         }
792                 }
793                 var nodeName = this.nodeName.toLowerCase();
794                 if (nodeName == 'input') {
795                         var instSettings = (inlineSettings ? $.extend($.extend({}, settings || {}),
796                                 inlineSettings || {}) : settings); // clone and customise
797                         var inst = (inst && !inlineSettings ? inst :
798                                 new PopUpCalInstance(instSettings, false));
799                         popUpCal._connectCalendar(this, inst);
800                 } 
801                 else if (nodeName == 'div' || nodeName == 'span') {
802                         var instSettings = $.extend($.extend({}, settings || {}),
803                                 inlineSettings || {}); // clone and customise
804                         var inst = new PopUpCalInstance(instSettings, true);
805                         popUpCal._inlineCalendar(this, inst);
806                 }
807         });
810 /* Initialise the calendar. */
811 $(document).ready(function() {
812    popUpCal = new PopUpCal(); // singleton instance