Converted Rico3 icons to CSS sprites. Moved minsrc and baselibs directories out of...
[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     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,direction) {
267     var span=document.createElement("span");
268     span.className=Rico.theme[direction+'Arrow'] || 'rico-icon Rico_'+direction+'Arrow';
269     Rico.eventBind(span,"click", Rico.eventHandle(this,funcname), false);
270     return span;
271   },
272
273 /**
274  * @returns true if yr/mo is within minDate/MaxDate
275  */
276   isValidMonth : function(yr,mo) {
277     if (yr < this.options.minDate.getFullYear()) return false;
278     if (yr == this.options.minDate.getFullYear() && mo < this.options.minDate.getMonth()) return false;
279     if (yr > this.options.maxDate.getFullYear()) return false;
280     if (yr == this.options.maxDate.getFullYear() && mo > this.options.maxDate.getMonth()) return false;
281     return true;
282   },
283
284   incMonth : function() {
285     var newMonth=this.monthSelected+1;
286     var newYear=this.yearSelected;
287     if (newMonth>11) {
288       newMonth=0;
289       newYear++;
290     }
291     if (!this.isValidMonth(newYear,newMonth)) return;
292     this.monthSelected=newMonth;
293     this.yearSelected=newYear;
294     this.constructCalendar();
295   },
296
297   decMonth : function() {
298     var newMonth=this.monthSelected-1;
299     var newYear=this.yearSelected;
300     if (newMonth<0) {
301       newMonth=11;
302       newYear--;
303     }
304     if (!this.isValidMonth(newYear,newMonth)) return;
305     this.monthSelected=newMonth;
306     this.yearSelected=newYear;
307     this.constructCalendar();
308   },
309   
310 /** @private */
311   selectMonth : function(e) {
312     var el=Rico.eventElement(e);
313     this.monthSelected=parseInt(el.name,10);
314     this.constructCalendar();
315     Rico.eventStop(e);
316   },
317
318   popUpMonth : function(e) {
319     if (this.monthPopup.visible()) {
320       this.popDownMonth();
321       return;
322     }
323     this.popDownYear();
324     this.monthPopup.openPopup(this.titleMonth.parentNode.offsetLeft, this.thead.offsetHeight+2);
325     Rico.eventStop(e);
326     return false;
327   },
328
329   popDownMonth : function() {
330     this.monthPopup.closePopup();
331   },
332
333   popDownYear : function() {
334     this.yearPopup.closePopup();
335     this.yearInput.disabled=true;  // make sure this does not get submitted
336   },
337
338 /**
339  * Prompt for year
340  */
341   popUpYear : function(e) {
342     if (this.yearPopup.visible()) {
343       this.popDownYear();
344       return;
345     }
346     this.popDownMonth();
347     this.yearPopup.openPopup(90, this.thead.offsetHeight+2);
348     this.yearLabel.innerHTML=Rico.getPhraseById("calYearRange",this.options.minDate.getFullYear(),this.options.maxDate.getFullYear());
349     this.yearInput.disabled=false;
350     this.yearInput.value='';   // this.yearSelected
351     this.yearInput.focus();
352     Rico.eventStop(e);
353     return false;
354   },
355   
356   yearKey : function(e) {
357     switch (Rico.eventKey(e)) {
358       case 27: this.popDownYear(); Rico.eventStop(e); return false;
359       case 13: this.processPopUpYear(); Rico.eventStop(e); return false;
360     }
361     return true;
362   },
363   
364   processPopUpYear : function() {
365     var newYear=this.yearInput.value;
366     newYear=parseInt(newYear,10);
367     if (isNaN(newYear) || newYear<this.options.minDate.getFullYear() || newYear>this.options.maxDate.getFullYear()) {
368       alert(Rico.getPhraseById("calInvalidYear"));
369     } else {
370       this.yearSelected=newYear;
371       this.popDownYear();
372       this.constructCalendar();
373     }
374   },
375   
376   incYear : function() {
377     if (this.yearSelected>=this.options.maxDate.getFullYear()) return;
378     this.yearSelected++;
379     this.constructCalendar();
380   },
381
382   decYear : function() {
383     if (this.yearSelected<=this.options.minDate.getFullYear()) return;
384     this.yearSelected--;
385     this.constructCalendar();
386   },
387
388   // tried a number of different week number functions posted on the net
389   // this is the only one that produced consistent results when comparing week numbers for December and the following January
390   WeekNbr : function(year,month,day) {
391     var when = new Date(year,month,day);
392     var newYear = new Date(year,0,1);
393     var offset = 7 + 1 - newYear.getDay();
394     if (offset == 8) offset = 1;
395     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;
396     var weeknum = Math.floor((daynum-offset+7)/7);
397     if (weeknum == 0) {
398       year--;
399       var prevNewYear = new Date(year,0,1);
400       var prevOffset = 7 + 1 - prevNewYear.getDay();
401       weeknum = (prevOffset == 2 || prevOffset == 8) ? 53 : 52;
402     }
403     return weeknum;
404   },
405
406   constructCalendar : function() {
407     var aNumDays = [31,0,31,30,31,30,31,31,30,31,30,31];
408     var startDate = new Date (this.yearSelected,this.monthSelected,1);
409     var endDate,numDaysInMonth,i,colnum;
410
411     if (typeof this.monthSelected!='number' || this.monthSelected>=12 || this.monthSelected<0) {
412       alert('ERROR in calendar: monthSelected='+this.monthSelected);
413       return;
414     }
415
416     if (this.monthSelected==1) {
417       endDate = new Date (this.yearSelected,this.monthSelected+1,1);
418       endDate = new Date (endDate - (24*60*60*1000));
419       numDaysInMonth = endDate.getDate();
420     } else {
421       numDaysInMonth = aNumDays[this.monthSelected];
422     }
423     var dayPointer = startDate.getDay() - this.options.startAt;
424     if (dayPointer<0) dayPointer+=7;
425     this.popDownMonth();
426     this.popDownYear();
427
428     //this.bgcolor=Rico.getStyle(this.tbody,'background-color');
429     //this.bgcolor=this.bgcolor.replace(/\"/g,'');
430     if (this.options.showWeekNumber) {
431       for (i=1; i<7; i++) {
432         this.tbody.rows[i].cells[0].innerHTML='&nbsp;';
433       }
434     }
435     for ( i=0; i<dayPointer; i++ ) {
436       this.resetCell(this.tbody.rows[1].cells[i+this.colStart]);
437     }
438
439     for ( var datePointer=1,r=1; datePointer<=numDaysInMonth; datePointer++,dayPointer++ ) {
440       colnum=dayPointer % 7;
441       if (this.options.showWeekNumber && colnum==0) {
442         this.tbody.rows[r].cells[0].innerHTML=this.WeekNbr(this.yearSelected,this.monthSelected,datePointer);
443       }
444       var c=this.tbody.rows[r].cells[colnum+this.colStart];
445       c.innerHTML=datePointer;
446       c.className=this.styles[colnum];
447       if ((datePointer==this.dateNow)&&(this.monthSelected==this.monthNow)&&(this.yearSelected==this.yearNow)) {
448         Rico.addClass(c,Rico.theme.calendarToday || 'ricoCalToday');
449       }
450       if (Rico.theme.calendarDay) Rico.addClass(c,Rico.theme.calendarDay);
451       if ((datePointer==this.odateSelected) && (this.monthSelected==this.omonthSelected) && (this.yearSelected==this.oyearSelected)) {
452         Rico.addClass(c,Rico.theme.calendarSelectedDay || 'ricoSelectedDay');
453       }
454       var h=this.Holidays[this.holidayKey(this.yearSelected,this.monthSelected,datePointer)];
455       if (!h)  {
456         h=this.Holidays[this.holidayKey(0,this.monthSelected,datePointer)];
457       }
458       c.style.color=h ? h.txtColor : '';
459       c.style.backgroundColor=h ? h.bgColor : '';
460       c.title=h ? h.desc : '';
461       if (colnum==6) r++;
462     }
463     while (dayPointer<42) {
464       colnum=dayPointer % 7;
465       this.resetCell(this.tbody.rows[r].cells[colnum+this.colStart]);
466       dayPointer++;
467       if (colnum==6) r++;
468     }
469
470     this.titleMonth.innerHTML = Rico.monthAbbr(this.monthSelected);
471     this.titleYear.innerHTML = this.yearSelected;
472     if (this.todayCell) {
473       this.todayCell.innerHTML = Rico.getPhraseById("calToday",this.dateNow,Rico.monthAbbr(this.monthNow),this.yearNow,this.monthNow+1);
474     }
475   },
476   
477 /** @private */
478   resetCell: function(c) {
479     c.innerHTML="&nbsp;";
480     c.className='ricoCalEmpty';
481     c.style.color='';
482     c.style.backgroundColor='';
483     c.title='';
484   },
485   
486 /** @private */
487   saveAndClose : function(e) {
488     Rico.eventStop(e);
489     var el=Rico.eventElement(e);
490     var s=el.innerHTML.replace(/&nbsp;/g,'');
491     if (s=='' || el.className=='ricoCalWeekNum') return;
492     var day=parseInt(s,10);
493     if (isNaN(day)) return;
494     var d=new Date(this.yearSelected,this.monthSelected,day);
495     var dateStr=Rico.formatDate(d,this.dateFmt=='ISO8601' ? 'yyyy-mm-dd' : this.dateFmt);
496     if (this.returnValue) {
497       this.returnValue(dateStr);
498       this.close();
499     }
500   },
501
502   open : function(curval) {
503     if (!this.bPageLoaded) return;
504     var today = new Date();
505     this.dateNow  = today.getDate();
506     this.monthNow = today.getMonth();
507     this.yearNow  = today.getFullYear();
508     this.oyearSelected = -1;
509     if (typeof curval=='object') {
510       this.odateSelected  = curval.getDate();
511       this.omonthSelected = curval.getMonth();
512       this.oyearSelected  = curval.getFullYear();
513     } else if (this.dateFmt=='ISO8601') {
514       var d=Rico.setISO8601(curval);
515       if (d) {
516         this.odateSelected  = d.getDate();
517         this.omonthSelected = d.getMonth();
518         this.oyearSelected  = d.getFullYear();
519       }
520     } else if (this.re.exec(curval)) {
521       var aDate = [ RegExp.$1, RegExp.$3, RegExp.$5 ];
522       this.odateSelected  = parseInt(aDate[this.dateParts.dd], 10);
523       this.omonthSelected = parseInt(aDate[this.dateParts.mm], 10) - 1;
524       this.oyearSelected  = parseInt(aDate[this.dateParts.yyyy], 10);
525       if (this.oyearSelected < 100) {
526         // apply a century to 2-digit years
527         this.oyearSelected+=this.yearNow - (this.yearNow % 100);
528         var maxyr=this.options.maxDate.getFullYear();
529         while (this.oyearSelected > maxyr) this.oyearSelected-=100;
530       }
531     } else {
532       if (curval) {
533         alert('ERROR: invalid date passed to calendar ('+curval+')');
534       }
535     }
536     if (this.oyearSelected > 0) {
537       this.dateSelected=this.odateSelected;
538       this.monthSelected=this.omonthSelected;
539       this.yearSelected=this.oyearSelected;
540     } else {
541       this.dateSelected=this.dateNow;
542       this.monthSelected=this.monthNow;
543       this.yearSelected=this.yearNow;
544     }
545     this.constructCalendar();
546     this.openPopup();
547   }
548 };