Fix wheel scrolling on LiveGrid content
[infodrom/rico3] / minsrc / ricoCalendar.js
1 /*
2  *  (c) 2005-2011 Richard Cowin (http://openrico.org)
3  *  (c) 2005-2011 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     this.defaultMin = new Date(today.getFullYear()-50,0,1);
43     this.defaultMax = new Date(today.getFullYear()+50,11,31);
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       dateFmt : 'ISO8601',
51       minDate : this.defaultMin,
52       maxDate : this.defaultMax
53     });
54     Rico.extend(this.options, options || {});
55     this.bPageLoaded=false;
56     this.Holidays={};
57     this.re=/^\s*(\w+)(\W)(\w+)(\W)(\w+)/i;
58     this.setDateFmt(this.options.dateFmt);
59     var self=this;
60     Rico.onLoad(function() { self.atLoad(); })
61   },
62
63
64   setDateFmt: function(fmt) {
65     this.dateFmt=(fmt=='rico') ? Rico.dateFmt : fmt;
66     Rico.log(this.id+' date format set to '+this.dateFmt);
67     this.dateParts={};
68     if (this.re.exec(this.dateFmt)) {
69       this.dateParts[RegExp.$1]=0;
70       this.dateParts[RegExp.$3]=1;
71       this.dateParts[RegExp.$5]=2;
72     }
73   },
74   
75 /**
76  * Call before displaying calendar to highlight special days
77  * @param d day (1-31)
78  * @param m month (1-12)
79  * @param y year (0 implies a repeating holiday)
80  * @param desc description
81  * @param bgColor background color for cell displaying this day (CSS value, defaults to '#DDF')
82  * @param txtColor text color for cell displaying this day (CSS value), if not specified it is displayed with the same color as other days
83  */
84   addHoliday : function(d, m, y, desc, bgColor, txtColor) {
85     this.Holidays[this.holidayKey(y,m-1,d)]={desc:desc, txtColor:txtColor, bgColor:bgColor || '#DDF'};
86   },
87   
88 /** @private */
89   holidayKey : function(y,m,d) {
90     return 'h'+Rico.zFill(y,4)+Rico.zFill(m,2)+Rico.zFill(d,2);
91   },
92
93   atLoad : function() {
94     Rico.log('Calendar#atLoad: '+this.id);
95     var div=Rico.$(this.id);
96     if (div) {
97       this.setDiv(div);
98     } else {
99       this.createContainer();
100       this.container.id=this.id;
101     }
102     Rico.addClass(this.content, Rico.theme.calendar || 'ricoCalContainer');
103     this.direction=Rico.direction(this.container);
104
105     var r,c,i,j,dow,a,s,tab;
106     this.colStart=this.options.showWeekNumber ? 1 : 0;
107     var colcnt=7+this.colStart
108     this.maintab=document.createElement("table");
109     this.maintab.cellSpacing=2;
110     this.maintab.cellPadding=0;
111     this.maintab.border=0;
112     this.maintab.style.borderCollapse='separate';
113     this.maintab.className=Rico.theme.calendarTable || 'ricoCalTab';
114
115     // thead (Navigation controls)
116     this.thead=this.maintab.createTHead();
117     r=this.thead.insertRow(-1);
118     this.heading=r.insertCell(-1);
119     this.heading.colSpan=colcnt;
120     //this.heading=this.content.appendChild(document.createElement("div"));
121     this.heading.className='RicoCalHeading';
122     if (Rico.theme.calendarHeading) Rico.addClass(this.heading,Rico.theme.calendarHeading)
123
124     // table footer (today)
125     if (this.options.showToday) {
126       this.tfoot=this.maintab.createTFoot();
127       this.tfoot.className='ricoCalFoot';
128       r=this.tfoot.insertRow(-1);
129       this.todayCell=r.insertCell(-1);
130       this.todayCell.colSpan=colcnt;
131       this.todayCell.className=Rico.theme.calendarFooter || 'ricoCalFoot';
132       Rico.eventBind(this.todayCell,"click", Rico.eventHandle(this,'selectNow'), false);
133     }
134
135     this.tbody=Rico.getTBody(this.maintab);
136     this.tbody.className='ricoCalBody';
137
138     this.content.style.display='block';
139     if (this.position == 'absolute') {
140       this.content.style.width='auto';
141       this.maintab.style.width='auto';
142     } else {
143       this.container.style.position='relative';
144       this.heading.style.position='static';  // fixes issue with ie7
145       this.content.style.padding='0px';
146       this.content.style.width='15em';
147       this.maintab.style.width='100%';
148     }
149
150     this.styles=[];
151     for (i=0; i<7; i++) {
152       r=this.tbody.insertRow(-1);
153       r.className=i==0 ? 'ricoCalDayNames' : 'row'+i;
154       if (this.options.showWeekNumber) {
155         c=r.insertCell(-1);
156         c.className='ricoCalWeekNum';
157         if (i==0) c.innerHTML=Rico.getPhraseById("calWeekHdg");
158       }
159       for (j=0; j<7; j++) {
160         c=r.insertCell(-1);
161         if (i==0) {
162           dow=(j+this.options.startAt) % 7;
163           c.innerHTML=Rico.dayAbbr(dow);
164           this.styles[j]='ricoCal'+dow;
165         } else {
166           c.className=this.styles[j];
167           if (Rico.theme.calendarDay) Rico.addClass(c,Rico.theme.calendarDay);
168         }
169       }
170     }
171     
172     this.content.appendChild(this.maintab);
173     new Rico.HoverSet(this.tbody.getElementsByTagName('td'),{ hoverNodes: function(e) { return e.innerHTML.match(/^\d+$/) ? [e] : []; } });
174
175     this.navtab=this.heading.appendChild(document.createElement("table"));
176     this.navrow=this.navtab.insertRow(-1);
177     this._createTitleSection('Month');
178     this.navrow.insertCell(-1).innerHTML="&nbsp;&nbsp;";
179     this._createTitleSection('Year');
180     new Rico.HoverSet(this.heading.getElementsByTagName('a'));
181     if (this.position == 'absolute') this.heading.appendChild(Rico.closeButton(Rico.eventHandle(this,'close')));
182     
183     // month selector
184     this.monthPopup=new Rico.Popup(document.createElement("div"),{shim:false,zIndex:10});
185     this.monthPopup.content.className='ricoCalMonthPrompt';
186     tab=document.createElement("table");
187     tab.className='ricoCalMenu';
188     if (Rico.theme.calendarPopdown) Rico.addClass(tab,Rico.theme.calendarPopdown);
189     tab.cellPadding=2;
190     tab.cellSpacing=0;
191     tab.border=0;
192     tab.style.borderCollapse='separate';
193     tab.style.margin='0px';
194     for (i=0; i<4; i++) {
195       r=tab.insertRow(-1);
196       for (j=0; j<3; j++) {
197         c=r.insertCell(-1);
198         a=document.createElement("a");
199         a.innerHTML=Rico.monthAbbr(i*3+j);
200         a.name=i*3+j;
201         if (Rico.theme.calendarDay) Rico.addClass(a,Rico.theme.calendarDay);
202         c.appendChild(a);
203         Rico.eventBind(a,"click", Rico.eventHandle(this,'selectMonth'), false);
204       }
205     }
206     new Rico.HoverSet(tab.getElementsByTagName('a'));
207     this.monthPopup.content.appendChild(tab);
208     this.container.appendChild(this.monthPopup.container);
209     this.monthPopup.closePopup();
210     
211     // year selector
212     this.yearPopup=new Rico.Popup(document.createElement("div"),{shim:false,zIndex:10});
213     this.yearPopup.content.className='ricoCalYearPrompt';
214     if (Rico.theme.calendarPopdown) Rico.addClass(this.yearPopup.content,Rico.theme.calendarPopdown);
215     this.yearPrompt=document.createElement("p");
216     this.yearPrompt.innerHTML="&nbsp;";
217     var p2=document.createElement("p");
218     this.yearInput=p2.appendChild(document.createElement("input"));
219     this.yearInput.maxlength=4;
220     this.yearInput.size=4;
221     Rico.eventBind(this.yearInput,"keyup", Rico.eventHandle(this,'yearKey'), false);
222     a=Rico.floatButton('Checkmark', Rico.eventHandle(this,'processPopUpYear'));
223     p2.appendChild(a);
224     a=Rico.floatButton('Cancel', Rico.eventHandle(this,'popDownYear'));
225     p2.appendChild(a);
226     this.yearPopup.content.appendChild(this.yearPrompt);
227     this.yearPopup.content.appendChild(p2);
228     this.container.appendChild(this.yearPopup.container);
229     this.yearPopup.closePopup();
230
231     // fix anchors so they work in IE6
232     a=this.content.getElementsByTagName('a');
233     for (i=0; i<a.length; i++) {
234       a[i].href='javascript:void(0)';
235     }
236     
237     Rico.eventBind(this.tbody,"click", Rico.eventHandle(this,'saveAndClose'));
238     this.bPageLoaded=true;
239   },
240
241   _createTitleSection : function(section) {
242     var arrows=['left','right'];
243     if (this.direction=='rtl') arrows.reverse();
244     var c=this.navrow.insertCell(-1);
245     var a=c.appendChild(document.createElement("a"));
246     a.className='Rico_'+arrows[0]+'Arrow';
247     a.appendChild(this._createNavArrow(arrows[0]));
248     Rico.eventBind(a,"click", Rico.eventHandle(this,'dec'+section), false);
249
250     c=this.navrow.insertCell(-1);
251     a=c.appendChild(document.createElement("a"));
252     Rico.eventBind(a,"click", Rico.eventHandle(this,'popUp'+section), false);
253     this['title'+section]=a;
254
255     c=this.navrow.insertCell(-1);
256     a=c.appendChild(document.createElement("a"));
257     a.className='Rico_'+arrows[1]+'Arrow';
258     a.appendChild(this._createNavArrow(arrows[1]));
259     Rico.eventBind(a,"click", Rico.eventHandle(this,'inc'+section), false);
260   },
261   
262   _createNavArrow: function(direction) {
263     var span=document.createElement("span");
264     span.className=Rico.theme[direction+'Arrow'] || 'rico-icon Rico_'+direction+'Arrow';
265     span.style.display="inline-block";
266     return span;
267   },
268
269   selectNow : function() {
270     var today = new Date();
271     this.dateNow  = today.getDate();
272     this.monthNow = today.getMonth();
273     this.yearNow  = today.getFullYear();
274     this.monthSelected=this.monthNow;
275     this.yearSelected=this.yearNow;
276     this.constructCalendar();
277   },
278   
279 /**
280  * @returns true if yr/mo is within minDate/MaxDate
281  */
282   isValidMonth : function(yr,mo) {
283     if (yr < this.options.minDate.getFullYear()) return false;
284     if (yr == this.options.minDate.getFullYear() && mo < this.options.minDate.getMonth()) return false;
285     if (yr > this.options.maxDate.getFullYear()) return false;
286     if (yr == this.options.maxDate.getFullYear() && mo > this.options.maxDate.getMonth()) return false;
287     return true;
288   },
289
290   incMonth : function(e) {
291     if (e) Rico.eventStop(e);
292     var newMonth=this.monthSelected+1;
293     var newYear=this.yearSelected;
294     if (newMonth>11) {
295       newMonth=0;
296       newYear++;
297     }
298     if (!this.isValidMonth(newYear,newMonth)) return;
299     this.monthSelected=newMonth;
300     this.yearSelected=newYear;
301     this.constructCalendar();
302   },
303
304   decMonth : function(e) {
305     if (e) Rico.eventStop(e);
306     var newMonth=this.monthSelected-1;
307     var newYear=this.yearSelected;
308     if (newMonth<0) {
309       newMonth=11;
310       newYear--;
311     }
312     if (!this.isValidMonth(newYear,newMonth)) return;
313     this.monthSelected=newMonth;
314     this.yearSelected=newYear;
315     this.constructCalendar();
316   },
317   
318 /** @private */
319   selectMonth : function(e) {
320     var el=Rico.eventElement(e);
321     this.monthSelected=parseInt(el.name,10);
322     this.constructCalendar();
323     Rico.eventStop(e);
324   },
325   
326   // position: 0=left, 1=right
327   openYrMo : function(popup,position) {
328     if (this.direction=='rtl') position=1-position;
329     popup.openPopup();
330     var left=position ? this.content.offsetWidth - popup.container.offsetWidth - 5 : 3;
331     popup.move(left, this.heading.offsetHeight+2);
332   },
333
334   popUpMonth : function(e) {
335     Rico.eventStop(e);
336     if (this.monthPopup.visible()) {
337       this.popDownMonth();
338       return false;
339     }
340     this.popDownYear();
341     this.openYrMo(this.monthPopup,0);
342     return false;
343   },
344
345   popDownMonth : function() {
346     this.monthPopup.closePopup();
347   },
348
349   popDownYear : function() {
350     this.yearPopup.closePopup();
351     this.yearInput.disabled=true;  // make sure this does not get submitted
352   },
353
354 /**
355  * Prompt for year
356  */
357   popUpYear : function(e) {
358     Rico.eventStop(e);
359     if (this.yearPopup.visible()) {
360       this.popDownYear();
361       return false;
362     }
363     this.popDownMonth();
364     this.yearPrompt.innerHTML=Rico.getPhraseById("calYearRange",this.options.minDate.getFullYear(),this.options.maxDate.getFullYear());
365     this.yearInput.disabled=false;
366     this.yearInput.value='';   // this.yearSelected
367     this.openYrMo(this.yearPopup,1);
368     var self=this;
369     setTimeout(function() { self.yearInput.focus(); }, 10);  // ie8 has issues without this delay
370     return false;
371   },
372   
373   yearKey : function(e) {
374     switch (Rico.eventKey(e)) {
375       case 27: this.popDownYear(); Rico.eventStop(e); return false;
376       case 13: this.processPopUpYear(); Rico.eventStop(e); return false;
377     }
378     return true;
379   },
380   
381   processPopUpYear : function() {
382     var newYear=this.yearInput.value;
383     newYear=parseInt(newYear,10);
384     if (isNaN(newYear) || newYear<this.options.minDate.getFullYear() || newYear>this.options.maxDate.getFullYear()) {
385       alert(Rico.getPhraseById("calInvalidYear"));
386     } else {
387       this.yearSelected=newYear;
388       this.popDownYear();
389       this.constructCalendar();
390     }
391   },
392   
393   incYear : function(e) {
394     if (e) Rico.eventStop(e);
395     if (this.yearSelected>=this.options.maxDate.getFullYear()) return;
396     this.yearSelected++;
397     this.constructCalendar();
398   },
399
400   decYear : function(e) {
401     if (e) Rico.eventStop(e);
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       c.style.visibility='visible';
481       if (colnum==6) r++;
482     }
483     while (dayPointer<42) {
484       colnum=dayPointer % 7;
485       this.resetCell(this.tbody.rows[r].cells[colnum+this.colStart]);
486       dayPointer++;
487       if (colnum==6) r++;
488     }
489
490     this.titleMonth.innerHTML = Rico.monthAbbr(this.monthSelected);
491     this.titleYear.innerHTML = this.yearSelected;
492     if (this.todayCell) {
493       this.todayCell.innerHTML = Rico.getPhraseById("calToday",this.dateNow,Rico.monthAbbr(this.monthNow),this.yearNow,this.monthNow+1);
494     }
495   },
496   
497 /** @private */
498   resetCell: function(c) {
499     c.innerHTML="&nbsp;";
500     c.title='';
501     c.style.visibility='hidden';
502   },
503   
504   close: function(e) {
505     if (e) Rico.eventStop(e);
506     this.closePopup();
507   },
508   
509 /** @private */
510   saveAndClose : function(e) {
511     Rico.eventStop(e);
512     var el=Rico.eventElement(e);
513     var s=el.innerHTML.replace(/&nbsp;/g,'');
514     if (s=='' || el.className=='ricoCalWeekNum') return;
515     var day=parseInt(s,10);
516     if (isNaN(day)) return;
517     var d=new Date(this.yearSelected,this.monthSelected,day);
518     var dateStr=Rico.formatDate(d,this.dateFmt=='ISO8601' ? 'yyyy-mm-dd' : this.dateFmt);
519     if (this.returnValue) {
520       this.returnValue(dateStr);
521       this.closePopup();
522     }
523   },
524
525   open : function(curval,column) {
526     if (!this.bPageLoaded) return;
527     if (column) {
528       this.setDateFmt(column.format.dateFmt);
529       this.options.minDate=column.format.min || this.defaultMin;
530       this.options.maxDate=column.format.max || this.defaultMax;
531     }
532     var today = new Date();
533     this.dateNow  = today.getDate();
534     this.monthNow = today.getMonth();
535     this.yearNow  = today.getFullYear();
536     this.oyearSelected = -1;
537     if (typeof curval=='object') {
538       this.odateSelected  = curval.getDate();
539       this.omonthSelected = curval.getMonth();
540       this.oyearSelected  = curval.getFullYear();
541     } else if (this.dateFmt=='ISO8601') {
542       var d=Rico.setISO8601(curval);
543       if (d) {
544         this.odateSelected  = d.getDate();
545         this.omonthSelected = d.getMonth();
546         this.oyearSelected  = d.getFullYear();
547       }
548     } else if (this.re.exec(curval)) {
549       var aDate = [ RegExp.$1, RegExp.$3, RegExp.$5 ];
550       this.odateSelected  = parseInt(aDate[this.dateParts.dd], 10);
551       this.omonthSelected = parseInt(aDate[this.dateParts.mm], 10) - 1;
552       this.oyearSelected  = parseInt(aDate[this.dateParts.yyyy], 10);
553       if (this.oyearSelected < 100) {
554         // apply a century to 2-digit years
555         this.oyearSelected+=this.yearNow - (this.yearNow % 100);
556         var maxyr=this.options.maxDate.getFullYear();
557         while (this.oyearSelected > maxyr) this.oyearSelected-=100;
558       }
559     } else {
560       if (curval) {
561         alert('ERROR: invalid date passed to calendar ('+curval+')');
562       }
563     }
564     if (this.oyearSelected > 0) {
565       this.dateSelected=this.odateSelected;
566       this.monthSelected=this.omonthSelected;
567       this.yearSelected=this.oyearSelected;
568     } else {
569       this.dateSelected=this.dateNow;
570       this.monthSelected=this.monthNow;
571       this.yearSelected=this.yearNow;
572     }
573     this.constructCalendar();
574     this.openPopup();
575   }
576 };