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