.net server control is almost feature complete and functional. All .net examples...
[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     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='ricoCalTab';
114     if (Rico.theme.calendarTable) Rico.addClass(this.maintab,Rico.theme.calendarTable)
115     this.tbody=Rico.getTBody(this.maintab);
116
117     var r,c,i,j,img,dow,a,s,tab;
118     this.colStart=this.options.showWeekNumber ? 1 : 0;
119     for (i=0; i<7; i++) {
120       r=this.tbody.insertRow(-1);
121       r.className='row'+i;
122       for (c=0; c<7+this.colStart; c++) {
123         r.insertCell(-1);
124       }
125     }
126     r=this.tbody.rows[0];
127     r.className='ricoCalDayNames';
128     if (this.options.showWeekNumber) {
129       r.cells[0].innerHTML=this.weekString;
130       for (i=0; i<7; i++) {
131         this.tbody.rows[i].cells[0].className='ricoCalWeekNum';
132       }
133     }
134     this.styles=[];
135     for (i=0; i<7; i++) {
136       dow=(i+this.options.startAt) % 7;
137       r.cells[i+this.colStart].innerHTML=Rico.dayAbbr(dow);
138       this.styles[i]='ricoCal'+dow;
139     }
140     
141     // Navigation controls
142     this.heading=this.content.appendChild(document.createElement("div"));
143     this.heading.className='RicoCalHeading';
144     if (Rico.theme.calendarHeading) Rico.addClass(this.heading,Rico.theme.calendarHeading)
145     var d2=this.heading.appendChild(document.createElement("div"));
146     d2.className='RicoCalHeadingInner';
147     d2.appendChild(this._createTitleSection('Month'));
148     d2.appendChild(this._createTitleSection('Year'));
149     new Rico.HoverSet(this.heading.getElementsByTagName('a'));
150     new Rico.HoverSet(this.tbody.getElementsByTagName('td'),{ hoverNodes: function(e) { return e.innerHTML.match(/^\d+$/) ? [e] : []; } });
151     if (this.position == 'absolute') this.heading.appendChild(Rico.closeButton(Rico.eventHandle(this,'close')));
152
153     // table footer (today)
154     if (this.options.showToday) {
155       this.tfoot=this.maintab.createTFoot();
156       r=this.tfoot.insertRow(-1);
157       this.todayCell=r.insertCell(-1);
158       this.todayCell.colSpan=7+this.colStart;
159       if (Rico.theme.calendarFooter) Rico.addClass(this.todayCell,Rico.theme.calendarFooter);
160       Rico.eventBind(this.todayCell,"click", Rico.eventHandle(this,'selectNow'), false);
161     }
162     this.content.appendChild(this.maintab);
163     
164     // month selector
165     this.monthPopup=new Rico.Popup(document.createElement("div"));
166     this.monthPopup.closePopup();
167     tab=document.createElement("table");
168     tab.className='ricoCalMenu';
169     if (Rico.theme.calendarPopdown) Rico.addClass(tab,Rico.theme.calendarPopdown);
170     tab.cellPadding=2;
171     tab.cellSpacing=0;
172     tab.border=0;
173     tab.style.borderCollapse='separate';
174     tab.style.margin='0px';
175     for (i=0; i<4; i++) {
176       r=tab.insertRow(-1);
177       for (j=0; j<3; j++) {
178         c=r.insertCell(-1);
179         a=document.createElement("a");
180         a.innerHTML=Rico.monthAbbr(i*3+j);
181         a.name=i*3+j;
182         if (Rico.theme.calendarDay) Rico.addClass(a,Rico.theme.calendarDay);
183         c.appendChild(a);
184         Rico.eventBind(a,"click", Rico.eventHandle(this,'selectMonth'), false);
185       }
186     }
187     new Rico.HoverSet(tab.getElementsByTagName('a'));
188     this.monthPopup.content.appendChild(tab);
189     this.container.appendChild(this.monthPopup.container);
190     
191     // year selector
192     this.yearPopup=new Rico.Popup(document.createElement("div"));
193     this.yearPopup.closePopup();
194     this.yearPopup.content.className='ricoCalYearPrompt';
195     if (Rico.theme.calendarPopdown) Rico.addClass(this.yearPopup.content,Rico.theme.calendarPopdown);
196     var tab=document.createElement("table");
197     tab.cellPadding=2;
198     tab.cellSpacing=0;
199     tab.border=0;
200     tab.style.borderCollapse='separate';
201     tab.style.margin='0px';
202     r=tab.insertRow(-1);
203     this.yearLabel=r.insertCell(-1);
204     this.yearLabel.colSpan=3;
205     this.yearLabel.innerHTML=Rico.getPhraseById("calYearRange",this.options.minDate.getFullYear(),this.options.maxDate.getFullYear());
206     r=tab.insertRow(-1);
207     c=r.insertCell(-1);
208     this.yearInput=c.appendChild(document.createElement("input"));
209     this.yearInput.maxlength=4;
210     this.yearInput.size=4;
211     Rico.eventBind(this.yearInput,"keyup", Rico.eventHandle(this,'yearKey'), false);
212     c=r.insertCell(-1);
213     var a=Rico.floatButton('Checkmark', Rico.eventHandle(this,'processPopUpYear'));
214     Rico.setStyle(a.firstChild,{ margin:"0px", padding:"0px", border:"none" });
215     c.appendChild(a);
216     c=r.insertCell(-1);
217     a=Rico.floatButton('Cancel', Rico.eventHandle(this,'popDownYear'));
218     Rico.setStyle(a.firstChild,{ margin:"0px", padding:"0px", border:"none" });
219     c.appendChild(a);
220     this.yearPopup.content.appendChild(tab);
221     this.container.appendChild(this.yearPopup.container);
222     this.yearPopup.container.style.left='';
223     this.yearPopup.container.style.right='5px';
224     this.yearPopup.container.style.zIndex=10;
225
226     // fix anchors so they work in IE6
227     a=this.content.getElementsByTagName('a');
228     for (i=0; i<a.length; i++) {
229       a[i].href='javascript:void(0)';
230     }
231     
232     Rico.eventBind(this.tbody,"click", Rico.eventHandle(this,'saveAndClose'));
233     this.close();
234     this.bPageLoaded=true;
235   },
236
237   _createTitleSection : function(section) {
238     var s=document.createElement("span");
239     s.className='RicoCal'+section+'Heading';
240     if (Rico.theme.calendarSubheading) Rico.addClass(s,Rico.theme.calendarSubheading);
241
242     var a=s.appendChild(document.createElement("a"));
243     a.className='Rico_leftArrow';
244     if (Rico.theme.leftArrowAnchor) Rico.addClass(a,Rico.theme.leftArrowAnchor);
245     a.appendChild(this.createNavArrow('dec'+section,'left'));
246
247     a=s.appendChild(document.createElement("a"));
248     a.style.display='inline';
249     Rico.eventBind(a,"click", Rico.eventHandle(this,'popUp'+section), false);
250     this['title'+section]=a;
251
252     a=s.appendChild(document.createElement("a"));
253     a.className='Rico_rightArrow';
254     if (Rico.theme.rightArrowAnchor) Rico.addClass(a,Rico.theme.rightArrowAnchor);
255     a.appendChild(this.createNavArrow('inc'+section,'right'));
256     return s
257   },
258   
259   selectNow : function() {
260     var today = new Date();
261     this.dateNow  = today.getDate();
262     this.monthNow = today.getMonth();
263     this.yearNow  = today.getFullYear();
264     this.monthSelected=this.monthNow;
265     this.yearSelected=this.yearNow;
266     this.constructCalendar();
267   },
268   
269 /** @private */
270   createNavArrow: function(funcname,direction) {
271     var span=document.createElement("span");
272     span.className=Rico.theme[direction+'Arrow'] || 'rico-icon Rico_'+direction+'Arrow';
273     Rico.eventBind(span,"click", Rico.eventHandle(this,funcname), false);
274     return span;
275   },
276
277 /**
278  * @returns true if yr/mo is within minDate/MaxDate
279  */
280   isValidMonth : function(yr,mo) {
281     if (yr < this.options.minDate.getFullYear()) return false;
282     if (yr == this.options.minDate.getFullYear() && mo < this.options.minDate.getMonth()) return false;
283     if (yr > this.options.maxDate.getFullYear()) return false;
284     if (yr == this.options.maxDate.getFullYear() && mo > this.options.maxDate.getMonth()) return false;
285     return true;
286   },
287
288   incMonth : function() {
289     var newMonth=this.monthSelected+1;
290     var newYear=this.yearSelected;
291     if (newMonth>11) {
292       newMonth=0;
293       newYear++;
294     }
295     if (!this.isValidMonth(newYear,newMonth)) return;
296     this.monthSelected=newMonth;
297     this.yearSelected=newYear;
298     this.constructCalendar();
299   },
300
301   decMonth : function() {
302     var newMonth=this.monthSelected-1;
303     var newYear=this.yearSelected;
304     if (newMonth<0) {
305       newMonth=11;
306       newYear--;
307     }
308     if (!this.isValidMonth(newYear,newMonth)) return;
309     this.monthSelected=newMonth;
310     this.yearSelected=newYear;
311     this.constructCalendar();
312   },
313   
314 /** @private */
315   selectMonth : function(e) {
316     var el=Rico.eventElement(e);
317     this.monthSelected=parseInt(el.name,10);
318     this.constructCalendar();
319     Rico.eventStop(e);
320   },
321
322   popUpMonth : function(e) {
323     Rico.eventStop(e);
324     if (this.monthPopup.visible()) {
325       this.popDownMonth();
326       return;
327     }
328     this.popDownYear();
329     if (Rico.isIE && Rico.ieVersion < 7) {
330       // fix position absolute inside container without hasLayout
331       this.monthPopup.openPopup(null, this.heading.offsetHeight+2);
332       this.monthPopup.container.style.left='';
333     } else {
334       this.monthPopup.openPopup(3, this.heading.offsetHeight+2);
335     }
336     return false;
337   },
338
339   popDownMonth : function() {
340     this.monthPopup.closePopup();
341   },
342
343   popDownYear : function() {
344     this.yearPopup.closePopup();
345     this.yearInput.disabled=true;  // make sure this does not get submitted
346   },
347
348 /**
349  * Prompt for year
350  */
351   popUpYear : function(e) {
352     Rico.eventStop(e);
353     if (this.yearPopup.visible()) {
354       this.popDownYear();
355       return;
356     }
357     this.popDownMonth();
358     this.yearPopup.openPopup(null, this.heading.offsetHeight+2);
359     this.yearInput.disabled=false;
360     this.yearInput.value='';   // this.yearSelected
361     this.yearInput.focus();
362     return false;
363   },
364   
365   yearKey : function(e) {
366     switch (Rico.eventKey(e)) {
367       case 27: this.popDownYear(); Rico.eventStop(e); return false;
368       case 13: this.processPopUpYear(); Rico.eventStop(e); return false;
369     }
370     return true;
371   },
372   
373   processPopUpYear : function() {
374     var newYear=this.yearInput.value;
375     newYear=parseInt(newYear,10);
376     if (isNaN(newYear) || newYear<this.options.minDate.getFullYear() || newYear>this.options.maxDate.getFullYear()) {
377       alert(Rico.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=Rico.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=0; i<dayPointer; i++ ) {
445       this.resetCell(this.tbody.rows[1].cells[i+this.colStart]);
446     }
447
448     for ( var datePointer=1,r=1; datePointer<=numDaysInMonth; datePointer++,dayPointer++ ) {
449       colnum=dayPointer % 7;
450       if (this.options.showWeekNumber && colnum==0) {
451         this.tbody.rows[r].cells[0].innerHTML=this.WeekNbr(this.yearSelected,this.monthSelected,datePointer);
452       }
453       var c=this.tbody.rows[r].cells[colnum+this.colStart];
454       c.innerHTML=datePointer;
455       c.className=this.styles[colnum];
456       if ((datePointer==this.dateNow)&&(this.monthSelected==this.monthNow)&&(this.yearSelected==this.yearNow)) {
457         Rico.addClass(c,Rico.theme.calendarToday || 'ricoCalToday');
458       }
459       if (Rico.theme.calendarDay) Rico.addClass(c,Rico.theme.calendarDay);
460       if ((datePointer==this.odateSelected) && (this.monthSelected==this.omonthSelected) && (this.yearSelected==this.oyearSelected)) {
461         Rico.addClass(c,Rico.theme.calendarSelectedDay || 'ricoSelectedDay');
462       }
463       var h=this.Holidays[this.holidayKey(this.yearSelected,this.monthSelected,datePointer)];
464       if (!h)  {
465         h=this.Holidays[this.holidayKey(0,this.monthSelected,datePointer)];
466       }
467       c.style.color=h ? h.txtColor : '';
468       c.style.backgroundColor=h ? h.bgColor : '';
469       c.title=h ? h.desc : '';
470       if (colnum==6) r++;
471     }
472     while (dayPointer<42) {
473       colnum=dayPointer % 7;
474       this.resetCell(this.tbody.rows[r].cells[colnum+this.colStart]);
475       dayPointer++;
476       if (colnum==6) r++;
477     }
478
479     this.titleMonth.innerHTML = Rico.monthAbbr(this.monthSelected);
480     this.titleYear.innerHTML = this.yearSelected;
481     if (this.todayCell) {
482       this.todayCell.innerHTML = Rico.getPhraseById("calToday",this.dateNow,Rico.monthAbbr(this.monthNow),this.yearNow,this.monthNow+1);
483     }
484   },
485   
486 /** @private */
487   resetCell: function(c) {
488     c.innerHTML="&nbsp;";
489     c.className='ricoCalEmpty';
490     c.style.color='';
491     c.style.backgroundColor='';
492     c.title='';
493   },
494   
495 /** @private */
496   saveAndClose : function(e) {
497     Rico.eventStop(e);
498     var el=Rico.eventElement(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=Rico.formatDate(d,this.dateFmt=='ISO8601' ? 'yyyy-mm-dd' : this.dateFmt);
505     if (this.returnValue) {
506       this.returnValue(dateStr);
507       this.close();
508     }
509   },
510
511   open : function(curval) {
512     if (!this.bPageLoaded) return;
513     var today = new Date();
514     this.dateNow  = today.getDate();
515     this.monthNow = today.getMonth();
516     this.yearNow  = today.getFullYear();
517     this.oyearSelected = -1;
518     if (typeof curval=='object') {
519       this.odateSelected  = curval.getDate();
520       this.omonthSelected = curval.getMonth();
521       this.oyearSelected  = curval.getFullYear();
522     } else if (this.dateFmt=='ISO8601') {
523       var d=Rico.setISO8601(curval);
524       if (d) {
525         this.odateSelected  = d.getDate();
526         this.omonthSelected = d.getMonth();
527         this.oyearSelected  = d.getFullYear();
528       }
529     } else if (this.re.exec(curval)) {
530       var aDate = [ RegExp.$1, RegExp.$3, RegExp.$5 ];
531       this.odateSelected  = parseInt(aDate[this.dateParts.dd], 10);
532       this.omonthSelected = parseInt(aDate[this.dateParts.mm], 10) - 1;
533       this.oyearSelected  = parseInt(aDate[this.dateParts.yyyy], 10);
534       if (this.oyearSelected < 100) {
535         // apply a century to 2-digit years
536         this.oyearSelected+=this.yearNow - (this.yearNow % 100);
537         var maxyr=this.options.maxDate.getFullYear();
538         while (this.oyearSelected > maxyr) this.oyearSelected-=100;
539       }
540     } else {
541       if (curval) {
542         alert('ERROR: invalid date passed to calendar ('+curval+')');
543       }
544     }
545     if (this.oyearSelected > 0) {
546       this.dateSelected=this.odateSelected;
547       this.monthSelected=this.omonthSelected;
548       this.yearSelected=this.oyearSelected;
549     } else {
550       this.dateSelected=this.dateNow;
551       this.monthSelected=this.monthNow;
552       this.yearSelected=this.yearNow;
553     }
554     this.constructCalendar();
555     this.openPopup();
556   }
557 };