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