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