2 * (c) 2005-2011 Richard Cowin (http://openrico.org)
3 * (c) 2005-2011 Matt Brown (http://dowdybrown.com)
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 // Inspired by code originally written by Tan Ling Wee on 2 Dec 2001
18 Rico.CalendarControl = function(id,options) {
19 this.initialize(id,options);
22 Rico.CalendarControl.prototype = {
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()
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>
39 initialize: function(id,options) {
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, {
51 minDate : this.defaultMin,
52 maxDate : this.defaultMax
54 Rico.extend(this.options, options || {});
56 * alias for closePopup
59 this.close=this.closePopup;
60 this.bPageLoaded=false;
62 this.re=/^\s*(\w+)(\W)(\w+)(\W)(\w+)/i;
63 this.setDateFmt(this.options.dateFmt);
65 Rico.onLoad(function() { self.atLoad(); })
69 setDateFmt: function(fmt) {
70 this.dateFmt=(fmt=='rico') ? Rico.dateFmt : fmt;
71 Rico.log(this.id+' date format set to '+this.dateFmt);
73 if (this.re.exec(this.dateFmt)) {
74 this.dateParts[RegExp.$1]=0;
75 this.dateParts[RegExp.$3]=1;
76 this.dateParts[RegExp.$5]=2;
81 * Call before displaying calendar to highlight special days
83 * @param m month (1-12)
84 * @param y year (0 implies a repeating holiday)
85 * @param desc description
86 * @param bgColor background color for cell displaying this day (CSS value, defaults to '#DDF')
87 * @param txtColor text color for cell displaying this day (CSS value), if not specified it is displayed with the same color as other days
89 addHoliday : function(d, m, y, desc, bgColor, txtColor) {
90 this.Holidays[this.holidayKey(y,m-1,d)]={desc:desc, txtColor:txtColor, bgColor:bgColor || '#DDF'};
94 holidayKey : function(y,m,d) {
95 return 'h'+Rico.zFill(y,4)+Rico.zFill(m,2)+Rico.zFill(d,2);
99 Rico.log('Calendar#atLoad: '+this.id);
100 var div=Rico.$(this.id);
104 this.createContainer();
105 this.container.id=this.id;
107 Rico.addClass(this.content, Rico.theme.calendar || 'ricoCalContainer');
108 this.direction=Rico.direction(this.container);
110 var r,c,i,j,dow,a,s,tab;
111 this.colStart=this.options.showWeekNumber ? 1 : 0;
112 var colcnt=7+this.colStart
113 this.maintab=document.createElement("table");
114 this.maintab.cellSpacing=2;
115 this.maintab.cellPadding=0;
116 this.maintab.border=0;
117 this.maintab.style.borderCollapse='separate';
118 this.maintab.className=Rico.theme.calendarTable || 'ricoCalTab';
120 // thead (Navigation controls)
121 this.thead=this.maintab.createTHead();
122 r=this.thead.insertRow(-1);
123 this.heading=r.insertCell(-1);
124 this.heading.colSpan=colcnt;
125 //this.heading=this.content.appendChild(document.createElement("div"));
126 this.heading.className='RicoCalHeading';
127 if (Rico.theme.calendarHeading) Rico.addClass(this.heading,Rico.theme.calendarHeading)
129 // table footer (today)
130 if (this.options.showToday) {
131 this.tfoot=this.maintab.createTFoot();
132 this.tfoot.className='ricoCalFoot';
133 r=this.tfoot.insertRow(-1);
134 this.todayCell=r.insertCell(-1);
135 this.todayCell.colSpan=colcnt;
136 this.todayCell.className=Rico.theme.calendarFooter || 'ricoCalFoot';
137 Rico.eventBind(this.todayCell,"click", Rico.eventHandle(this,'selectNow'), false);
140 this.tbody=Rico.getTBody(this.maintab);
141 this.tbody.className='ricoCalBody';
143 this.content.style.display='block';
144 if (this.position == 'absolute') {
145 this.content.style.width='auto';
146 this.maintab.style.width='auto';
148 this.container.style.position='relative';
149 this.heading.style.position='static'; // fixes issue with ie7
150 this.content.style.padding='0px';
151 this.content.style.width='15em';
152 this.maintab.style.width='100%';
156 for (i=0; i<7; i++) {
157 r=this.tbody.insertRow(-1);
158 r.className=i==0 ? 'ricoCalDayNames' : 'row'+i;
159 if (this.options.showWeekNumber) {
161 c.className='ricoCalWeekNum';
162 if (i==0) c.innerHTML=Rico.getPhraseById("calWeekHdg");
164 for (j=0; j<7; j++) {
167 dow=(j+this.options.startAt) % 7;
168 c.innerHTML=Rico.dayAbbr(dow);
169 this.styles[j]='ricoCal'+dow;
171 c.className=this.styles[j];
172 if (Rico.theme.calendarDay) Rico.addClass(c,Rico.theme.calendarDay);
177 this.content.appendChild(this.maintab);
178 new Rico.HoverSet(this.tbody.getElementsByTagName('td'),{ hoverNodes: function(e) { return e.innerHTML.match(/^\d+$/) ? [e] : []; } });
180 this.navtab=this.heading.appendChild(document.createElement("table"));
181 this.navrow=this.navtab.insertRow(-1);
182 this._createTitleSection('Month');
183 this.navrow.insertCell(-1).innerHTML=" ";
184 this._createTitleSection('Year');
185 new Rico.HoverSet(this.heading.getElementsByTagName('a'));
186 if (this.position == 'absolute') this.heading.appendChild(Rico.closeButton(Rico.eventHandle(this,'close')));
189 this.monthPopup=new Rico.Popup(document.createElement("div"),{shim:false,zIndex:10});
190 this.monthPopup.content.className='ricoCalMonthPrompt';
191 tab=document.createElement("table");
192 tab.className='ricoCalMenu';
193 if (Rico.theme.calendarPopdown) Rico.addClass(tab,Rico.theme.calendarPopdown);
197 tab.style.borderCollapse='separate';
198 tab.style.margin='0px';
199 for (i=0; i<4; i++) {
201 for (j=0; j<3; j++) {
203 a=document.createElement("a");
204 a.innerHTML=Rico.monthAbbr(i*3+j);
206 if (Rico.theme.calendarDay) Rico.addClass(a,Rico.theme.calendarDay);
208 Rico.eventBind(a,"click", Rico.eventHandle(this,'selectMonth'), false);
211 new Rico.HoverSet(tab.getElementsByTagName('a'));
212 this.monthPopup.content.appendChild(tab);
213 this.container.appendChild(this.monthPopup.container);
214 this.monthPopup.closePopup();
217 this.yearPopup=new Rico.Popup(document.createElement("div"),{shim:false,zIndex:10});
218 this.yearPopup.content.className='ricoCalYearPrompt';
219 if (Rico.theme.calendarPopdown) Rico.addClass(this.yearPopup.content,Rico.theme.calendarPopdown);
220 this.yearPrompt=document.createElement("p");
221 this.yearPrompt.innerHTML=" ";
222 var p2=document.createElement("p");
223 this.yearInput=p2.appendChild(document.createElement("input"));
224 this.yearInput.maxlength=4;
225 this.yearInput.size=4;
226 Rico.eventBind(this.yearInput,"keyup", Rico.eventHandle(this,'yearKey'), false);
227 a=Rico.floatButton('Checkmark', Rico.eventHandle(this,'processPopUpYear'));
229 a=Rico.floatButton('Cancel', Rico.eventHandle(this,'popDownYear'));
231 this.yearPopup.content.appendChild(this.yearPrompt);
232 this.yearPopup.content.appendChild(p2);
233 this.container.appendChild(this.yearPopup.container);
234 this.yearPopup.closePopup();
236 // fix anchors so they work in IE6
237 a=this.content.getElementsByTagName('a');
238 for (i=0; i<a.length; i++) {
239 a[i].href='javascript:void(0)';
242 Rico.eventBind(this.tbody,"click", Rico.eventHandle(this,'saveAndClose'));
244 this.bPageLoaded=true;
247 _createTitleSection : function(section) {
248 var arrows=['left','right'];
249 if (this.direction=='rtl') arrows.reverse();
250 var c=this.navrow.insertCell(-1);
251 var a=c.appendChild(document.createElement("a"));
252 a.className='Rico_'+arrows[0]+'Arrow';
253 a.appendChild(this._createNavArrow(arrows[0]));
254 Rico.eventBind(a,"click", Rico.eventHandle(this,'dec'+section), false);
256 c=this.navrow.insertCell(-1);
257 a=c.appendChild(document.createElement("a"));
258 Rico.eventBind(a,"click", Rico.eventHandle(this,'popUp'+section), false);
259 this['title'+section]=a;
261 c=this.navrow.insertCell(-1);
262 a=c.appendChild(document.createElement("a"));
263 a.className='Rico_'+arrows[1]+'Arrow';
264 a.appendChild(this._createNavArrow(arrows[1]));
265 Rico.eventBind(a,"click", Rico.eventHandle(this,'inc'+section), false);
268 _createNavArrow: function(direction) {
269 var span=document.createElement("span");
270 span.className=Rico.theme[direction+'Arrow'] || 'rico-icon Rico_'+direction+'Arrow';
271 span.style.display="inline-block";
275 selectNow : function() {
276 var today = new Date();
277 this.dateNow = today.getDate();
278 this.monthNow = today.getMonth();
279 this.yearNow = today.getFullYear();
280 this.monthSelected=this.monthNow;
281 this.yearSelected=this.yearNow;
282 this.constructCalendar();
286 * @returns true if yr/mo is within minDate/MaxDate
288 isValidMonth : function(yr,mo) {
289 if (yr < this.options.minDate.getFullYear()) return false;
290 if (yr == this.options.minDate.getFullYear() && mo < this.options.minDate.getMonth()) return false;
291 if (yr > this.options.maxDate.getFullYear()) return false;
292 if (yr == this.options.maxDate.getFullYear() && mo > this.options.maxDate.getMonth()) return false;
296 incMonth : function() {
297 var newMonth=this.monthSelected+1;
298 var newYear=this.yearSelected;
303 if (!this.isValidMonth(newYear,newMonth)) return;
304 this.monthSelected=newMonth;
305 this.yearSelected=newYear;
306 this.constructCalendar();
309 decMonth : function() {
310 var newMonth=this.monthSelected-1;
311 var newYear=this.yearSelected;
316 if (!this.isValidMonth(newYear,newMonth)) return;
317 this.monthSelected=newMonth;
318 this.yearSelected=newYear;
319 this.constructCalendar();
323 selectMonth : function(e) {
324 var el=Rico.eventElement(e);
325 this.monthSelected=parseInt(el.name,10);
326 this.constructCalendar();
330 // position: 0=left, 1=right
331 openYrMo : function(popup,position) {
332 if (this.direction=='rtl') position=1-position;
334 var left=position ? this.content.offsetWidth - popup.container.offsetWidth - 5 : 3;
335 popup.move(left, this.heading.offsetHeight+2);
338 popUpMonth : function(e) {
340 if (this.monthPopup.visible()) {
345 this.openYrMo(this.monthPopup,0);
349 popDownMonth : function() {
350 this.monthPopup.closePopup();
353 popDownYear : function() {
354 this.yearPopup.closePopup();
355 this.yearInput.disabled=true; // make sure this does not get submitted
361 popUpYear : function(e) {
363 if (this.yearPopup.visible()) {
368 this.yearPrompt.innerHTML=Rico.getPhraseById("calYearRange",this.options.minDate.getFullYear(),this.options.maxDate.getFullYear());
369 this.yearInput.disabled=false;
370 this.yearInput.value=''; // this.yearSelected
371 this.openYrMo(this.yearPopup,1);
373 setTimeout(function() { self.yearInput.focus(); }, 10); // ie8 has issues without this delay
377 yearKey : function(e) {
378 switch (Rico.eventKey(e)) {
379 case 27: this.popDownYear(); Rico.eventStop(e); return false;
380 case 13: this.processPopUpYear(); Rico.eventStop(e); return false;
385 processPopUpYear : function() {
386 var newYear=this.yearInput.value;
387 newYear=parseInt(newYear,10);
388 if (isNaN(newYear) || newYear<this.options.minDate.getFullYear() || newYear>this.options.maxDate.getFullYear()) {
389 alert(Rico.getPhraseById("calInvalidYear"));
391 this.yearSelected=newYear;
393 this.constructCalendar();
397 incYear : function() {
398 if (this.yearSelected>=this.options.maxDate.getFullYear()) return;
400 this.constructCalendar();
403 decYear : function() {
404 if (this.yearSelected<=this.options.minDate.getFullYear()) return;
406 this.constructCalendar();
409 // tried a number of different week number functions posted on the net
410 // this is the only one that produced consistent results when comparing week numbers for December and the following January
411 WeekNbr : function(year,month,day) {
412 var when = new Date(year,month,day);
413 var newYear = new Date(year,0,1);
414 var offset = 7 + 1 - newYear.getDay();
415 if (offset == 8) offset = 1;
416 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;
417 var weeknum = Math.floor((daynum-offset+7)/7);
420 var prevNewYear = new Date(year,0,1);
421 var prevOffset = 7 + 1 - prevNewYear.getDay();
422 weeknum = (prevOffset == 2 || prevOffset == 8) ? 53 : 52;
427 constructCalendar : function() {
428 var aNumDays = [31,0,31,30,31,30,31,31,30,31,30,31];
429 var startDate = new Date (this.yearSelected,this.monthSelected,1);
430 var endDate,numDaysInMonth,i,colnum;
432 if (typeof this.monthSelected!='number' || this.monthSelected>=12 || this.monthSelected<0) {
433 alert('ERROR in calendar: monthSelected='+this.monthSelected);
437 if (this.monthSelected==1) {
438 endDate = new Date (this.yearSelected,this.monthSelected+1,1);
439 endDate = new Date (endDate - (24*60*60*1000));
440 numDaysInMonth = endDate.getDate();
442 numDaysInMonth = aNumDays[this.monthSelected];
444 var dayPointer = startDate.getDay() - this.options.startAt;
445 if (dayPointer<0) dayPointer+=7;
449 //this.bgcolor=Rico.getStyle(this.tbody,'background-color');
450 //this.bgcolor=this.bgcolor.replace(/\"/g,'');
451 if (this.options.showWeekNumber) {
452 for (i=1; i<7; i++) {
453 this.tbody.rows[i].cells[0].innerHTML=' ';
456 for ( i=0; i<dayPointer; i++ ) {
457 this.resetCell(this.tbody.rows[1].cells[i+this.colStart]);
460 for ( var datePointer=1,r=1; datePointer<=numDaysInMonth; datePointer++,dayPointer++ ) {
461 colnum=dayPointer % 7;
462 if (this.options.showWeekNumber && colnum==0) {
463 this.tbody.rows[r].cells[0].innerHTML=this.WeekNbr(this.yearSelected,this.monthSelected,datePointer);
465 var c=this.tbody.rows[r].cells[colnum+this.colStart];
466 c.innerHTML=datePointer;
467 c.className=this.styles[colnum];
468 if ((datePointer==this.dateNow)&&(this.monthSelected==this.monthNow)&&(this.yearSelected==this.yearNow)) {
469 Rico.addClass(c,Rico.theme.calendarToday || 'ricoCalToday');
471 if (Rico.theme.calendarDay) Rico.addClass(c,Rico.theme.calendarDay);
472 if ((datePointer==this.odateSelected) && (this.monthSelected==this.omonthSelected) && (this.yearSelected==this.oyearSelected)) {
473 Rico.addClass(c,Rico.theme.calendarSelectedDay || 'ricoSelectedDay');
475 var h=this.Holidays[this.holidayKey(this.yearSelected,this.monthSelected,datePointer)];
477 h=this.Holidays[this.holidayKey(0,this.monthSelected,datePointer)];
479 c.style.color=h ? h.txtColor : '';
480 c.style.backgroundColor=h ? h.bgColor : '';
481 c.title=h ? h.desc : '';
482 c.style.visibility='visible';
485 while (dayPointer<42) {
486 colnum=dayPointer % 7;
487 this.resetCell(this.tbody.rows[r].cells[colnum+this.colStart]);
492 this.titleMonth.innerHTML = Rico.monthAbbr(this.monthSelected);
493 this.titleYear.innerHTML = this.yearSelected;
494 if (this.todayCell) {
495 this.todayCell.innerHTML = Rico.getPhraseById("calToday",this.dateNow,Rico.monthAbbr(this.monthNow),this.yearNow,this.monthNow+1);
500 resetCell: function(c) {
501 c.innerHTML=" ";
503 c.style.visibility='hidden';
507 saveAndClose : function(e) {
509 var el=Rico.eventElement(e);
510 var s=el.innerHTML.replace(/ /g,'');
511 if (s=='' || el.className=='ricoCalWeekNum') return;
512 var day=parseInt(s,10);
513 if (isNaN(day)) return;
514 var d=new Date(this.yearSelected,this.monthSelected,day);
515 var dateStr=Rico.formatDate(d,this.dateFmt=='ISO8601' ? 'yyyy-mm-dd' : this.dateFmt);
516 if (this.returnValue) {
517 this.returnValue(dateStr);
522 open : function(curval,column) {
523 if (!this.bPageLoaded) return;
525 this.setDateFmt(column.format.dateFmt);
526 this.options.minDate=column.format.min || this.defaultMin;
527 this.options.maxDate=column.format.max || this.defaultMax;
529 var today = new Date();
530 this.dateNow = today.getDate();
531 this.monthNow = today.getMonth();
532 this.yearNow = today.getFullYear();
533 this.oyearSelected = -1;
534 if (typeof curval=='object') {
535 this.odateSelected = curval.getDate();
536 this.omonthSelected = curval.getMonth();
537 this.oyearSelected = curval.getFullYear();
538 } else if (this.dateFmt=='ISO8601') {
539 var d=Rico.setISO8601(curval);
541 this.odateSelected = d.getDate();
542 this.omonthSelected = d.getMonth();
543 this.oyearSelected = d.getFullYear();
545 } else if (this.re.exec(curval)) {
546 var aDate = [ RegExp.$1, RegExp.$3, RegExp.$5 ];
547 this.odateSelected = parseInt(aDate[this.dateParts.dd], 10);
548 this.omonthSelected = parseInt(aDate[this.dateParts.mm], 10) - 1;
549 this.oyearSelected = parseInt(aDate[this.dateParts.yyyy], 10);
550 if (this.oyearSelected < 100) {
551 // apply a century to 2-digit years
552 this.oyearSelected+=this.yearNow - (this.yearNow % 100);
553 var maxyr=this.options.maxDate.getFullYear();
554 while (this.oyearSelected > maxyr) this.oyearSelected-=100;
558 alert('ERROR: invalid date passed to calendar ('+curval+')');
561 if (this.oyearSelected > 0) {
562 this.dateSelected=this.odateSelected;
563 this.monthSelected=this.omonthSelected;
564 this.yearSelected=this.oyearSelected;
566 this.dateSelected=this.dateNow;
567 this.monthSelected=this.monthNow;
568 this.yearSelected=this.yearNow;
570 this.constructCalendar();