2 * (c) 2005-2009 Richard Cowin (http://openrico.org)
3 * (c) 2005-2009 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
17 // Requires prototype.js and ricoCommon.js
19 Rico.CalendarControl = function(id,options) {
20 this.initialize(id,options);
23 Rico.CalendarControl.prototype = {
25 * @class Implements a pop-up Gregorian calendar.
26 * Dates of adoption of the Gregorian calendar vary by country - accurate as a US & British calendar from 14 Sept 1752 to present.
27 * Mark special dates with calls to addHoliday()
30 * @param id unique identifier
31 * @param options object may contain any of the following:<dl>
32 * <dt>startAt </dt><dd> week starts with 0=sunday, 1=monday? default=0</dd>
33 * <dt>showWeekNumber</dt><dd> show week number in first column? default=0</dd>
34 * <dt>showToday </dt><dd> show "Today is..." in footer? default=1</dd>
35 * <dt>repeatInterval</dt><dd> when left/right arrow is pressed, repeat action every x milliseconds, default=100</dd>
36 * <dt>dateFmt </dt><dd> date format for return value (one of values accepted by {@link Date#formatDate}), default=ISO8601</dd>
37 * <dt>minDate </dt><dd> earliest selectable date? default=today-50 years</dd>
38 * <dt>maxDate </dt><dd> last selectable date? default=today+50 years</dd>
41 initialize: function(id,options) {
44 Rico.extend(this, new Rico.Popup());
45 Rico.extend(this.options, {
52 minDate : new Date(today.getFullYear()-50,0,1),
53 maxDate : new Date(today.getFullYear()+50,11,31)
55 Rico.extend(this.options, options || {});
57 * alias for closePopup
60 this.close=this.closePopup;
61 this.bPageLoaded=false;
64 this.weekString=Rico.getPhraseById("calWeekHdg");
65 this.re=/^\s*(\w+)(\W)(\w+)(\W)(\w+)/i;
66 this.setDateFmt(this.options.dateFmt);
70 setDateFmt: function(fmt) {
71 this.dateFmt=(fmt=='rico') ? Rico.dateFmt : fmt;
72 Rico.log(this.id+' date format set to '+this.dateFmt);
74 if (this.re.exec(this.dateFmt)) {
75 this.dateParts[RegExp.$1]=0;
76 this.dateParts[RegExp.$3]=1;
77 this.dateParts[RegExp.$5]=2;
82 * Call before displaying calendar to highlight special days
84 * @param m month (1-12)
85 * @param y year (0 implies a repeating holiday)
86 * @param desc description
87 * @param bgColor background color for cell displaying this day (CSS value, defaults to '#DDF')
88 * @param txtColor text color for cell displaying this day (CSS value), if not specified it is displayed with the same color as other days
90 addHoliday : function(d, m, y, desc, bgColor, txtColor) {
91 this.Holidays[this.holidayKey(y,m-1,d)]={desc:desc, txtColor:txtColor, bgColor:bgColor || '#DDF'};
95 holidayKey : function(y,m,d) {
96 return 'h'+Rico.zFill(y,4)+Rico.zFill(m,2)+Rico.zFill(d,2);
100 Rico.log('Calendar#atLoad: '+this.id);
101 this.createContainer();
102 this.container.id=this.id;
103 //this.container.style.width="auto";
104 this.content.className=Rico.theme.calendar || 'ricoCalContainer';
106 this.maintab=document.createElement("table");
107 this.maintab.cellSpacing=2;
108 this.maintab.cellPadding=0;
109 this.maintab.border=0;
110 this.maintab.style.borderCollapse='separate';
111 this.maintab.className='ricoCalTab';
112 if (Rico.theme.calendarTable) Rico.addClass(this.maintab,Rico.theme.calendarTable)
113 this.tbody=Rico.getTBody(this.maintab);
115 var r,c,d,i,j,img,dow,a,s,tab;
116 this.colStart=this.options.showWeekNumber ? 1 : 0;
117 for (i=0; i<7; i++) {
118 r=this.tbody.insertRow(-1);
120 for (c=0; c<7+this.colStart; c++) {
124 r=this.tbody.rows[0];
125 r.className='ricoCalDayNames';
126 if (this.options.showWeekNumber) {
127 r.cells[0].innerHTML=this.weekString;
128 for (i=0; i<7; i++) {
129 this.tbody.rows[i].cells[0].className='ricoCalWeekNum';
133 for (i=0; i<7; i++) {
134 dow=(i+this.options.startAt) % 7;
135 r.cells[i+this.colStart].innerHTML=Rico.dayAbbr(dow);
136 this.styles[i]='ricoCal'+dow;
139 // table header (navigation controls)
140 this.thead=this.maintab.createTHead();
141 r=this.thead.insertRow(-1);
142 c=r.appendChild(document.createElement("th"));
143 c.colSpan=7+this.colStart;
144 d=c.appendChild(document.createElement("div"));
145 //d.style.padding='3px';
146 d.className=Rico.theme.calendarHeading || 'RicoCalHeading';
148 d.appendChild(this._createTitleSection('Month'));
149 d.appendChild(this._createTitleSection('Year'));
150 new Rico.HoverSet(d.getElementsByTagName('a'));
151 new Rico.HoverSet(this.tbody.getElementsByTagName('td'),{ hoverNodes: function(e) { return e.innerHTML.match(/^\d+$/) ? [e] : []; } });
152 d.appendChild(Rico.closeButton(Rico.eventHandle(this,'close')));
154 // table footer (today)
155 if (this.options.showToday) {
156 this.tfoot=this.maintab.createTFoot();
157 r=this.tfoot.insertRow(-1);
158 this.todayCell=r.insertCell(-1);
159 this.todayCell.colSpan=7+this.colStart;
160 if (Rico.theme.calendarFooter) Rico.addClass(this.todayCell,Rico.theme.calendarFooter);
161 Rico.eventBind(this.todayCell,"click", Rico.eventHandle(this,'selectNow'), false);
163 this.content.appendChild(this.maintab);
164 var ie6=Rico.isIE && Rico.ieVersion < 7;
165 var selectOptions={shadow: !ie6};
168 this.monthPopup=new Rico.Popup(document.createElement("div"),selectOptions);
169 this.monthPopup.closePopup();
170 tab=document.createElement("table");
171 tab.className='ricoCalMenu';
172 if (Rico.theme.calendarPopdown) Rico.addClass(tab,Rico.theme.calendarPopdown);
176 tab.style.borderCollapse='separate';
177 tab.style.margin='0px';
178 for (i=0; i<4; i++) {
180 for (j=0; j<3; j++) {
182 a=document.createElement("a");
183 a.innerHTML=Rico.monthAbbr(i*3+j);
185 if (Rico.theme.calendarDay) Rico.addClass(a,Rico.theme.calendarDay);
187 Rico.eventBind(a,"click", Rico.eventHandle(this,'selectMonth'), false);
190 new Rico.HoverSet(tab.getElementsByTagName('a'));
191 this.monthPopup.content.appendChild(tab);
192 this.content.appendChild(this.monthPopup.container);
195 this.yearPopup=new Rico.Popup(document.createElement("div"),selectOptions);
196 this.yearPopup.closePopup();
197 this.yearPopup.content.className='ricoCalYearPrompt';
198 if (Rico.theme.calendarPopdown) Rico.addClass(this.yearPopup.content,Rico.theme.calendarPopdown);
199 var tab=document.createElement("table");
203 tab.style.borderCollapse='separate';
204 tab.style.margin='0px';
206 this.yearLabel=r.insertCell(-1);
207 this.yearLabel.colSpan=3;
210 this.yearInput=c.appendChild(document.createElement("input"));
211 this.yearInput.maxlength=4;
212 this.yearInput.size=4;
213 Rico.eventBind(this.yearInput,"keypress", Rico.eventHandle(this,'yearKey'), false);
215 c.appendChild(Rico.floatButton('Checkmark', Rico.eventHandle(this,'processPopUpYear')));
217 c.appendChild(Rico.floatButton('Cancel', Rico.eventHandle(this,'popDownYear')));
218 this.yearPopup.content.appendChild(tab);
219 this.content.appendChild(this.yearPopup.container);
221 //this.yearLabel.className='ricoCalYearPromptText';
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)';
229 Rico.eventBind(this.tbody,"click", Rico.eventHandle(this,'saveAndClose'));
231 this.bPageLoaded=true;
234 _createTitleSection : function(section) {
235 var s=document.createElement("span");
236 s.className='RicoCal'+section+'Heading';
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'));
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;
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'));
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();
266 createNavArrow: function(funcname,gifname) {
268 img=document.createElement("span");
269 img.className=Rico.theme[gifname+'Arrow'] || 'Rico_'+gifname+'Arrow';
270 Rico.eventBind(img,"click", Rico.eventHandle(this,funcname), false);
271 Rico.eventBind(img,"mousedown", Rico.eventHandle(this,'mouseDown'), false);
272 Rico.eventBind(img,"mouseup", Rico.eventHandle(this,'mouseUp'), false);
273 Rico.eventBind(img,"mouseout", Rico.eventHandle(this,'mouseUp'), false);
278 mouseDown: function(e) {
279 var el=Rico.eventElement(e);
280 this.repeatFunc=Rico.bind(this,el.name);
281 this.timeoutID=Rico.runLater(500,this,'repeatStart');
285 mouseUp: function(e) {
286 clearTimeout(this.timeoutID);
287 clearInterval(this.intervalID);
291 repeatStart : function() {
292 clearInterval(this.intervalID);
293 this.intervalID=setInterval(this.repeatFunc,this.options.repeatInterval);
297 * @returns true if yr/mo is within minDate/MaxDate
299 isValidMonth : function(yr,mo) {
300 if (yr < this.options.minDate.getFullYear()) return false;
301 if (yr == this.options.minDate.getFullYear() && mo < this.options.minDate.getMonth()) return false;
302 if (yr > this.options.maxDate.getFullYear()) return false;
303 if (yr == this.options.maxDate.getFullYear() && mo > this.options.maxDate.getMonth()) return false;
307 incMonth : function() {
308 var newMonth=this.monthSelected+1;
309 var newYear=this.yearSelected;
314 if (!this.isValidMonth(newYear,newMonth)) return;
315 this.monthSelected=newMonth;
316 this.yearSelected=newYear;
317 this.constructCalendar();
320 decMonth : function() {
321 var newMonth=this.monthSelected-1;
322 var newYear=this.yearSelected;
327 if (!this.isValidMonth(newYear,newMonth)) return;
328 this.monthSelected=newMonth;
329 this.yearSelected=newYear;
330 this.constructCalendar();
334 selectMonth : function(e) {
335 var el=Rico.eventElement(e);
336 this.monthSelected=parseInt(el.name,10);
337 this.constructCalendar();
341 popUpMonth : function() {
342 if (this.monthPopup.visible()) {
347 this.monthPopup.openPopup(this.titleMonth.parentNode.offsetLeft, this.thead.offsetHeight+2);
350 popDownMonth : function() {
351 this.monthPopup.closePopup();
354 popDownYear : function() {
355 this.yearPopup.closePopup();
356 this.yearInput.disabled=true; // make sure this does not get submitted
362 popUpYear : function() {
363 if (this.yearPopup.visible()) {
368 this.yearPopup.openPopup(90, this.thead.offsetHeight+2);
369 this.yearLabel.innerHTML=Rico.getPhraseById("calYearRange",this.options.minDate.getFullYear(),this.options.maxDate.getFullYear());
370 this.yearInput.disabled=false;
371 this.yearInput.value=''; // this.yearSelected
372 this.yearInput.focus();
375 yearKey : function(e) {
376 switch (Rico.eventKey(e)) {
377 case 27: this.popDownYear(); Rico.eventStop(e); return false;
378 case 13: this.processPopUpYear(); Rico.eventStop(e); return false;
383 processPopUpYear : function() {
384 var newYear=this.yearInput.value;
385 newYear=parseInt(newYear,10);
386 if (isNaN(newYear) || newYear<this.options.minDate.getFullYear() || newYear>this.options.maxDate.getFullYear()) {
387 alert(Rico.getPhraseById("calInvalidYear"));
389 this.yearSelected=newYear;
391 this.constructCalendar();
395 incYear : function() {
396 if (this.yearSelected>=this.options.maxDate.getFullYear()) return;
398 this.constructCalendar();
401 decYear : function() {
402 if (this.yearSelected<=this.options.minDate.getFullYear()) return;
404 this.constructCalendar();
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);
418 var prevNewYear = new Date(year,0,1);
419 var prevOffset = 7 + 1 - prevNewYear.getDay();
420 weeknum = (prevOffset == 2 || prevOffset == 8) ? 53 : 52;
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;
430 if (typeof this.monthSelected!='number' || this.monthSelected>=12 || this.monthSelected<0) {
431 alert('ERROR in calendar: monthSelected='+this.monthSelected);
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();
440 numDaysInMonth = aNumDays[this.monthSelected];
442 var dayPointer = startDate.getDay() - this.options.startAt;
443 if (dayPointer<0) dayPointer+=7;
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=' ';
454 for ( i=0; i<dayPointer; i++ ) {
455 this.resetCell(this.tbody.rows[1].cells[i+this.colStart]);
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);
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');
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');
473 var h=this.Holidays[this.holidayKey(this.yearSelected,this.monthSelected,datePointer)];
475 h=this.Holidays[this.holidayKey(0,this.monthSelected,datePointer)];
477 c.style.color=h ? h.txtColor : '';
478 c.style.backgroundColor=h ? h.bgColor : '';
479 c.title=h ? h.desc : '';
482 while (dayPointer<42) {
483 colnum=dayPointer % 7;
484 this.resetCell(this.tbody.rows[r].cells[colnum+this.colStart]);
489 this.titleMonth.innerHTML = Rico.monthAbbr(this.monthSelected);
490 this.titleYear.innerHTML = this.yearSelected;
491 if (this.todayCell) {
492 this.todayCell.innerHTML = Rico.getPhraseById("calToday",this.dateNow,Rico.monthAbbr(this.monthNow),this.yearNow,this.monthNow+1);
497 resetCell: function(c) {
498 c.innerHTML=" ";
499 c.className='ricoCalEmpty';
501 c.style.backgroundColor='';
506 saveAndClose : function(e) {
508 var el=Rico.eventElement(e);
509 var s=el.innerHTML.replace(/ /g,'');
510 if (s=='' || el.className=='ricoCalWeekNum') return;
511 var day=parseInt(s,10);
512 if (isNaN(day)) return;
513 var d=new Date(this.yearSelected,this.monthSelected,day);
514 var dateStr=Rico.formatDate(d,this.dateFmt=='ISO8601' ? 'yyyy-mm-dd' : this.dateFmt);
515 if (this.returnValue) {
516 this.returnValue(dateStr);
521 open : function(curval) {
522 if (!this.bPageLoaded) return;
523 var today = new Date();
524 this.dateNow = today.getDate();
525 this.monthNow = today.getMonth();
526 this.yearNow = today.getFullYear();
527 this.oyearSelected = -1;
528 if (typeof curval=='object') {
529 this.odateSelected = curval.getDate();
530 this.omonthSelected = curval.getMonth();
531 this.oyearSelected = curval.getFullYear();
532 } else if (this.dateFmt=='ISO8601') {
533 var d=Rico.setISO8601(curval);
535 this.odateSelected = d.getDate();
536 this.omonthSelected = d.getMonth();
537 this.oyearSelected = d.getFullYear();
539 } else if (this.re.exec(curval)) {
540 var aDate = [ RegExp.$1, RegExp.$3, RegExp.$5 ];
541 this.odateSelected = parseInt(aDate[this.dateParts.dd], 10);
542 this.omonthSelected = parseInt(aDate[this.dateParts.mm], 10) - 1;
543 this.oyearSelected = parseInt(aDate[this.dateParts.yyyy], 10);
544 if (this.oyearSelected < 100) {
545 // apply a century to 2-digit years
546 this.oyearSelected+=this.yearNow - (this.yearNow % 100);
547 var maxyr=this.options.maxDate.getFullYear();
548 while (this.oyearSelected > maxyr) this.oyearSelected-=100;
552 alert('ERROR: invalid date passed to calendar ('+curval+')');
555 if (this.oyearSelected > 0) {
556 this.dateSelected=this.odateSelected;
557 this.monthSelected=this.omonthSelected;
558 this.yearSelected=this.oyearSelected;
560 this.dateSelected=this.dateNow;
561 this.monthSelected=this.monthNow;
562 this.yearSelected=this.yearNow;
564 this.constructCalendar();
569 Rico.includeLoaded('ricoCalendar.js');