calendar popups!
[freeside.git] / httemplate / elements / calendar.js
1 /*  Copyright Mihai Bazon, 2002, 2003  |  http://students.infoiasi.ro/~mishoo
2  * ---------------------------------------------------------------------------
3  *
4  * The DHTML Calendar, version 0.9.3 "It's still alive & keeps rocking"
5  *
6  * Details and latest version at:
7  * http://students.infoiasi.ro/~mishoo/site/calendar.epl
8  *
9  * Feel free to use this script under the terms of the GNU Lesser General
10  * Public License, as long as you do not remove or alter this notice.
11  */
12
13 // $Id: calendar.js,v 1.2 2003-09-30 08:23:15 ivan Exp $
14
15 /** The Calendar object constructor. */
16 Calendar = function (mondayFirst, dateStr, onSelected, onClose) {
17         // member variables
18         this.activeDiv = null;
19         this.currentDateEl = null;
20         this.checkDisabled = null;
21         this.timeout = null;
22         this.onSelected = onSelected || null;
23         this.onClose = onClose || null;
24         this.dragging = false;
25         this.hidden = false;
26         this.minYear = 1970;
27         this.maxYear = 2050;
28         this.dateFormat = Calendar._TT["DEF_DATE_FORMAT"];
29         this.ttDateFormat = Calendar._TT["TT_DATE_FORMAT"];
30         this.isPopup = true;
31         this.weekNumbers = true;
32         this.mondayFirst = mondayFirst;
33         this.dateStr = dateStr;
34         this.ar_days = null;
35         // HTML elements
36         this.table = null;
37         this.element = null;
38         this.tbody = null;
39         this.firstdayname = null;
40         // Combo boxes
41         this.monthsCombo = null;
42         this.yearsCombo = null;
43         this.hilitedMonth = null;
44         this.activeMonth = null;
45         this.hilitedYear = null;
46         this.activeYear = null;
47         // Information
48         this.dateClicked = false;
49
50         // one-time initializations
51         if (!Calendar._DN3) {
52                 // table of short day names
53                 var ar = new Array();
54                 for (var i = 8; i > 0;) {
55                         ar[--i] = Calendar._DN[i].substr(0, 3);
56                 }
57                 Calendar._DN3 = ar;
58                 // table of short month names
59                 ar = new Array();
60                 for (var i = 12; i > 0;) {
61                         ar[--i] = Calendar._MN[i].substr(0, 3);
62                 }
63                 Calendar._MN3 = ar;
64         }
65 };
66
67 // ** constants
68
69 /// "static", needed for event handlers.
70 Calendar._C = null;
71
72 /// detect a special case of "web browser"
73 Calendar.is_ie = ( /msie/i.test(navigator.userAgent) &&
74                    !/opera/i.test(navigator.userAgent) );
75
76 // short day names array (initialized at first constructor call)
77 Calendar._DN3 = null;
78
79 // short month names array (initialized at first constructor call)
80 Calendar._MN3 = null;
81
82 // BEGIN: UTILITY FUNCTIONS; beware that these might be moved into a separate
83 //        library, at some point.
84
85 Calendar.getAbsolutePos = function(el) {
86         var r = { x: el.offsetLeft, y: el.offsetTop };
87         if (el.offsetParent) {
88                 var tmp = Calendar.getAbsolutePos(el.offsetParent);
89                 r.x += tmp.x;
90                 r.y += tmp.y;
91         }
92         return r;
93 };
94
95 Calendar.isRelated = function (el, evt) {
96         var related = evt.relatedTarget;
97         if (!related) {
98                 var type = evt.type;
99                 if (type == "mouseover") {
100                         related = evt.fromElement;
101                 } else if (type == "mouseout") {
102                         related = evt.toElement;
103                 }
104         }
105         while (related) {
106                 if (related == el) {
107                         return true;
108                 }
109                 related = related.parentNode;
110         }
111         return false;
112 };
113
114 Calendar.removeClass = function(el, className) {
115         if (!(el && el.className)) {
116                 return;
117         }
118         var cls = el.className.split(" ");
119         var ar = new Array();
120         for (var i = cls.length; i > 0;) {
121                 if (cls[--i] != className) {
122                         ar[ar.length] = cls[i];
123                 }
124         }
125         el.className = ar.join(" ");
126 };
127
128 Calendar.addClass = function(el, className) {
129         Calendar.removeClass(el, className);
130         el.className += " " + className;
131 };
132
133 Calendar.getElement = function(ev) {
134         if (Calendar.is_ie) {
135                 return window.event.srcElement;
136         } else {
137                 return ev.currentTarget;
138         }
139 };
140
141 Calendar.getTargetElement = function(ev) {
142         if (Calendar.is_ie) {
143                 return window.event.srcElement;
144         } else {
145                 return ev.target;
146         }
147 };
148
149 Calendar.stopEvent = function(ev) {
150         if (Calendar.is_ie) {
151                 window.event.cancelBubble = true;
152                 window.event.returnValue = false;
153         } else {
154                 ev.preventDefault();
155                 ev.stopPropagation();
156         }
157         return false;
158 };
159
160 Calendar.addEvent = function(el, evname, func) {
161         if (el.attachEvent) { // IE
162                 el.attachEvent("on" + evname, func);
163         } else if (el.addEventListener) { // Gecko / W3C
164                 el.addEventListener(evname, func, true);
165         } else { // Opera (or old browsers)
166                 el["on" + evname] = func;
167         }
168 };
169
170 Calendar.removeEvent = function(el, evname, func) {
171         if (el.detachEvent) { // IE
172                 el.detachEvent("on" + evname, func);
173         } else if (el.removeEventListener) { // Gecko / W3C
174                 el.removeEventListener(evname, func, true);
175         } else { // Opera (or old browsers)
176                 el["on" + evname] = null;
177         }
178 };
179
180 Calendar.createElement = function(type, parent) {
181         var el = null;
182         if (document.createElementNS) {
183                 // use the XHTML namespace; IE won't normally get here unless
184                 // _they_ "fix" the DOM2 implementation.
185                 el = document.createElementNS("http://www.w3.org/1999/xhtml", type);
186         } else {
187                 el = document.createElement(type);
188         }
189         if (typeof parent != "undefined") {
190                 parent.appendChild(el);
191         }
192         return el;
193 };
194
195 // END: UTILITY FUNCTIONS
196
197 // BEGIN: CALENDAR STATIC FUNCTIONS
198
199 /** Internal -- adds a set of events to make some element behave like a button. */
200 Calendar._add_evs = function(el) {
201         with (Calendar) {
202                 addEvent(el, "mouseover", dayMouseOver);
203                 addEvent(el, "mousedown", dayMouseDown);
204                 addEvent(el, "mouseout", dayMouseOut);
205                 if (is_ie) {
206                         addEvent(el, "dblclick", dayMouseDblClick);
207                         el.setAttribute("unselectable", true);
208                 }
209         }
210 };
211
212 Calendar.findMonth = function(el) {
213         if (typeof el.month != "undefined") {
214                 return el;
215         } else if (typeof el.parentNode.month != "undefined") {
216                 return el.parentNode;
217         }
218         return null;
219 };
220
221 Calendar.findYear = function(el) {
222         if (typeof el.year != "undefined") {
223                 return el;
224         } else if (typeof el.parentNode.year != "undefined") {
225                 return el.parentNode;
226         }
227         return null;
228 };
229
230 Calendar.showMonthsCombo = function () {
231         var cal = Calendar._C;
232         if (!cal) {
233                 return false;
234         }
235         var cal = cal;
236         var cd = cal.activeDiv;
237         var mc = cal.monthsCombo;
238         if (cal.hilitedMonth) {
239                 Calendar.removeClass(cal.hilitedMonth, "hilite");
240         }
241         if (cal.activeMonth) {
242                 Calendar.removeClass(cal.activeMonth, "active");
243         }
244         var mon = cal.monthsCombo.getElementsByTagName("div")[cal.date.getMonth()];
245         Calendar.addClass(mon, "active");
246         cal.activeMonth = mon;
247         mc.style.left = cd.offsetLeft + "px";
248         mc.style.top = (cd.offsetTop + cd.offsetHeight) + "px";
249         mc.style.display = "block";
250 };
251
252 Calendar.showYearsCombo = function (fwd) {
253         var cal = Calendar._C;
254         if (!cal) {
255                 return false;
256         }
257         var cal = cal;
258         var cd = cal.activeDiv;
259         var yc = cal.yearsCombo;
260         if (cal.hilitedYear) {
261                 Calendar.removeClass(cal.hilitedYear, "hilite");
262         }
263         if (cal.activeYear) {
264                 Calendar.removeClass(cal.activeYear, "active");
265         }
266         cal.activeYear = null;
267         var Y = cal.date.getFullYear() + (fwd ? 1 : -1);
268         var yr = yc.firstChild;
269         var show = false;
270         for (var i = 12; i > 0; --i) {
271                 if (Y >= cal.minYear && Y <= cal.maxYear) {
272                         yr.firstChild.data = Y;
273                         yr.year = Y;
274                         yr.style.display = "block";
275                         show = true;
276                 } else {
277                         yr.style.display = "none";
278                 }
279                 yr = yr.nextSibling;
280                 Y += fwd ? 2 : -2;
281         }
282         if (show) {
283                 yc.style.left = cd.offsetLeft + "px";
284                 yc.style.top = (cd.offsetTop + cd.offsetHeight) + "px";
285                 yc.style.display = "block";
286         }
287 };
288
289 // event handlers
290
291 Calendar.tableMouseUp = function(ev) {
292         var cal = Calendar._C;
293         if (!cal) {
294                 return false;
295         }
296         if (cal.timeout) {
297                 clearTimeout(cal.timeout);
298         }
299         var el = cal.activeDiv;
300         if (!el) {
301                 return false;
302         }
303         var target = Calendar.getTargetElement(ev);
304         Calendar.removeClass(el, "active");
305         if (target == el || target.parentNode == el) {
306                 Calendar.cellClick(el);
307         }
308         var mon = Calendar.findMonth(target);
309         var date = null;
310         if (mon) {
311                 date = new Date(cal.date);
312                 if (mon.month != date.getMonth()) {
313                         date.setMonth(mon.month);
314                         cal.setDate(date);
315                         cal.dateClicked = false;
316                         cal.callHandler();
317                 }
318         } else {
319                 var year = Calendar.findYear(target);
320                 if (year) {
321                         date = new Date(cal.date);
322                         if (year.year != date.getFullYear()) {
323                                 date.setFullYear(year.year);
324                                 cal.setDate(date);
325                                 cal.dateClicked = false;
326                                 cal.callHandler();
327                         }
328                 }
329         }
330         with (Calendar) {
331                 removeEvent(document, "mouseup", tableMouseUp);
332                 removeEvent(document, "mouseover", tableMouseOver);
333                 removeEvent(document, "mousemove", tableMouseOver);
334                 cal._hideCombos();
335                 _C = null;
336                 return stopEvent(ev);
337         }
338 };
339
340 Calendar.tableMouseOver = function (ev) {
341         var cal = Calendar._C;
342         if (!cal) {
343                 return;
344         }
345         var el = cal.activeDiv;
346         var target = Calendar.getTargetElement(ev);
347         if (target == el || target.parentNode == el) {
348                 Calendar.addClass(el, "hilite active");
349                 Calendar.addClass(el.parentNode, "rowhilite");
350         } else {
351                 Calendar.removeClass(el, "active");
352                 Calendar.removeClass(el, "hilite");
353                 Calendar.removeClass(el.parentNode, "rowhilite");
354         }
355         var mon = Calendar.findMonth(target);
356         if (mon) {
357                 if (mon.month != cal.date.getMonth()) {
358                         if (cal.hilitedMonth) {
359                                 Calendar.removeClass(cal.hilitedMonth, "hilite");
360                         }
361                         Calendar.addClass(mon, "hilite");
362                         cal.hilitedMonth = mon;
363                 } else if (cal.hilitedMonth) {
364                         Calendar.removeClass(cal.hilitedMonth, "hilite");
365                 }
366         } else {
367                 var year = Calendar.findYear(target);
368                 if (year) {
369                         if (year.year != cal.date.getFullYear()) {
370                                 if (cal.hilitedYear) {
371                                         Calendar.removeClass(cal.hilitedYear, "hilite");
372                                 }
373                                 Calendar.addClass(year, "hilite");
374                                 cal.hilitedYear = year;
375                         } else if (cal.hilitedYear) {
376                                 Calendar.removeClass(cal.hilitedYear, "hilite");
377                         }
378                 }
379         }
380         return Calendar.stopEvent(ev);
381 };
382
383 Calendar.tableMouseDown = function (ev) {
384         if (Calendar.getTargetElement(ev) == Calendar.getElement(ev)) {
385                 return Calendar.stopEvent(ev);
386         }
387 };
388
389 Calendar.calDragIt = function (ev) {
390         var cal = Calendar._C;
391         if (!(cal && cal.dragging)) {
392                 return false;
393         }
394         var posX;
395         var posY;
396         if (Calendar.is_ie) {
397                 posY = window.event.clientY + document.body.scrollTop;
398                 posX = window.event.clientX + document.body.scrollLeft;
399         } else {
400                 posX = ev.pageX;
401                 posY = ev.pageY;
402         }
403         cal.hideShowCovered();
404         var st = cal.element.style;
405         st.left = (posX - cal.xOffs) + "px";
406         st.top = (posY - cal.yOffs) + "px";
407         return Calendar.stopEvent(ev);
408 };
409
410 Calendar.calDragEnd = function (ev) {
411         var cal = Calendar._C;
412         if (!cal) {
413                 return false;
414         }
415         cal.dragging = false;
416         with (Calendar) {
417                 removeEvent(document, "mousemove", calDragIt);
418                 removeEvent(document, "mouseover", stopEvent);
419                 removeEvent(document, "mouseup", calDragEnd);
420                 tableMouseUp(ev);
421         }
422         cal.hideShowCovered();
423 };
424
425 Calendar.dayMouseDown = function(ev) {
426         var el = Calendar.getElement(ev);
427         if (el.disabled) {
428                 return false;
429         }
430         var cal = el.calendar;
431         cal.activeDiv = el;
432         Calendar._C = cal;
433         if (el.navtype != 300) with (Calendar) {
434                 addClass(el, "hilite active");
435                 addEvent(document, "mouseover", tableMouseOver);
436                 addEvent(document, "mousemove", tableMouseOver);
437                 addEvent(document, "mouseup", tableMouseUp);
438         } else if (cal.isPopup) {
439                 cal._dragStart(ev);
440         }
441         if (el.navtype == -1 || el.navtype == 1) {
442                 cal.timeout = setTimeout("Calendar.showMonthsCombo()", 250);
443         } else if (el.navtype == -2 || el.navtype == 2) {
444                 cal.timeout = setTimeout((el.navtype > 0) ? "Calendar.showYearsCombo(true)" : "Calendar.showYearsCombo(false)", 250);
445         } else {
446                 cal.timeout = null;
447         }
448         return Calendar.stopEvent(ev);
449 };
450
451 Calendar.dayMouseDblClick = function(ev) {
452         Calendar.cellClick(Calendar.getElement(ev));
453         if (Calendar.is_ie) {
454                 document.selection.empty();
455         }
456 };
457
458 Calendar.dayMouseOver = function(ev) {
459         var el = Calendar.getElement(ev);
460         if (Calendar.isRelated(el, ev) || Calendar._C || el.disabled) {
461                 return false;
462         }
463         if (el.ttip) {
464                 if (el.ttip.substr(0, 1) == "_") {
465                         var date = null;
466                         with (el.calendar.date) {
467                                 date = new Date(getFullYear(), getMonth(), el.caldate);
468                         }
469                         el.ttip = date.print(el.calendar.ttDateFormat) + el.ttip.substr(1);
470                 }
471                 el.calendar.tooltips.firstChild.data = el.ttip;
472         }
473         if (el.navtype != 300) {
474                 Calendar.addClass(el, "hilite");
475                 if (el.caldate) {
476                         Calendar.addClass(el.parentNode, "rowhilite");
477                 }
478         }
479         return Calendar.stopEvent(ev);
480 };
481
482 Calendar.dayMouseOut = function(ev) {
483         with (Calendar) {
484                 var el = getElement(ev);
485                 if (isRelated(el, ev) || _C || el.disabled) {
486                         return false;
487                 }
488                 removeClass(el, "hilite");
489                 if (el.caldate) {
490                         removeClass(el.parentNode, "rowhilite");
491                 }
492                 el.calendar.tooltips.firstChild.data = _TT["SEL_DATE"];
493                 return stopEvent(ev);
494         }
495 };
496
497 /**
498  *  A generic "click" handler :) handles all types of buttons defined in this
499  *  calendar.
500  */
501 Calendar.cellClick = function(el) {
502         var cal = el.calendar;
503         var closing = false;
504         var newdate = false;
505         var date = null;
506         if (typeof el.navtype == "undefined") {
507                 Calendar.removeClass(cal.currentDateEl, "selected");
508                 Calendar.addClass(el, "selected");
509                 closing = (cal.currentDateEl == el);
510                 if (!closing) {
511                         cal.currentDateEl = el;
512                 }
513                 cal.date.setDate(el.caldate);
514                 date = cal.date;
515                 newdate = true;
516                 // a date was clicked
517                 cal.dateClicked = true;
518         } else {
519                 if (el.navtype == 200) {
520                         Calendar.removeClass(el, "hilite");
521                         cal.callCloseHandler();
522                         return;
523                 }
524                 date = (el.navtype == 0) ? new Date() : new Date(cal.date);
525                 // unless "today" was clicked, we assume no date was clicked so
526                 // the selected handler will know not to close the calenar when
527                 // in single-click mode.
528                 cal.dateClicked = (el.navtype == 0);
529                 var year = date.getFullYear();
530                 var mon = date.getMonth();
531                 function setMonth(m) {
532                         var day = date.getDate();
533                         var max = date.getMonthDays(m);
534                         if (day > max) {
535                                 date.setDate(max);
536                         }
537                         date.setMonth(m);
538                 };
539                 switch (el.navtype) {
540                     case -2:
541                         if (year > cal.minYear) {
542                                 date.setFullYear(year - 1);
543                         }
544                         break;
545                     case -1:
546                         if (mon > 0) {
547                                 setMonth(mon - 1);
548                         } else if (year-- > cal.minYear) {
549                                 date.setFullYear(year);
550                                 setMonth(11);
551                         }
552                         break;
553                     case 1:
554                         if (mon < 11) {
555                                 setMonth(mon + 1);
556                         } else if (year < cal.maxYear) {
557                                 date.setFullYear(year + 1);
558                                 setMonth(0);
559                         }
560                         break;
561                     case 2:
562                         if (year < cal.maxYear) {
563                                 date.setFullYear(year + 1);
564                         }
565                         break;
566                     case 100:
567                         cal.setMondayFirst(!cal.mondayFirst);
568                         return;
569                     case 0:
570                         // TODAY will bring us here
571                         if ((typeof cal.checkDisabled == "function") && cal.checkDisabled(date)) {
572                                 // remember, "date" was previously set to new
573                                 // Date() if TODAY was clicked; thus, it
574                                 // contains today date.
575                                 return false;
576                         }
577                         break;
578                 }
579                 if (!date.equalsTo(cal.date)) {
580                         cal.setDate(date);
581                         newdate = true;
582                 }
583         }
584         if (newdate) {
585                 cal.callHandler();
586         }
587         if (closing) {
588                 Calendar.removeClass(el, "hilite");
589                 cal.callCloseHandler();
590         }
591 };
592
593 // END: CALENDAR STATIC FUNCTIONS
594
595 // BEGIN: CALENDAR OBJECT FUNCTIONS
596
597 /**
598  *  This function creates the calendar inside the given parent.  If _par is
599  *  null than it creates a popup calendar inside the BODY element.  If _par is
600  *  an element, be it BODY, then it creates a non-popup calendar (still
601  *  hidden).  Some properties need to be set before calling this function.
602  */
603 Calendar.prototype.create = function (_par) {
604         var parent = null;
605         if (! _par) {
606                 // default parent is the document body, in which case we create
607                 // a popup calendar.
608                 parent = document.getElementsByTagName("body")[0];
609                 this.isPopup = true;
610         } else {
611                 parent = _par;
612                 this.isPopup = false;
613         }
614         this.date = this.dateStr ? new Date(this.dateStr) : new Date();
615
616         var table = Calendar.createElement("table");
617         this.table = table;
618         table.cellSpacing = 0;
619         table.cellPadding = 0;
620         table.calendar = this;
621         Calendar.addEvent(table, "mousedown", Calendar.tableMouseDown);
622
623         var div = Calendar.createElement("div");
624         this.element = div;
625         div.className = "calendar";
626         if (this.isPopup) {
627                 div.style.position = "absolute";
628                 div.style.display = "none";
629         }
630         div.appendChild(table);
631
632         var thead = Calendar.createElement("thead", table);
633         var cell = null;
634         var row = null;
635
636         var cal = this;
637         var hh = function (text, cs, navtype) {
638                 cell = Calendar.createElement("td", row);
639                 cell.colSpan = cs;
640                 cell.className = "button";
641                 Calendar._add_evs(cell);
642                 cell.calendar = cal;
643                 cell.navtype = navtype;
644                 if (text.substr(0, 1) != "&") {
645                         cell.appendChild(document.createTextNode(text));
646                 }
647                 else {
648                         // FIXME: dirty hack for entities
649                         cell.innerHTML = text;
650                 }
651                 return cell;
652         };
653
654         row = Calendar.createElement("tr", thead);
655         var title_length = 6;
656         (this.isPopup) && --title_length;
657         (this.weekNumbers) && ++title_length;
658
659         hh("-", 1, 100).ttip = Calendar._TT["TOGGLE"];
660         this.title = hh("", title_length, 300);
661         this.title.className = "title";
662         if (this.isPopup) {
663                 this.title.ttip = Calendar._TT["DRAG_TO_MOVE"];
664                 this.title.style.cursor = "move";
665                 hh("&#x00d7;", 1, 200).ttip = Calendar._TT["CLOSE"];
666         }
667
668         row = Calendar.createElement("tr", thead);
669         row.className = "headrow";
670
671         this._nav_py = hh("&#x00ab;", 1, -2);
672         this._nav_py.ttip = Calendar._TT["PREV_YEAR"];
673
674         this._nav_pm = hh("&#x2039;", 1, -1);
675         this._nav_pm.ttip = Calendar._TT["PREV_MONTH"];
676
677         this._nav_now = hh(Calendar._TT["TODAY"], this.weekNumbers ? 4 : 3, 0);
678         this._nav_now.ttip = Calendar._TT["GO_TODAY"];
679
680         this._nav_nm = hh("&#x203a;", 1, 1);
681         this._nav_nm.ttip = Calendar._TT["NEXT_MONTH"];
682
683         this._nav_ny = hh("&#x00bb;", 1, 2);
684         this._nav_ny.ttip = Calendar._TT["NEXT_YEAR"];
685
686         // day names
687         row = Calendar.createElement("tr", thead);
688         row.className = "daynames";
689         if (this.weekNumbers) {
690                 cell = Calendar.createElement("td", row);
691                 cell.className = "name wn";
692                 cell.appendChild(document.createTextNode(Calendar._TT["WK"]));
693         }
694         for (var i = 7; i > 0; --i) {
695                 cell = Calendar.createElement("td", row);
696                 cell.appendChild(document.createTextNode(""));
697                 if (!i) {
698                         cell.navtype = 100;
699                         cell.calendar = this;
700                         Calendar._add_evs(cell);
701                 }
702         }
703         this.firstdayname = (this.weekNumbers) ? row.firstChild.nextSibling : row.firstChild;
704         this._displayWeekdays();
705
706         var tbody = Calendar.createElement("tbody", table);
707         this.tbody = tbody;
708
709         for (i = 6; i > 0; --i) {
710                 row = Calendar.createElement("tr", tbody);
711                 if (this.weekNumbers) {
712                         cell = Calendar.createElement("td", row);
713                         cell.appendChild(document.createTextNode(""));
714                 }
715                 for (var j = 7; j > 0; --j) {
716                         cell = Calendar.createElement("td", row);
717                         cell.appendChild(document.createTextNode(""));
718                         cell.calendar = this;
719                         Calendar._add_evs(cell);
720                 }
721         }
722
723         var tfoot = Calendar.createElement("tfoot", table);
724
725         row = Calendar.createElement("tr", tfoot);
726         row.className = "footrow";
727
728         cell = hh(Calendar._TT["SEL_DATE"], this.weekNumbers ? 8 : 7, 300);
729         cell.className = "ttip";
730         if (this.isPopup) {
731                 cell.ttip = Calendar._TT["DRAG_TO_MOVE"];
732                 cell.style.cursor = "move";
733         }
734         this.tooltips = cell;
735
736         div = Calendar.createElement("div", this.element);
737         this.monthsCombo = div;
738         div.className = "combo";
739         for (i = 0; i < Calendar._MN.length; ++i) {
740                 var mn = Calendar.createElement("div");
741                 mn.className = "label";
742                 mn.month = i;
743                 mn.appendChild(document.createTextNode(Calendar._MN3[i]));
744                 div.appendChild(mn);
745         }
746
747         div = Calendar.createElement("div", this.element);
748         this.yearsCombo = div;
749         div.className = "combo";
750         for (i = 12; i > 0; --i) {
751                 var yr = Calendar.createElement("div");
752                 yr.className = "label";
753                 yr.appendChild(document.createTextNode(""));
754                 div.appendChild(yr);
755         }
756
757         this._init(this.mondayFirst, this.date);
758         parent.appendChild(this.element);
759 };
760
761 /** keyboard navigation, only for popup calendars */
762 Calendar._keyEvent = function(ev) {
763         if (!window.calendar) {
764                 return false;
765         }
766         (Calendar.is_ie) && (ev = window.event);
767         var cal = window.calendar;
768         var act = (Calendar.is_ie || ev.type == "keypress");
769         if (ev.ctrlKey) {
770                 switch (ev.keyCode) {
771                     case 37: // KEY left
772                         act && Calendar.cellClick(cal._nav_pm);
773                         break;
774                     case 38: // KEY up
775                         act && Calendar.cellClick(cal._nav_py);
776                         break;
777                     case 39: // KEY right
778                         act && Calendar.cellClick(cal._nav_nm);
779                         break;
780                     case 40: // KEY down
781                         act && Calendar.cellClick(cal._nav_ny);
782                         break;
783                     default:
784                         return false;
785                 }
786         } else switch (ev.keyCode) {
787             case 32: // KEY space (now)
788                 Calendar.cellClick(cal._nav_now);
789                 break;
790             case 27: // KEY esc
791                 act && cal.hide();
792                 break;
793             case 37: // KEY left
794             case 38: // KEY up
795             case 39: // KEY right
796             case 40: // KEY down
797                 if (act) {
798                         var date = cal.date.getDate() - 1;
799                         var el = cal.currentDateEl;
800                         var ne = null;
801                         var prev = (ev.keyCode == 37) || (ev.keyCode == 38);
802                         switch (ev.keyCode) {
803                             case 37: // KEY left
804                                 (--date >= 0) && (ne = cal.ar_days[date]);
805                                 break;
806                             case 38: // KEY up
807                                 date -= 7;
808                                 (date >= 0) && (ne = cal.ar_days[date]);
809                                 break;
810                             case 39: // KEY right
811                                 (++date < cal.ar_days.length) && (ne = cal.ar_days[date]);
812                                 break;
813                             case 40: // KEY down
814                                 date += 7;
815                                 (date < cal.ar_days.length) && (ne = cal.ar_days[date]);
816                                 break;
817                         }
818                         if (!ne) {
819                                 if (prev) {
820                                         Calendar.cellClick(cal._nav_pm);
821                                 } else {
822                                         Calendar.cellClick(cal._nav_nm);
823                                 }
824                                 date = (prev) ? cal.date.getMonthDays() : 1;
825                                 el = cal.currentDateEl;
826                                 ne = cal.ar_days[date - 1];
827                         }
828                         Calendar.removeClass(el, "selected");
829                         Calendar.addClass(ne, "selected");
830                         cal.date.setDate(ne.caldate);
831                         cal.callHandler();
832                         cal.currentDateEl = ne;
833                 }
834                 break;
835             case 13: // KEY enter
836                 if (act) {
837                         cal.callHandler();
838                         cal.hide();
839                 }
840                 break;
841             default:
842                 return false;
843         }
844         return Calendar.stopEvent(ev);
845 };
846
847 /**
848  *  (RE)Initializes the calendar to the given date and style (if mondayFirst is
849  *  true it makes Monday the first day of week, otherwise the weeks start on
850  *  Sunday.
851  */
852 Calendar.prototype._init = function (mondayFirst, date) {
853         var today = new Date();
854         var year = date.getFullYear();
855         if (year < this.minYear) {
856                 year = this.minYear;
857                 date.setFullYear(year);
858         } else if (year > this.maxYear) {
859                 year = this.maxYear;
860                 date.setFullYear(year);
861         }
862         this.mondayFirst = mondayFirst;
863         this.date = new Date(date);
864         var month = date.getMonth();
865         var mday = date.getDate();
866         var no_days = date.getMonthDays();
867         date.setDate(1);
868         var wday = date.getDay();
869         var MON = mondayFirst ? 1 : 0;
870         var SAT = mondayFirst ? 5 : 6;
871         var SUN = mondayFirst ? 6 : 0;
872         if (mondayFirst) {
873                 wday = (wday > 0) ? (wday - 1) : 6;
874         }
875         var iday = 1;
876         var row = this.tbody.firstChild;
877         var MN = Calendar._MN3[month];
878         var hasToday = ((today.getFullYear() == year) && (today.getMonth() == month));
879         var todayDate = today.getDate();
880         var week_number = date.getWeekNumber();
881         var ar_days = new Array();
882         for (var i = 0; i < 6; ++i) {
883                 if (iday > no_days) {
884                         row.className = "emptyrow";
885                         row = row.nextSibling;
886                         continue;
887                 }
888                 var cell = row.firstChild;
889                 if (this.weekNumbers) {
890                         cell.className = "day wn";
891                         cell.firstChild.data = week_number;
892                         cell = cell.nextSibling;
893                 }
894                 ++week_number;
895                 row.className = "daysrow";
896                 for (var j = 0; j < 7; ++j) {
897                         cell.className = "day";
898                         if ((!i && j < wday) || iday > no_days) {
899                                 // cell.className = "emptycell";
900                                 cell.innerHTML = "&nbsp;";
901                                 cell.disabled = true;
902                                 cell = cell.nextSibling;
903                                 continue;
904                         }
905                         cell.disabled = false;
906                         cell.firstChild.data = iday;
907                         if (typeof this.checkDisabled == "function") {
908                                 date.setDate(iday);
909                                 if (this.checkDisabled(date)) {
910                                         cell.className += " disabled";
911                                         cell.disabled = true;
912                                 }
913                         }
914                         if (!cell.disabled) {
915                                 ar_days[ar_days.length] = cell;
916                                 cell.caldate = iday;
917                                 cell.ttip = "_";
918                                 if (iday == mday) {
919                                         cell.className += " selected";
920                                         this.currentDateEl = cell;
921                                 }
922                                 if (hasToday && (iday == todayDate)) {
923                                         cell.className += " today";
924                                         cell.ttip += Calendar._TT["PART_TODAY"];
925                                 }
926                                 if (wday == SAT || wday == SUN) {
927                                         cell.className += " weekend";
928                                 }
929                         }
930                         ++iday;
931                         ((++wday) ^ 7) || (wday = 0);
932                         cell = cell.nextSibling;
933                 }
934                 row = row.nextSibling;
935         }
936         this.ar_days = ar_days;
937         this.title.firstChild.data = Calendar._MN[month] + ", " + year;
938         // PROFILE
939         // this.tooltips.firstChild.data = "Generated in " + ((new Date()) - today) + " ms";
940 };
941
942 /**
943  *  Calls _init function above for going to a certain date (but only if the
944  *  date is different than the currently selected one).
945  */
946 Calendar.prototype.setDate = function (date) {
947         if (!date.equalsTo(this.date)) {
948                 this._init(this.mondayFirst, date);
949         }
950 };
951
952 /**
953  *  Refreshes the calendar.  Useful if the "disabledHandler" function is
954  *  dynamic, meaning that the list of disabled date can change at runtime.
955  *  Just * call this function if you think that the list of disabled dates
956  *  should * change.
957  */
958 Calendar.prototype.refresh = function () {
959         this._init(this.mondayFirst, this.date);
960 };
961
962 /** Modifies the "mondayFirst" parameter (EU/US style). */
963 Calendar.prototype.setMondayFirst = function (mondayFirst) {
964         this._init(mondayFirst, this.date);
965         this._displayWeekdays();
966 };
967
968 /**
969  *  Allows customization of what dates are enabled.  The "unaryFunction"
970  *  parameter must be a function object that receives the date (as a JS Date
971  *  object) and returns a boolean value.  If the returned value is true then
972  *  the passed date will be marked as disabled.
973  */
974 Calendar.prototype.setDisabledHandler = function (unaryFunction) {
975         this.checkDisabled = unaryFunction;
976 };
977
978 /** Customization of allowed year range for the calendar. */
979 Calendar.prototype.setRange = function (a, z) {
980         this.minYear = a;
981         this.maxYear = z;
982 };
983
984 /** Calls the first user handler (selectedHandler). */
985 Calendar.prototype.callHandler = function () {
986         if (this.onSelected) {
987                 this.onSelected(this, this.date.print(this.dateFormat));
988         }
989 };
990
991 /** Calls the second user handler (closeHandler). */
992 Calendar.prototype.callCloseHandler = function () {
993         if (this.onClose) {
994                 this.onClose(this);
995         }
996         this.hideShowCovered();
997 };
998
999 /** Removes the calendar object from the DOM tree and destroys it. */
1000 Calendar.prototype.destroy = function () {
1001         var el = this.element.parentNode;
1002         el.removeChild(this.element);
1003         Calendar._C = null;
1004 };
1005
1006 /**
1007  *  Moves the calendar element to a different section in the DOM tree (changes
1008  *  its parent).
1009  */
1010 Calendar.prototype.reparent = function (new_parent) {
1011         var el = this.element;
1012         el.parentNode.removeChild(el);
1013         new_parent.appendChild(el);
1014 };
1015
1016 // This gets called when the user presses a mouse button anywhere in the
1017 // document, if the calendar is shown.  If the click was outside the open
1018 // calendar this function closes it.
1019 Calendar._checkCalendar = function(ev) {
1020         if (!window.calendar) {
1021                 return false;
1022         }
1023         var el = Calendar.is_ie ? Calendar.getElement(ev) : Calendar.getTargetElement(ev);
1024         for (; el != null && el != calendar.element; el = el.parentNode);
1025         if (el == null) {
1026                 // calls closeHandler which should hide the calendar.
1027                 window.calendar.callCloseHandler();
1028                 return Calendar.stopEvent(ev);
1029         }
1030 };
1031
1032 /** Shows the calendar. */
1033 Calendar.prototype.show = function () {
1034         var rows = this.table.getElementsByTagName("tr");
1035         for (var i = rows.length; i > 0;) {
1036                 var row = rows[--i];
1037                 Calendar.removeClass(row, "rowhilite");
1038                 var cells = row.getElementsByTagName("td");
1039                 for (var j = cells.length; j > 0;) {
1040                         var cell = cells[--j];
1041                         Calendar.removeClass(cell, "hilite");
1042                         Calendar.removeClass(cell, "active");
1043                 }
1044         }
1045         this.element.style.display = "block";
1046         this.hidden = false;
1047         if (this.isPopup) {
1048                 window.calendar = this;
1049                 Calendar.addEvent(document, "keydown", Calendar._keyEvent);
1050                 Calendar.addEvent(document, "keypress", Calendar._keyEvent);
1051                 Calendar.addEvent(document, "mousedown", Calendar._checkCalendar);
1052         }
1053         this.hideShowCovered();
1054 };
1055
1056 /**
1057  *  Hides the calendar.  Also removes any "hilite" from the class of any TD
1058  *  element.
1059  */
1060 Calendar.prototype.hide = function () {
1061         if (this.isPopup) {
1062                 Calendar.removeEvent(document, "keydown", Calendar._keyEvent);
1063                 Calendar.removeEvent(document, "keypress", Calendar._keyEvent);
1064                 Calendar.removeEvent(document, "mousedown", Calendar._checkCalendar);
1065         }
1066         this.element.style.display = "none";
1067         this.hidden = true;
1068         this.hideShowCovered();
1069 };
1070
1071 /**
1072  *  Shows the calendar at a given absolute position (beware that, depending on
1073  *  the calendar element style -- position property -- this might be relative
1074  *  to the parent's containing rectangle).
1075  */
1076 Calendar.prototype.showAt = function (x, y) {
1077         var s = this.element.style;
1078         s.left = x + "px";
1079         s.top = y + "px";
1080         this.show();
1081 };
1082
1083 /** Shows the calendar near a given element. */
1084 Calendar.prototype.showAtElement = function (el, opts) {
1085         var p = Calendar.getAbsolutePos(el);
1086         if (!opts || typeof opts != "string") {
1087                 this.showAt(p.x, p.y + el.offsetHeight);
1088                 return true;
1089         }
1090         this.show();
1091         var w = this.element.offsetWidth;
1092         var h = this.element.offsetHeight;
1093         this.hide();
1094         var valign = opts.substr(0, 1);
1095         var halign = "l";
1096         if (opts.length > 1) {
1097                 halign = opts.substr(1, 1);
1098         }
1099         // vertical alignment
1100         switch (valign) {
1101             case "T": p.y -= h; break;
1102             case "B": p.y += el.offsetHeight; break;
1103             case "C": p.y += (el.offsetHeight - h) / 2; break;
1104             case "t": p.y += el.offsetHeight - h; break;
1105             case "b": break; // already there
1106         }
1107         // horizontal alignment
1108         switch (halign) {
1109             case "L": p.x -= w; break;
1110             case "R": p.x += el.offsetWidth; break;
1111             case "C": p.x += (el.offsetWidth - w) / 2; break;
1112             case "r": p.x += el.offsetWidth - w; break;
1113             case "l": break; // already there
1114         }
1115         this.showAt(p.x, p.y);
1116 };
1117
1118 /** Customizes the date format. */
1119 Calendar.prototype.setDateFormat = function (str) {
1120         this.dateFormat = str;
1121 };
1122
1123 /** Customizes the tooltip date format. */
1124 Calendar.prototype.setTtDateFormat = function (str) {
1125         this.ttDateFormat = str;
1126 };
1127
1128 /**
1129  *  Tries to identify the date represented in a string.  If successful it also
1130  *  calls this.setDate which moves the calendar to the given date.
1131  */
1132 Calendar.prototype.parseDate = function (str, fmt) {
1133         var y = 0;
1134         var m = -1;
1135         var d = 0;
1136         var a = str.split(/\W+/);
1137         if (!fmt) {
1138                 fmt = this.dateFormat;
1139         }
1140         var b = fmt.split(/\W+/);
1141         var i = 0, j = 0;
1142         for (i = 0; i < a.length; ++i) {
1143                 if (b[i] == "D" || b[i] == "DD") {
1144                         continue;
1145                 }
1146                 if (b[i] == "d" || b[i] == "dd") {
1147                         d = parseInt(a[i], 10);
1148                 }
1149                 if (b[i] == "m" || b[i] == "mm") {
1150                         m = parseInt(a[i], 10) - 1;
1151                 }
1152                 if ((b[i] == "y") || (b[i] == "yy")) {
1153                         y = parseInt(a[i], 10);
1154                         (y < 100) && (y += (y > 29) ? 1900 : 2000);
1155                 }
1156                 if (b[i] == "M" || b[i] == "MM") {
1157                         for (j = 0; j < 12; ++j) {
1158                                 if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { m = j; break; }
1159                         }
1160                 }
1161         }
1162         if (y != 0 && m != -1 && d != 0) {
1163                 this.setDate(new Date(y, m, d));
1164                 return;
1165         }
1166         y = 0; m = -1; d = 0;
1167         for (i = 0; i < a.length; ++i) {
1168                 if (a[i].search(/[a-zA-Z]+/) != -1) {
1169                         var t = -1;
1170                         for (j = 0; j < 12; ++j) {
1171                                 if (Calendar._MN[j].substr(0, a[i].length).toLowerCase() == a[i].toLowerCase()) { t = j; break; }
1172                         }
1173                         if (t != -1) {
1174                                 if (m != -1) {
1175                                         d = m+1;
1176                                 }
1177                                 m = t;
1178                         }
1179                 } else if (parseInt(a[i], 10) <= 12 && m == -1) {
1180                         m = a[i]-1;
1181                 } else if (parseInt(a[i], 10) > 31 && y == 0) {
1182                         y = parseInt(a[i], 10);
1183                         (y < 100) && (y += (y > 29) ? 1900 : 2000);
1184                 } else if (d == 0) {
1185                         d = a[i];
1186                 }
1187         }
1188         if (y == 0) {
1189                 var today = new Date();
1190                 y = today.getFullYear();
1191         }
1192         if (m != -1 && d != 0) {
1193                 this.setDate(new Date(y, m, d));
1194         }
1195 };
1196
1197 Calendar.prototype.hideShowCovered = function () {
1198         function getStyleProp(obj, style){
1199                 var value = obj.style[style];
1200                 if (!value) {
1201                         if (document.defaultView && typeof (document.defaultView.getComputedStyle) == "function") { // Gecko, W3C
1202                                 value = document.defaultView.
1203                                         getComputedStyle(obj, "").getPropertyValue(style);
1204                         } else if (obj.currentStyle) { // IE
1205                                 value = obj.currentStyle[style];
1206                         } else {
1207                                 value = obj.style[style];
1208                         }
1209                 }
1210                 return value;
1211         };
1212
1213         var tags = new Array("applet", "iframe", "select");
1214         var el = this.element;
1215
1216         var p = Calendar.getAbsolutePos(el);
1217         var EX1 = p.x;
1218         var EX2 = el.offsetWidth + EX1;
1219         var EY1 = p.y;
1220         var EY2 = el.offsetHeight + EY1;
1221
1222         for (var k = tags.length; k > 0; ) {
1223                 var ar = document.getElementsByTagName(tags[--k]);
1224                 var cc = null;
1225
1226                 for (var i = ar.length; i > 0;) {
1227                         cc = ar[--i];
1228
1229                         p = Calendar.getAbsolutePos(cc);
1230                         var CX1 = p.x;
1231                         var CX2 = cc.offsetWidth + CX1;
1232                         var CY1 = p.y;
1233                         var CY2 = cc.offsetHeight + CY1;
1234
1235                         if (this.hidden || (CX1 > EX2) || (CX2 < EX1) || (CY1 > EY2) || (CY2 < EY1)) {
1236                                 if (!cc.__msh_save_visibility) {
1237                                         cc.__msh_save_visibility = getStyleProp(cc, "visibility");
1238                                 }
1239                                 cc.style.visibility = cc.__msh_save_visibility;
1240                         } else {
1241                                 if (!cc.__msh_save_visibility) {
1242                                         cc.__msh_save_visibility = getStyleProp(cc, "visibility");
1243                                 }
1244                                 cc.style.visibility = "hidden";
1245                         }
1246                 }
1247         }
1248 };
1249
1250 /** Internal function; it displays the bar with the names of the weekday. */
1251 Calendar.prototype._displayWeekdays = function () {
1252         var MON = this.mondayFirst ? 0 : 1;
1253         var SUN = this.mondayFirst ? 6 : 0;
1254         var SAT = this.mondayFirst ? 5 : 6;
1255         var cell = this.firstdayname;
1256         for (var i = 0; i < 7; ++i) {
1257                 cell.className = "day name";
1258                 if (!i) {
1259                         cell.ttip = this.mondayFirst ? Calendar._TT["SUN_FIRST"] : Calendar._TT["MON_FIRST"];
1260                         cell.navtype = 100;
1261                         cell.calendar = this;
1262                         Calendar._add_evs(cell);
1263                 }
1264                 if (i == SUN || i == SAT) {
1265                         Calendar.addClass(cell, "weekend");
1266                 }
1267                 cell.firstChild.data = Calendar._DN3[i + 1 - MON];
1268                 cell = cell.nextSibling;
1269         }
1270 };
1271
1272 /** Internal function.  Hides all combo boxes that might be displayed. */
1273 Calendar.prototype._hideCombos = function () {
1274         this.monthsCombo.style.display = "none";
1275         this.yearsCombo.style.display = "none";
1276 };
1277
1278 /** Internal function.  Starts dragging the element. */
1279 Calendar.prototype._dragStart = function (ev) {
1280         if (this.dragging) {
1281                 return;
1282         }
1283         this.dragging = true;
1284         var posX;
1285         var posY;
1286         if (Calendar.is_ie) {
1287                 posY = window.event.clientY + document.body.scrollTop;
1288                 posX = window.event.clientX + document.body.scrollLeft;
1289         } else {
1290                 posY = ev.clientY + window.scrollY;
1291                 posX = ev.clientX + window.scrollX;
1292         }
1293         var st = this.element.style;
1294         this.xOffs = posX - parseInt(st.left);
1295         this.yOffs = posY - parseInt(st.top);
1296         with (Calendar) {
1297                 addEvent(document, "mousemove", calDragIt);
1298                 addEvent(document, "mouseover", stopEvent);
1299                 addEvent(document, "mouseup", calDragEnd);
1300         }
1301 };
1302
1303 // BEGIN: DATE OBJECT PATCHES
1304
1305 /** Adds the number of days array to the Date object. */
1306 Date._MD = new Array(31,28,31,30,31,30,31,31,30,31,30,31);
1307
1308 /** Constants used for time computations */
1309 Date.SECOND = 1000 /* milliseconds */;
1310 Date.MINUTE = 60 * Date.SECOND;
1311 Date.HOUR   = 60 * Date.MINUTE;
1312 Date.DAY    = 24 * Date.HOUR;
1313 Date.WEEK   =  7 * Date.DAY;
1314
1315 /** Returns the number of days in the current month */
1316 Date.prototype.getMonthDays = function(month) {
1317         var year = this.getFullYear();
1318         if (typeof month == "undefined") {
1319                 month = this.getMonth();
1320         }
1321         if (((0 == (year%4)) && ( (0 != (year%100)) || (0 == (year%400)))) && month == 1) {
1322                 return 29;
1323         } else {
1324                 return Date._MD[month];
1325         }
1326 };
1327
1328 /** Returns the number of the week.  The algorithm was "stolen" from PPK's
1329  * website, hope it's correct :) http://www.xs4all.nl/~ppk/js/week.html */
1330 Date.prototype.getWeekNumber = function() {
1331         var now = new Date(this.getFullYear(), this.getMonth(), this.getDate(), 0, 0, 0);
1332         var then = new Date(this.getFullYear(), 0, 1, 0, 0, 0);
1333         var time = now - then;
1334         var day = then.getDay();
1335         (day > 3) && (day -= 4) || (day += 3);
1336         return Math.round(((time / Date.DAY) + day) / 7);
1337 };
1338
1339 /** Checks dates equality (ignores time) */
1340 Date.prototype.equalsTo = function(date) {
1341         return ((this.getFullYear() == date.getFullYear()) &&
1342                 (this.getMonth() == date.getMonth()) &&
1343                 (this.getDate() == date.getDate()));
1344 };
1345
1346 /** Prints the date in a string according to the given format. */
1347 Date.prototype.print = function (frm) {
1348         var str = new String(frm);
1349         var m = this.getMonth();
1350         var d = this.getDate();
1351         var y = this.getFullYear();
1352         var wn = this.getWeekNumber();
1353         var w = this.getDay();
1354         var s = new Array();
1355         s["d"] = d;
1356         s["dd"] = (d < 10) ? ("0" + d) : d;
1357         s["m"] = 1+m;
1358         s["mm"] = (m < 9) ? ("0" + (1+m)) : (1+m);
1359         s["y"] = y;
1360         s["yy"] = new String(y).substr(2, 2);
1361         s["w"] = wn;
1362         s["ww"] = (wn < 10) ? ("0" + wn) : wn;
1363         with (Calendar) {
1364                 s["D"] = _DN3[w];
1365                 s["DD"] = _DN[w];
1366                 s["M"] = _MN3[m];
1367                 s["MM"] = _MN[m];
1368         }
1369         var re = /(.*)(\W|^)(d|dd|m|mm|y|yy|MM|M|DD|D|w|ww)(\W|$)(.*)/;
1370         while (re.exec(str) != null) {
1371                 str = RegExp.$1 + RegExp.$2 + s[RegExp.$3] + RegExp.$4 + RegExp.$5;
1372         }
1373         return str;
1374 };
1375
1376 // END: DATE OBJECT PATCHES
1377
1378 // global object that remembers the calendar
1379 window.calendar = null;