Always disable Rico debugging
[misc/kostenrechnung] / lib / rico / ricoCalendar.js
1 //  By Matt Brown
2 //  June-October 2006
3 //  email: dowdybrown@yahoo.com
4 //  Inspired by code originally written by Tan Ling Wee on 2 Dec 2001
5 //  Requires prototype.js and ricoCommon.js
6
7 Rico.CalendarControl = Class.create(
8 /** @lends Rico.CalendarControl# */
9 {
10 /**
11  * @class Implements a pop-up Gregorian calendar.
12  * Dates of adoption of the Gregorian calendar vary by country - accurate as a US & British calendar from 14 Sept 1752 to present.
13  * Mark special dates with calls to addHoliday()
14  * @extends Rico.Popup
15  * @constructs
16  * @param id unique identifier
17  * @param options object may contain any of the following:<dl>
18  *   <dt>startAt       </dt><dd> week starts with 0=sunday, 1=monday? default=0</dd>
19  *   <dt>showWeekNumber</dt><dd> show week number in first column? default=0</dd>
20  *   <dt>showToday     </dt><dd> show "Today is..." in footer? default=1</dd>
21  *   <dt>cursorColor   </dt><dd> color used to highlight dates as the user moves their mouse, default=#FDD</dd>
22  *   <dt>repeatInterval</dt><dd> when left/right arrow is pressed, repeat action every x milliseconds, default=100</dd>
23  *   <dt>dateFmt       </dt><dd> date format for return value (one of values accepted by {@link Date#formatDate}), default=ISO8601</dd>
24  *   <dt>selectedDateBorder</dt><dd> border to indicate currently selected date? default=#666666</dd>
25  *   <dt>minDate       </dt><dd> earliest selectable date? default=today-50 years</dd>
26  *   <dt>maxDate       </dt><dd> last selectable date? default=today+50 years</dd>
27  *</dl>
28  */
29   initialize: function(id,options) {
30     this.id=id;
31     var today=new Date();
32     Object.extend(this, new Rico.Popup({ignoreClicks:true}));
33     Object.extend(this.options, {
34       startAt : 0,
35       showWeekNumber : 0,
36       showToday : 1,
37       cursorColor: '#FDD',
38       repeatInterval : 100,
39       dateFmt : 'ISO8601',
40       selectedDateBorder : "#666666",
41       minDate : new Date(today.getFullYear()-50,0,1),
42       maxDate : new Date(today.getFullYear()+50,11,31)
43     });
44     Object.extend(this.options, options || {});
45     /**
46      * alias for closePopup
47      * @function
48      */
49     this.close=this.closePopup;
50     this.bPageLoaded=false;
51     this.img=[];
52     this.Holidays={};
53     this.weekString=RicoTranslate.getPhraseById("calWeekHdg");
54     this.re=/^\s*(\w+)(\W)(\w+)(\W)(\w+)/i;
55     this.setDateFmt(this.options.dateFmt);
56   },
57
58
59   setDateFmt: function(fmt) {
60     this.dateFmt=(fmt=='rico') ? RicoTranslate.dateFmt : fmt;
61     this.dateParts=[];
62     if (this.re.exec(this.dateFmt)) {
63       this.dateParts[RegExp.$1]=0;
64       this.dateParts[RegExp.$3]=1;
65       this.dateParts[RegExp.$5]=2;
66     }
67   },
68   
69 /**
70  * Call before displaying calendar to highlight special days
71  * @param d day (1-31)
72  * @param m month (1-12)
73  * @param y year (0 implies a repeating holiday)
74  * @param desc description
75  * @param bgColor background color for cell displaying this day (CSS value, defaults to '#DDF')
76  * @param txtColor text color for cell displaying this day (CSS value), if not specified it is displayed with the same color as other days
77  */
78   addHoliday : function(d, m, y, desc, bgColor, txtColor) {
79     this.Holidays[this.holidayKey(y,m-1,d)]={desc:desc, txtColor:txtColor, bgColor:bgColor || '#DDF'};
80   },
81   
82 /** @private */
83   holidayKey : function(y,m,d) {
84     return 'h'+y.toPaddedString(4)+m.toPaddedString(2)+d.toPaddedString(2);
85   },
86
87   atLoad : function() {
88     this.container=document.createElement("div");
89     this.container.style.display="none";
90     this.container.id=this.id;
91     this.container.className='ricoCalContainer';
92
93     this.maintab=document.createElement("table");
94     this.maintab.cellSpacing=0;
95     this.maintab.cellPadding=0;
96     this.maintab.border=0;
97     this.maintab.className='ricoCalTab';
98
99     var r,c,i,j,img,dow,a,s;
100     for (i=0; i<7; i++) {
101       r=this.maintab.insertRow(-1);
102       r.className='row'+i;
103       for (c=0; c<8; c++) {
104         r.insertCell(-1);
105       }
106     }
107     this.tbody=this.maintab.tBodies[0];
108     r=this.tbody.rows[0];
109     r.className='ricoCalDayNames';
110     if (this.options.showWeekNumber) {
111       r.cells[0].innerHTML=this.weekString;
112       for (i=0; i<7; i++) {
113         this.tbody.rows[i].cells[0].className='ricoCalWeekNum';
114       }
115     }
116     this.styles=[];
117     for (i=0; i<7; i++) {
118       dow=(i+this.options.startAt) % 7;
119       r.cells[i+1].innerHTML=RicoTranslate.dayAbbr(dow);
120       this.styles[i+1]='ricoCal'+dow;
121     }
122     
123     // table header (navigation controls)
124     this.thead=this.maintab.createTHead();
125     r=this.thead.insertRow(-1);
126     c=r.insertCell(-1);
127     c.colSpan=8;
128     img=this.createNavArrow('decMonth','left');
129     c.appendChild(document.createElement("a")).appendChild(img);
130     this.titleMonth=document.createElement("a");
131     c.appendChild(this.titleMonth);
132     Event.observe(this.titleMonth,"click", this.popUpMonth.bindAsEventListener(this), false);
133     img=this.createNavArrow('incMonth','right');
134     c.appendChild(document.createElement("a")).appendChild(img);
135     s=document.createElement("span");
136     s.innerHTML='&nbsp;';
137     s.style.paddingLeft='3em';
138     c.appendChild(s);
139
140     img=this.createNavArrow('decYear','left');
141     c.appendChild(document.createElement("a")).appendChild(img);
142     this.titleYear=document.createElement("a");
143     Event.observe(this.titleYear,"click", this.popUpYear.bindAsEventListener(this), false);
144     c.appendChild(this.titleYear);
145     img=this.createNavArrow('incYear','right');
146     c.appendChild(document.createElement("a")).appendChild(img);
147
148     // table footer (today)
149     if (this.options.showToday) {
150       this.tfoot=this.maintab.createTFoot();
151       r=this.tfoot.insertRow(-1);
152       this.todayCell=r.insertCell(-1);
153       this.todayCell.colSpan=8;
154       Event.observe(this.todayCell,"click", this.selectNow.bindAsEventListener(this), false);
155     }
156     
157
158     this.container.appendChild(this.maintab);
159     
160     // close icon (upper right)
161     img=document.createElement("img");
162     img.src=Rico.imgDir+'close.gif';
163     img.onclick=this.close.bind(this);
164     img.style.cursor='pointer';
165     img.style.position='absolute';
166     img.style.top='1px';   /* assumes a 1px border */
167     img.style.right='1px';
168     img.title=RicoTranslate.getPhraseById('close');
169     this.container.appendChild(img);
170     
171     // month selector
172     this.monthSelect=document.createElement("table");
173     this.monthSelect.className='ricoCalMenu';
174     this.monthSelect.cellPadding=2;
175     this.monthSelect.cellSpacing=0;
176     this.monthSelect.border=0;
177     for (i=0; i<4; i++) {
178       r=this.monthSelect.insertRow(-1);
179       for (j=0; j<3; j++) {
180         c=r.insertCell(-1);
181         a=document.createElement("a");
182         a.innerHTML=RicoTranslate.monthAbbr(i*3+j);
183         a.name=i*3+j;
184         c.appendChild(a);
185         Event.observe(a,"click", this.selectMonth.bindAsEventListener(this), false);
186       }
187     }
188     this.monthSelect.style.display='none';
189     this.container.appendChild(this.monthSelect);
190     
191     // year selector
192     this.yearPopup=document.createElement("div");
193     this.yearPopup.style.display="block";
194     this.yearPopup.className='ricoCalYearPrompt';
195     this.container.appendChild(this.yearPopup);
196     this.yearPopupSpan=this.yearPopup.appendChild(document.createElement("span"));
197     this.yearPopupYear=this.yearPopup.appendChild(document.createElement("input"));
198     this.yearPopupYear.maxlength=4;
199     this.yearPopupYear.size=4;
200     Event.observe(this.yearPopupYear,"keypress", this.yearKey.bindAsEventListener(this), false);
201
202     img=document.createElement("img");
203     img.src=Rico.imgDir+'checkmark.gif';
204     Event.observe(img,"click", this.processPopUpYear.bindAsEventListener(this), false);
205     this.yearPopup.appendChild(img);
206
207     img=document.createElement("img");
208     img.src=Rico.imgDir+'delete.gif';
209     Event.observe(img,"click", this.popDownYear.bindAsEventListener(this), false);
210     this.yearPopup.appendChild(img);
211     
212     // fix anchors so they work in IE6
213     a=this.container.getElementsByTagName('a');
214     for (i=0; i<a.length; i++) {
215       a[i].href='javascript:void(0)';
216     }
217     
218     Event.observe(this.tbody,"click", this.saveAndClose.bindAsEventListener(this));
219     Event.observe(this.tbody,"mouseover", this.mouseOver.bindAsEventListener(this));
220     Event.observe(this.tbody,"mouseout",  this.mouseOut.bindAsEventListener(this));
221     document.getElementsByTagName("body")[0].appendChild(this.container);
222     this.setDiv(this.container);
223     this.close();
224     this.bPageLoaded=true;
225   },
226   
227   selectNow : function() {
228     this.monthSelected=this.monthNow;
229     this.yearSelected=this.yearNow;
230     this.constructCalendar();
231   },
232   
233 /** @private */
234   createNavArrow: function(funcname,gifname) {
235     var img=document.createElement("img");
236     img.src=Rico.imgDir+gifname+'.gif';
237     img.name=funcname;
238     Event.observe(img,"click", this[funcname].bindAsEventListener(this), false);
239     Event.observe(img,"mousedown", this.mouseDown.bindAsEventListener(this), false);
240     Event.observe(img,"mouseup", this.mouseUp.bindAsEventListener(this), false);
241     Event.observe(img,"mouseout", this.mouseUp.bindAsEventListener(this), false);
242     return img;
243   },
244
245 /** @private */
246   mouseOver: function(e) {
247     var el=Event.element(e);
248     if (this.lastHighlight==el) return;
249     this.unhighlight();
250     var s=el.innerHTML.replace(/&nbsp;/g,'');
251     if (s=='' || el.className=='ricoCalWeekNum') return;
252     var day=parseInt(s,10);
253     if (isNaN(day)) return;
254     this.lastHighlight=el;
255     this.tmpColor=el.style.backgroundColor;
256     el.style.backgroundColor=this.options.cursorColor;
257   },
258   
259 /** @private */
260   unhighlight: function() {
261     if (!this.lastHighlight) return;
262     this.lastHighlight.style.backgroundColor=this.tmpColor;
263     this.lastHighlight=null;
264   },
265   
266 /** @private */
267   mouseOut: function(e) {
268     var el=Event.element(e);
269     if (el==this.lastHighlight) this.unhighlight();
270   },
271   
272 /** @private */
273   mouseDown: function(e) {
274     var el=Event.element(e);
275     this.repeatFunc=this[el.name].bind(this);
276     this.timeoutID=setTimeout(this.repeatStart.bind(this),500);
277   },
278   
279 /** @private */
280   mouseUp: function(e) {
281     clearTimeout(this.timeoutID);
282     clearInterval(this.intervalID);
283   },
284   
285 /** @private */
286   repeatStart : function() {
287     clearInterval(this.intervalID);
288     this.intervalID=setInterval(this.repeatFunc,this.options.repeatInterval);
289   },
290   
291 /**
292  * @returns true if yr/mo is within minDate/MaxDate
293  */
294   isValidMonth : function(yr,mo) {
295     if (yr < this.options.minDate.getFullYear()) return false;
296     if (yr == this.options.minDate.getFullYear() && mo < this.options.minDate.getMonth()) return false;
297     if (yr > this.options.maxDate.getFullYear()) return false;
298     if (yr == this.options.maxDate.getFullYear() && mo > this.options.maxDate.getMonth()) return false;
299     return true;
300   },
301
302   incMonth : function() {
303     var newMonth=this.monthSelected+1;
304     var newYear=this.yearSelected;
305     if (newMonth>11) {
306       newMonth=0;
307       newYear++;
308     }
309     if (!this.isValidMonth(newYear,newMonth)) return;
310     this.monthSelected=newMonth;
311     this.yearSelected=newYear;
312     this.constructCalendar();
313   },
314
315   decMonth : function() {
316     var newMonth=this.monthSelected-1;
317     var newYear=this.yearSelected;
318     if (newMonth<0) {
319       newMonth=11;
320       newYear--;
321     }
322     if (!this.isValidMonth(newYear,newMonth)) return;
323     this.monthSelected=newMonth;
324     this.yearSelected=newYear;
325     this.constructCalendar();
326   },
327   
328 /** @private */
329   selectMonth : function(e) {
330     var el=Event.element(e);
331     this.monthSelected=parseInt(el.name,10);
332     this.constructCalendar();
333     Event.stop(e);
334   },
335
336   popUpMonth : function() {
337     Element.toggle(this.monthSelect);
338     this.monthSelect.style.top=(this.thead.offsetHeight+2)+'px';
339     this.monthSelect.style.left=this.titleMonth.offsetLeft+'px';
340   },
341
342   popDownMonth : function() {
343     Element.hide(this.monthSelect);
344   },
345
346   popDownYear : function() {
347     Element.hide(this.yearPopup);
348     this.yearPopup.disabled=true;  // make sure this does not get submitted
349   },
350
351 /**
352  * Prompt for year
353  */
354   popUpYear : function() {
355     Element.toggle(this.yearPopup);
356     if (!Element.visible(this.yearPopup)) return;
357     this.yearPopup.disabled=false;
358     this.yearPopup.style.left='120px';
359     this.yearPopup.style.top=(this.thead.offsetHeight+2)+'px';
360     this.yearPopupSpan.innerHTML='&nbsp;'+RicoTranslate.getPhraseById("calYearRange",this.options.minDate.getFullYear(),this.options.maxDate.getFullYear())+'<br>';
361     this.yearPopupYear.value='';   // this.yearSelected
362     this.yearPopupYear.focus();
363   },
364   
365   yearKey : function(e) {
366     switch (RicoUtil.eventKey(e)) {
367       case 27: this.popDownYear(); Event.stop(e); return false;
368       case 13: this.processPopUpYear(); Event.stop(e); return false;
369     }
370     return true;
371   },
372   
373   processPopUpYear : function() {
374     var newYear=this.yearPopupYear.value;
375     newYear=parseInt(newYear,10);
376     if (isNaN(newYear) || newYear<this.options.minDate.getFullYear() || newYear>this.options.maxDate.getFullYear()) {
377       alert(RicoTranslate.getPhraseById("calInvalidYear"));
378     } else {
379       this.yearSelected=newYear;
380       this.popDownYear();
381       this.constructCalendar();
382     }
383   },
384   
385   incYear : function() {
386     if (this.yearSelected>=this.options.maxDate.getFullYear()) return;
387     this.yearSelected++;
388     this.constructCalendar();
389   },
390
391   decYear : function() {
392     if (this.yearSelected<=this.options.minDate.getFullYear()) return;
393     this.yearSelected--;
394     this.constructCalendar();
395   },
396
397   // tried a number of different week number functions posted on the net
398   // this is the only one that produced consistent results when comparing week numbers for December and the following January
399   WeekNbr : function(year,month,day) {
400     var when = new Date(year,month,day);
401     var newYear = new Date(year,0,1);
402     var offset = 7 + 1 - newYear.getDay();
403     if (offset == 8) offset = 1;
404     var daynum = ((Date.UTC(year,when.getMonth(),when.getDate(),0,0,0) - Date.UTC(year,0,1,0,0,0)) /1000/60/60/24) + 1;
405     var weeknum = Math.floor((daynum-offset+7)/7);
406     if (weeknum == 0) {
407       year--;
408       var prevNewYear = new Date(year,0,1);
409       var prevOffset = 7 + 1 - prevNewYear.getDay();
410       weeknum = (prevOffset == 2 || prevOffset == 8) ? 53 : 52;
411     }
412     return weeknum;
413   },
414
415   constructCalendar : function() {
416     var aNumDays = [31,0,31,30,31,30,31,31,30,31,30,31];
417     var startDate = new Date (this.yearSelected,this.monthSelected,1);
418     var endDate,numDaysInMonth,i,colnum;
419
420     if (typeof this.monthSelected!='number' || this.monthSelected>=12 || this.monthSelected<0) {
421       alert('ERROR in calendar: monthSelected='+this.monthSelected);
422       return;
423     }
424
425     if (this.monthSelected==1) {
426       endDate = new Date (this.yearSelected,this.monthSelected+1,1);
427       endDate = new Date (endDate - (24*60*60*1000));
428       numDaysInMonth = endDate.getDate();
429     } else {
430       numDaysInMonth = aNumDays[this.monthSelected];
431     }
432     var dayPointer = startDate.getDay() - this.options.startAt;
433     if (dayPointer<0) dayPointer+=7;
434     this.popDownMonth();
435     this.popDownYear();
436
437     this.bgcolor=Element.getStyle(this.tbody,'background-color');
438     this.bgcolor=this.bgcolor.replace(/\"/g,'');
439     if (this.options.showWeekNumber) {
440       for (i=1; i<7; i++) {
441         this.tbody.rows[i].cells[0].innerHTML='&nbsp;';
442       }
443     }
444     for ( i=1; i<=dayPointer; i++ ) {
445       this.resetCell(this.tbody.rows[1].cells[i]);
446     }
447
448     for ( var datePointer=1,r=1; datePointer<=numDaysInMonth; datePointer++,dayPointer++ ) {
449       colnum=dayPointer % 7 + 1;
450       if (this.options.showWeekNumber==1 && colnum==1) {
451         this.tbody.rows[r].cells[0].innerHTML=this.WeekNbr(this.yearSelected,this.monthSelected,datePointer);
452       }
453       var dateClass=this.styles[colnum];
454       if ((datePointer==this.dateNow)&&(this.monthSelected==this.monthNow)&&(this.yearSelected==this.yearNow)) {
455         dateClass='ricoCalToday';
456       }
457       var c=this.tbody.rows[r].cells[colnum];
458       c.innerHTML="&nbsp;" + datePointer + "&nbsp;";
459       c.className=dateClass;
460       var bordercolor=(datePointer==this.odateSelected) && (this.monthSelected==this.omonthSelected) && (this.yearSelected==this.oyearSelected) ? this.options.selectedDateBorder : this.bgcolor;
461       c.style.border='1px solid '+bordercolor;
462       var h=this.Holidays[this.holidayKey(this.yearSelected,this.monthSelected,datePointer)];
463       if (!h)  {
464         h=this.Holidays[this.holidayKey(0,this.monthSelected,datePointer)];
465       }
466       c.style.color=h ? h.txtColor : '';
467       c.style.backgroundColor=h ? h.bgColor : '';
468       c.title=h ? h.desc : '';
469       if (colnum==7) r++;
470     }
471     while (dayPointer<42) {
472       colnum=dayPointer % 7 + 1;
473       this.resetCell(this.tbody.rows[r].cells[colnum]);
474       dayPointer++;
475       if (colnum==7) r++;
476     }
477
478     this.titleMonth.innerHTML = RicoTranslate.monthAbbr(this.monthSelected);
479     this.titleYear.innerHTML = this.yearSelected;
480     if (this.todayCell) {
481       this.todayCell.innerHTML = RicoTranslate.getPhraseById("calToday",this.dateNow,RicoTranslate.monthAbbr(this.monthNow),this.yearNow,this.monthNow+1);
482     }
483   },
484   
485 /** @private */
486   resetCell: function(c) {
487     c.innerHTML="&nbsp;";
488     c.className='ricoCalEmpty';
489     c.style.border='1px solid '+this.bgcolor;
490     c.style.color='';
491     c.style.backgroundColor='';
492     c.title='';
493   },
494   
495 /** @private */
496   saveAndClose : function(e) {
497     Event.stop(e);
498     var el=Event.element(e);
499     var s=el.innerHTML.replace(/&nbsp;/g,'');
500     if (s=='' || el.className=='ricoCalWeekNum') return;
501     var day=parseInt(s,10);
502     if (isNaN(day)) return;
503     var d=new Date(this.yearSelected,this.monthSelected,day);
504     var dateStr=d.formatDate(this.dateFmt=='ISO8601' ? 'yyyy-mm-dd' : this.dateFmt);
505     if (this.returnValue) this.returnValue(dateStr);
506     this.close();
507   },
508
509   open : function(curval) {
510     if (!this.bPageLoaded) return;
511     var today = new Date();
512     this.dateNow  = today.getDate();
513     this.monthNow = today.getMonth();
514     this.yearNow  = today.getFullYear();
515     if (typeof curval=='object') {
516       this.dateSelected  = curval.getDate();
517       this.monthSelected = curval.getMonth();
518       this.yearSelected  = curval.getFullYear();
519     } else if (this.dateFmt=='ISO8601') {
520       var d=new Date();
521       d.setISO8601(curval);
522       this.dateSelected  = d.getDate();
523       this.monthSelected = d.getMonth();
524       this.yearSelected  = d.getFullYear();
525     } else if (this.re.exec(curval)) {
526       var aDate = [ RegExp.$1, RegExp.$3, RegExp.$5 ];
527       this.dateSelected  = parseInt(aDate[this.dateParts.dd], 10);
528       this.monthSelected = parseInt(aDate[this.dateParts.mm], 10) - 1;
529       this.yearSelected  = parseInt(aDate[this.dateParts.yyyy], 10);
530       if (this.yearSelected < 100) {
531         // apply a century to 2-digit years
532         this.yearSelected+=this.yearNow - (this.yearNow % 100);
533         var maxyr=this.options.maxDate.getFullYear();
534         while (this.yearSelected > maxyr) this.yearSelected-=100;
535       }
536     } else {
537       if (curval) {
538         alert('ERROR: invalid date passed to calendar ('+curval+')');
539       }
540       this.dateSelected  = this.dateNow;
541       this.monthSelected = this.monthNow;
542       this.yearSelected  = this.yearNow;
543     }
544     this.odateSelected=this.dateSelected;
545     this.omonthSelected=this.monthSelected;
546     this.oyearSelected=this.yearSelected;
547     this.constructCalendar();
548     this.openPopup();
549   }
550 });
551
552 Rico.includeLoaded('ricoCalendar.js');