Other recent changes had broken the Rico calendar on IE6-8. This update fixes all...
[infodrom/rico3] / 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     var div=Rico.$(this.id);
99     if (div) {
100       this.setDiv(div);
101     } else {
102       this.createContainer();
103       this.container.id=this.id;
104     }
105     Rico.addClass(this.content, Rico.theme.calendar || 'ricoCalContainer');
106     this.content.style.display='block';  // override jquery ui
107
108     // Navigation controls
109     this.heading=this.content.appendChild(document.createElement("div"));
110     this.heading.className='RicoCalHeading';
111     if (Rico.theme.calendarHeading) Rico.addClass(this.heading,Rico.theme.calendarHeading)
112     var monthHdg=this._createTitleSection('Month');
113     this.heading.appendChild(monthHdg);
114     this.heading.appendChild(this._createTitleSection('Year'));
115     new Rico.HoverSet(this.heading.getElementsByTagName('a'));
116     if (this.position == 'absolute') this.heading.appendChild(Rico.closeButton(Rico.eventHandle(this,'close')));
117
118     this.maintab=document.createElement("table");
119     this.maintab.cellSpacing=2;
120     this.maintab.cellPadding=0;
121     this.maintab.border=0;
122     this.maintab.style.borderCollapse='separate';
123     this.maintab.className='ricoCalTab';
124     if (Rico.theme.calendarTable) Rico.addClass(this.maintab,Rico.theme.calendarTable)
125     this.tbody=Rico.getTBody(this.maintab);
126
127     var r,c,i,j,img,dow,a,s,tab;
128     this.colStart=this.options.showWeekNumber ? 1 : 0;
129     for (i=0; i<7; i++) {
130       r=this.tbody.insertRow(-1);
131       r.className='row'+i;
132       for (c=0; c<7+this.colStart; c++) {
133         r.insertCell(-1);
134       }
135     }
136     r=this.tbody.rows[0];
137     r.className='ricoCalDayNames';
138     if (this.options.showWeekNumber) {
139       r.cells[0].innerHTML=this.weekString;
140       for (i=0; i<7; i++) {
141         this.tbody.rows[i].cells[0].className='ricoCalWeekNum';
142       }
143     }
144     this.styles=[];
145     for (i=0; i<7; i++) {
146       dow=(i+this.options.startAt) % 7;
147       r.cells[i+this.colStart].innerHTML=Rico.dayAbbr(dow);
148       this.styles[i]='ricoCal'+dow;
149     }
150     
151     // table footer (today)
152     if (this.options.showToday) {
153       this.tfoot=this.maintab.createTFoot();
154       r=this.tfoot.insertRow(-1);
155       this.todayCell=r.insertCell(-1);
156       this.todayCell.colSpan=7+this.colStart;
157       if (Rico.theme.calendarFooter) Rico.addClass(this.todayCell,Rico.theme.calendarFooter);
158       Rico.eventBind(this.todayCell,"click", Rico.eventHandle(this,'selectNow'), false);
159     }
160     this.content.appendChild(this.maintab);
161     new Rico.HoverSet(this.tbody.getElementsByTagName('td'),{ hoverNodes: function(e) { return e.innerHTML.match(/^\d+$/) ? [e] : []; } });
162     
163     // month selector
164     this.monthPopup=new Rico.Popup(document.createElement("div"));
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.container.appendChild(this.monthPopup.container);
189     
190     // year selector
191     this.yearPopup=new Rico.Popup(document.createElement("div"));
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 p1=document.createElement("p");
196     p1.innerHTML=Rico.getPhraseById("calYearRange",this.options.minDate.getFullYear(),this.options.maxDate.getFullYear());
197     var p2=document.createElement("p");
198     this.yearInput=p2.appendChild(document.createElement("input"));
199     this.yearInput.maxlength=4;
200     this.yearInput.size=4;
201     Rico.eventBind(this.yearInput,"keyup", Rico.eventHandle(this,'yearKey'), false);
202     a=Rico.floatButton('Checkmark', Rico.eventHandle(this,'processPopUpYear'));
203     p2.appendChild(a);
204     a=Rico.floatButton('Cancel', Rico.eventHandle(this,'popDownYear'));
205     p2.appendChild(a);
206     this.yearPopup.content.appendChild(p1);
207     this.yearPopup.content.appendChild(p2);
208     this.container.appendChild(this.yearPopup.container);
209     this.yearPopup.container.style.left='';
210     this.yearPopup.container.style.right='5px';
211     this.yearPopup.container.style.zIndex=10;
212
213     // fix anchors so they work in IE6
214     a=this.content.getElementsByTagName('a');
215     for (i=0; i<a.length; i++) {
216       a[i].href='javascript:void(0)';
217     }
218     
219     Rico.eventBind(this.tbody,"click", Rico.eventHandle(this,'saveAndClose'));
220     this.close();
221     this.bPageLoaded=true;
222   },
223
224   _createTitleSection : function(section) {
225     var s=document.createElement("span");
226     s.className='RicoCal'+section+'Heading';
227     if (Rico.theme.calendarSubheading) Rico.addClass(s,Rico.theme.calendarSubheading);
228
229     var a=s.appendChild(document.createElement("a"));
230     a.className='Rico_leftArrow';
231     a.appendChild(this.createNavArrow('dec'+section,'left'));
232
233     var a=s.appendChild(document.createElement("a"));
234     Rico.eventBind(a,"click", Rico.eventHandle(this,'popUp'+section), false);
235     this['title'+section]=a;
236
237     a=s.appendChild(document.createElement("a"));
238     a.className='Rico_rightArrow';
239     a.appendChild(this.createNavArrow('inc'+section,'right'));
240     return s
241   },
242   
243   selectNow : function() {
244     var today = new Date();
245     this.dateNow  = today.getDate();
246     this.monthNow = today.getMonth();
247     this.yearNow  = today.getFullYear();
248     this.monthSelected=this.monthNow;
249     this.yearSelected=this.yearNow;
250     this.constructCalendar();
251   },
252   
253 /** @private */
254   createNavArrow: function(funcname,direction) {
255     var span=document.createElement("span");
256     span.className=Rico.theme[direction+'Arrow'] || 'rico-icon Rico_'+direction+'Arrow';
257     span.style.display="inline-block";
258     Rico.eventBind(span,"click", Rico.eventHandle(this,funcname), false);
259     return span;
260   },
261
262 /**
263  * @returns true if yr/mo is within minDate/MaxDate
264  */
265   isValidMonth : function(yr,mo) {
266     if (yr < this.options.minDate.getFullYear()) return false;
267     if (yr == this.options.minDate.getFullYear() && mo < this.options.minDate.getMonth()) return false;
268     if (yr > this.options.maxDate.getFullYear()) return false;
269     if (yr == this.options.maxDate.getFullYear() && mo > this.options.maxDate.getMonth()) return false;
270     return true;
271   },
272
273   incMonth : function() {
274     var newMonth=this.monthSelected+1;
275     var newYear=this.yearSelected;
276     if (newMonth>11) {
277       newMonth=0;
278       newYear++;
279     }
280     if (!this.isValidMonth(newYear,newMonth)) return;
281     this.monthSelected=newMonth;
282     this.yearSelected=newYear;
283     this.constructCalendar();
284   },
285
286   decMonth : function() {
287     var newMonth=this.monthSelected-1;
288     var newYear=this.yearSelected;
289     if (newMonth<0) {
290       newMonth=11;
291       newYear--;
292     }
293     if (!this.isValidMonth(newYear,newMonth)) return;
294     this.monthSelected=newMonth;
295     this.yearSelected=newYear;
296     this.constructCalendar();
297   },
298   
299 /** @private */
300   selectMonth : function(e) {
301     var el=Rico.eventElement(e);
302     this.monthSelected=parseInt(el.name,10);
303     this.constructCalendar();
304     Rico.eventStop(e);
305   },
306
307   popUpMonth : function(e) {
308     Rico.eventStop(e);
309     if (this.monthPopup.visible()) {
310       this.popDownMonth();
311       return;
312     }
313     this.popDownYear();
314     if (Rico.isIE && Rico.ieVersion < 7) {
315       // fix position absolute inside container without hasLayout
316       this.monthPopup.openPopup(null, this.heading.offsetHeight+2);
317       this.monthPopup.container.style.left='';
318     } else {
319       this.monthPopup.openPopup(3, this.heading.offsetHeight+2);
320     }
321     return false;
322   },
323
324   popDownMonth : function() {
325     this.monthPopup.closePopup();
326   },
327
328   popDownYear : function() {
329     this.yearPopup.closePopup();
330     this.yearInput.disabled=true;  // make sure this does not get submitted
331   },
332
333 /**
334  * Prompt for year
335  */
336   popUpYear : function(e) {
337     Rico.eventStop(e);
338     if (this.yearPopup.visible()) {
339       this.popDownYear();
340       return;
341     }
342     this.popDownMonth();
343     this.yearInput.disabled=false;
344     this.yearInput.value='';   // this.yearSelected
345     this.yearPopup.openPopup(null, this.heading.offsetHeight+2);
346     var self=this;
347     setTimeout(function() { self.yearInput.focus(); }, 10);  // ie8 has issues without this delay
348     return false;
349   },
350   
351   yearKey : function(e) {
352     switch (Rico.eventKey(e)) {
353       case 27: this.popDownYear(); Rico.eventStop(e); return false;
354       case 13: this.processPopUpYear(); Rico.eventStop(e); return false;
355     }
356     return true;
357   },
358   
359   processPopUpYear : function() {
360     var newYear=this.yearInput.value;
361     newYear=parseInt(newYear,10);
362     if (isNaN(newYear) || newYear<this.options.minDate.getFullYear() || newYear>this.options.maxDate.getFullYear()) {
363       alert(Rico.getPhraseById("calInvalidYear"));
364     } else {
365       this.yearSelected=newYear;
366       this.popDownYear();
367       this.constructCalendar();
368     }
369   },
370   
371   incYear : function() {
372     if (this.yearSelected>=this.options.maxDate.getFullYear()) return;
373     this.yearSelected++;
374     this.constructCalendar();
375   },
376
377   decYear : function() {
378     if (this.yearSelected<=this.options.minDate.getFullYear()) return;
379     this.yearSelected--;
380     this.constructCalendar();
381   },
382
383   // tried a number of different week number functions posted on the net
384   // this is the only one that produced consistent results when comparing week numbers for December and the following January
385   WeekNbr : function(year,month,day) {
386     var when = new Date(year,month,day);
387     var newYear = new Date(year,0,1);
388     var offset = 7 + 1 - newYear.getDay();
389     if (offset == 8) offset = 1;
390     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;
391     var weeknum = Math.floor((daynum-offset+7)/7);
392     if (weeknum == 0) {
393       year--;
394       var prevNewYear = new Date(year,0,1);
395       var prevOffset = 7 + 1 - prevNewYear.getDay();
396       weeknum = (prevOffset == 2 || prevOffset == 8) ? 53 : 52;
397     }
398     return weeknum;
399   },
400
401   constructCalendar : function() {
402     var aNumDays = [31,0,31,30,31,30,31,31,30,31,30,31];
403     var startDate = new Date (this.yearSelected,this.monthSelected,1);
404     var endDate,numDaysInMonth,i,colnum;
405
406     if (typeof this.monthSelected!='number' || this.monthSelected>=12 || this.monthSelected<0) {
407       alert('ERROR in calendar: monthSelected='+this.monthSelected);
408       return;
409     }
410
411     if (this.monthSelected==1) {
412       endDate = new Date (this.yearSelected,this.monthSelected+1,1);
413       endDate = new Date (endDate - (24*60*60*1000));
414       numDaysInMonth = endDate.getDate();
415     } else {
416       numDaysInMonth = aNumDays[this.monthSelected];
417     }
418     var dayPointer = startDate.getDay() - this.options.startAt;
419     if (dayPointer<0) dayPointer+=7;
420     this.popDownMonth();
421     this.popDownYear();
422
423     //this.bgcolor=Rico.getStyle(this.tbody,'background-color');
424     //this.bgcolor=this.bgcolor.replace(/\"/g,'');
425     if (this.options.showWeekNumber) {
426       for (i=1; i<7; i++) {
427         this.tbody.rows[i].cells[0].innerHTML='&nbsp;';
428       }
429     }
430     for ( i=0; i<dayPointer; i++ ) {
431       this.resetCell(this.tbody.rows[1].cells[i+this.colStart]);
432     }
433
434     for ( var datePointer=1,r=1; datePointer<=numDaysInMonth; datePointer++,dayPointer++ ) {
435       colnum=dayPointer % 7;
436       if (this.options.showWeekNumber && colnum==0) {
437         this.tbody.rows[r].cells[0].innerHTML=this.WeekNbr(this.yearSelected,this.monthSelected,datePointer);
438       }
439       var c=this.tbody.rows[r].cells[colnum+this.colStart];
440       c.innerHTML=datePointer;
441       c.className=this.styles[colnum];
442       if ((datePointer==this.dateNow)&&(this.monthSelected==this.monthNow)&&(this.yearSelected==this.yearNow)) {
443         Rico.addClass(c,Rico.theme.calendarToday || 'ricoCalToday');
444       }
445       if (Rico.theme.calendarDay) Rico.addClass(c,Rico.theme.calendarDay);
446       if ((datePointer==this.odateSelected) && (this.monthSelected==this.omonthSelected) && (this.yearSelected==this.oyearSelected)) {
447         Rico.addClass(c,Rico.theme.calendarSelectedDay || 'ricoSelectedDay');
448       }
449       var h=this.Holidays[this.holidayKey(this.yearSelected,this.monthSelected,datePointer)];
450       if (!h)  {
451         h=this.Holidays[this.holidayKey(0,this.monthSelected,datePointer)];
452       }
453       c.style.color=h ? h.txtColor : '';
454       c.style.backgroundColor=h ? h.bgColor : '';
455       c.title=h ? h.desc : '';
456       if (colnum==6) r++;
457     }
458     while (dayPointer<42) {
459       colnum=dayPointer % 7;
460       this.resetCell(this.tbody.rows[r].cells[colnum+this.colStart]);
461       dayPointer++;
462       if (colnum==6) r++;
463     }
464
465     this.titleMonth.innerHTML = Rico.monthAbbr(this.monthSelected);
466     this.titleYear.innerHTML = this.yearSelected;
467     if (this.todayCell) {
468       this.todayCell.innerHTML = Rico.getPhraseById("calToday",this.dateNow,Rico.monthAbbr(this.monthNow),this.yearNow,this.monthNow+1);
469     }
470   },
471   
472 /** @private */
473   resetCell: function(c) {
474     c.innerHTML="&nbsp;";
475     c.className='ricoCalEmpty';
476     c.style.color='';
477     c.style.backgroundColor='';
478     c.title='';
479   },
480   
481 /** @private */
482   saveAndClose : function(e) {
483     Rico.eventStop(e);
484     var el=Rico.eventElement(e);
485     var s=el.innerHTML.replace(/&nbsp;/g,'');
486     if (s=='' || el.className=='ricoCalWeekNum') return;
487     var day=parseInt(s,10);
488     if (isNaN(day)) return;
489     var d=new Date(this.yearSelected,this.monthSelected,day);
490     var dateStr=Rico.formatDate(d,this.dateFmt=='ISO8601' ? 'yyyy-mm-dd' : this.dateFmt);
491     if (this.returnValue) {
492       this.returnValue(dateStr);
493       this.close();
494     }
495   },
496
497   open : function(curval) {
498     if (!this.bPageLoaded) return;
499     var today = new Date();
500     this.dateNow  = today.getDate();
501     this.monthNow = today.getMonth();
502     this.yearNow  = today.getFullYear();
503     this.oyearSelected = -1;
504     if (typeof curval=='object') {
505       this.odateSelected  = curval.getDate();
506       this.omonthSelected = curval.getMonth();
507       this.oyearSelected  = curval.getFullYear();
508     } else if (this.dateFmt=='ISO8601') {
509       var d=Rico.setISO8601(curval);
510       if (d) {
511         this.odateSelected  = d.getDate();
512         this.omonthSelected = d.getMonth();
513         this.oyearSelected  = d.getFullYear();
514       }
515     } else if (this.re.exec(curval)) {
516       var aDate = [ RegExp.$1, RegExp.$3, RegExp.$5 ];
517       this.odateSelected  = parseInt(aDate[this.dateParts.dd], 10);
518       this.omonthSelected = parseInt(aDate[this.dateParts.mm], 10) - 1;
519       this.oyearSelected  = parseInt(aDate[this.dateParts.yyyy], 10);
520       if (this.oyearSelected < 100) {
521         // apply a century to 2-digit years
522         this.oyearSelected+=this.yearNow - (this.yearNow % 100);
523         var maxyr=this.options.maxDate.getFullYear();
524         while (this.oyearSelected > maxyr) this.oyearSelected-=100;
525       }
526     } else {
527       if (curval) {
528         alert('ERROR: invalid date passed to calendar ('+curval+')');
529       }
530     }
531     if (this.oyearSelected > 0) {
532       this.dateSelected=this.odateSelected;
533       this.monthSelected=this.omonthSelected;
534       this.yearSelected=this.oyearSelected;
535     } else {
536       this.dateSelected=this.dateNow;
537       this.monthSelected=this.monthNow;
538       this.yearSelected=this.yearNow;
539     }
540     this.constructCalendar();
541     this.openPopup();
542   }
543 };