3 // email: dowdybrown@yahoo.com
4 // Inspired by code originally written by Tan Ling Wee on 2 Dec 2001
5 // Requires prototype.js and ricoCommon.js
7 Rico.CalendarControl = Class.create(
8 /** @lends Rico.CalendarControl# */
11 * @class Implements a pop-up Gregorian calendar.
12 * Dates of adoption of the Gregorian calendar vary by country - accurate as a US & British calendar from 14 Sept 1752 to present.
13 * Mark special dates with calls to addHoliday()
16 * @param id unique identifier
17 * @param options object may contain any of the following:<dl>
18 * <dt>startAt </dt><dd> week starts with 0=sunday, 1=monday? default=0</dd>
19 * <dt>showWeekNumber</dt><dd> show week number in first column? default=0</dd>
20 * <dt>showToday </dt><dd> show "Today is..." in footer? default=1</dd>
21 * <dt>cursorColor </dt><dd> color used to highlight dates as the user moves their mouse, default=#FDD</dd>
22 * <dt>repeatInterval</dt><dd> when left/right arrow is pressed, repeat action every x milliseconds, default=100</dd>
23 * <dt>dateFmt </dt><dd> date format for return value (one of values accepted by {@link Date#formatDate}), default=ISO8601</dd>
24 * <dt>selectedDateBorder</dt><dd> border to indicate currently selected date? default=#666666</dd>
25 * <dt>minDate </dt><dd> earliest selectable date? default=today-50 years</dd>
26 * <dt>maxDate </dt><dd> last selectable date? default=today+50 years</dd>
29 initialize: function(id,options) {
32 Object.extend(this, new Rico.Popup({ignoreClicks:true}));
33 Object.extend(this.options, {
40 selectedDateBorder : "#666666",
41 minDate : new Date(today.getFullYear()-50,0,1),
42 maxDate : new Date(today.getFullYear()+50,11,31)
44 Object.extend(this.options, options || {});
46 * alias for closePopup
49 this.close=this.closePopup;
50 this.bPageLoaded=false;
53 this.weekString=RicoTranslate.getPhraseById("calWeekHdg");
54 this.re=/^\s*(\w+)(\W)(\w+)(\W)(\w+)/i;
55 this.setDateFmt(this.options.dateFmt);
59 setDateFmt: function(fmt) {
60 this.dateFmt=(fmt=='rico') ? RicoTranslate.dateFmt : fmt;
62 if (this.re.exec(this.dateFmt)) {
63 this.dateParts[RegExp.$1]=0;
64 this.dateParts[RegExp.$3]=1;
65 this.dateParts[RegExp.$5]=2;
70 * Call before displaying calendar to highlight special days
72 * @param m month (1-12)
73 * @param y year (0 implies a repeating holiday)
74 * @param desc description
75 * @param bgColor background color for cell displaying this day (CSS value, defaults to '#DDF')
76 * @param txtColor text color for cell displaying this day (CSS value), if not specified it is displayed with the same color as other days
78 addHoliday : function(d, m, y, desc, bgColor, txtColor) {
79 this.Holidays[this.holidayKey(y,m-1,d)]={desc:desc, txtColor:txtColor, bgColor:bgColor || '#DDF'};
83 holidayKey : function(y,m,d) {
84 return 'h'+y.toPaddedString(4)+m.toPaddedString(2)+d.toPaddedString(2);
88 this.container=document.createElement("div");
89 this.container.style.display="none";
90 this.container.id=this.id;
91 this.container.className='ricoCalContainer';
93 this.maintab=document.createElement("table");
94 this.maintab.cellSpacing=0;
95 this.maintab.cellPadding=0;
96 this.maintab.border=0;
97 this.maintab.className='ricoCalTab';
99 var r,c,i,j,img,dow,a,s;
100 for (i=0; i<7; i++) {
101 r=this.maintab.insertRow(-1);
103 for (c=0; c<8; c++) {
107 this.tbody=this.maintab.tBodies[0];
108 r=this.tbody.rows[0];
109 r.className='ricoCalDayNames';
110 if (this.options.showWeekNumber) {
111 r.cells[0].innerHTML=this.weekString;
112 for (i=0; i<7; i++) {
113 this.tbody.rows[i].cells[0].className='ricoCalWeekNum';
117 for (i=0; i<7; i++) {
118 dow=(i+this.options.startAt) % 7;
119 r.cells[i+1].innerHTML=RicoTranslate.dayAbbr(dow);
120 this.styles[i+1]='ricoCal'+dow;
123 // table header (navigation controls)
124 this.thead=this.maintab.createTHead();
125 r=this.thead.insertRow(-1);
128 img=this.createNavArrow('decMonth','left');
129 c.appendChild(document.createElement("a")).appendChild(img);
130 this.titleMonth=document.createElement("a");
131 c.appendChild(this.titleMonth);
132 Event.observe(this.titleMonth,"click", this.popUpMonth.bindAsEventListener(this), false);
133 img=this.createNavArrow('incMonth','right');
134 c.appendChild(document.createElement("a")).appendChild(img);
135 s=document.createElement("span");
136 s.innerHTML=' ';
137 s.style.paddingLeft='3em';
140 img=this.createNavArrow('decYear','left');
141 c.appendChild(document.createElement("a")).appendChild(img);
142 this.titleYear=document.createElement("a");
143 Event.observe(this.titleYear,"click", this.popUpYear.bindAsEventListener(this), false);
144 c.appendChild(this.titleYear);
145 img=this.createNavArrow('incYear','right');
146 c.appendChild(document.createElement("a")).appendChild(img);
148 // table footer (today)
149 if (this.options.showToday) {
150 this.tfoot=this.maintab.createTFoot();
151 r=this.tfoot.insertRow(-1);
152 this.todayCell=r.insertCell(-1);
153 this.todayCell.colSpan=8;
154 Event.observe(this.todayCell,"click", this.selectNow.bindAsEventListener(this), false);
158 this.container.appendChild(this.maintab);
160 // close icon (upper right)
161 img=document.createElement("img");
162 img.src=Rico.imgDir+'close.gif';
163 img.onclick=this.close.bind(this);
164 img.style.cursor='pointer';
165 img.style.position='absolute';
166 img.style.top='1px'; /* assumes a 1px border */
167 img.style.right='1px';
168 img.title=RicoTranslate.getPhraseById('close');
169 this.container.appendChild(img);
172 this.monthSelect=document.createElement("table");
173 this.monthSelect.className='ricoCalMenu';
174 this.monthSelect.cellPadding=2;
175 this.monthSelect.cellSpacing=0;
176 this.monthSelect.border=0;
177 for (i=0; i<4; i++) {
178 r=this.monthSelect.insertRow(-1);
179 for (j=0; j<3; j++) {
181 a=document.createElement("a");
182 a.innerHTML=RicoTranslate.monthAbbr(i*3+j);
185 Event.observe(a,"click", this.selectMonth.bindAsEventListener(this), false);
188 this.monthSelect.style.display='none';
189 this.container.appendChild(this.monthSelect);
192 this.yearPopup=document.createElement("div");
193 this.yearPopup.style.display="block";
194 this.yearPopup.className='ricoCalYearPrompt';
195 this.container.appendChild(this.yearPopup);
196 this.yearPopupSpan=this.yearPopup.appendChild(document.createElement("span"));
197 this.yearPopupYear=this.yearPopup.appendChild(document.createElement("input"));
198 this.yearPopupYear.maxlength=4;
199 this.yearPopupYear.size=4;
200 Event.observe(this.yearPopupYear,"keypress", this.yearKey.bindAsEventListener(this), false);
202 img=document.createElement("img");
203 img.src=Rico.imgDir+'checkmark.gif';
204 Event.observe(img,"click", this.processPopUpYear.bindAsEventListener(this), false);
205 this.yearPopup.appendChild(img);
207 img=document.createElement("img");
208 img.src=Rico.imgDir+'delete.gif';
209 Event.observe(img,"click", this.popDownYear.bindAsEventListener(this), false);
210 this.yearPopup.appendChild(img);
212 // fix anchors so they work in IE6
213 a=this.container.getElementsByTagName('a');
214 for (i=0; i<a.length; i++) {
215 a[i].href='javascript:void(0)';
218 Event.observe(this.tbody,"click", this.saveAndClose.bindAsEventListener(this));
219 Event.observe(this.tbody,"mouseover", this.mouseOver.bindAsEventListener(this));
220 Event.observe(this.tbody,"mouseout", this.mouseOut.bindAsEventListener(this));
221 document.getElementsByTagName("body")[0].appendChild(this.container);
222 this.setDiv(this.container);
224 this.bPageLoaded=true;
227 selectNow : function() {
228 this.monthSelected=this.monthNow;
229 this.yearSelected=this.yearNow;
230 this.constructCalendar();
234 createNavArrow: function(funcname,gifname) {
235 var img=document.createElement("img");
236 img.src=Rico.imgDir+gifname+'.gif';
238 Event.observe(img,"click", this[funcname].bindAsEventListener(this), false);
239 Event.observe(img,"mousedown", this.mouseDown.bindAsEventListener(this), false);
240 Event.observe(img,"mouseup", this.mouseUp.bindAsEventListener(this), false);
241 Event.observe(img,"mouseout", this.mouseUp.bindAsEventListener(this), false);
246 mouseOver: function(e) {
247 var el=Event.element(e);
248 if (this.lastHighlight==el) return;
250 var s=el.innerHTML.replace(/ /g,'');
251 if (s=='' || el.className=='ricoCalWeekNum') return;
252 var day=parseInt(s,10);
253 if (isNaN(day)) return;
254 this.lastHighlight=el;
255 this.tmpColor=el.style.backgroundColor;
256 el.style.backgroundColor=this.options.cursorColor;
260 unhighlight: function() {
261 if (!this.lastHighlight) return;
262 this.lastHighlight.style.backgroundColor=this.tmpColor;
263 this.lastHighlight=null;
267 mouseOut: function(e) {
268 var el=Event.element(e);
269 if (el==this.lastHighlight) this.unhighlight();
273 mouseDown: function(e) {
274 var el=Event.element(e);
275 this.repeatFunc=this[el.name].bind(this);
276 this.timeoutID=setTimeout(this.repeatStart.bind(this),500);
280 mouseUp: function(e) {
281 clearTimeout(this.timeoutID);
282 clearInterval(this.intervalID);
286 repeatStart : function() {
287 clearInterval(this.intervalID);
288 this.intervalID=setInterval(this.repeatFunc,this.options.repeatInterval);
292 * @returns true if yr/mo is within minDate/MaxDate
294 isValidMonth : function(yr,mo) {
295 if (yr < this.options.minDate.getFullYear()) return false;
296 if (yr == this.options.minDate.getFullYear() && mo < this.options.minDate.getMonth()) return false;
297 if (yr > this.options.maxDate.getFullYear()) return false;
298 if (yr == this.options.maxDate.getFullYear() && mo > this.options.maxDate.getMonth()) return false;
302 incMonth : function() {
303 var newMonth=this.monthSelected+1;
304 var newYear=this.yearSelected;
309 if (!this.isValidMonth(newYear,newMonth)) return;
310 this.monthSelected=newMonth;
311 this.yearSelected=newYear;
312 this.constructCalendar();
315 decMonth : function() {
316 var newMonth=this.monthSelected-1;
317 var newYear=this.yearSelected;
322 if (!this.isValidMonth(newYear,newMonth)) return;
323 this.monthSelected=newMonth;
324 this.yearSelected=newYear;
325 this.constructCalendar();
329 selectMonth : function(e) {
330 var el=Event.element(e);
331 this.monthSelected=parseInt(el.name,10);
332 this.constructCalendar();
336 popUpMonth : function() {
337 Element.toggle(this.monthSelect);
338 this.monthSelect.style.top=(this.thead.offsetHeight+2)+'px';
339 this.monthSelect.style.left=this.titleMonth.offsetLeft+'px';
342 popDownMonth : function() {
343 Element.hide(this.monthSelect);
346 popDownYear : function() {
347 Element.hide(this.yearPopup);
348 this.yearPopup.disabled=true; // make sure this does not get submitted
354 popUpYear : function() {
355 Element.toggle(this.yearPopup);
356 if (!Element.visible(this.yearPopup)) return;
357 this.yearPopup.disabled=false;
358 this.yearPopup.style.left='120px';
359 this.yearPopup.style.top=(this.thead.offsetHeight+2)+'px';
360 this.yearPopupSpan.innerHTML=' '+RicoTranslate.getPhraseById("calYearRange",this.options.minDate.getFullYear(),this.options.maxDate.getFullYear())+'<br>';
361 this.yearPopupYear.value=''; // this.yearSelected
362 this.yearPopupYear.focus();
365 yearKey : function(e) {
366 switch (RicoUtil.eventKey(e)) {
367 case 27: this.popDownYear(); Event.stop(e); return false;
368 case 13: this.processPopUpYear(); Event.stop(e); return false;
373 processPopUpYear : function() {
374 var newYear=this.yearPopupYear.value;
375 newYear=parseInt(newYear,10);
376 if (isNaN(newYear) || newYear<this.options.minDate.getFullYear() || newYear>this.options.maxDate.getFullYear()) {
377 alert(RicoTranslate.getPhraseById("calInvalidYear"));
379 this.yearSelected=newYear;
381 this.constructCalendar();
385 incYear : function() {
386 if (this.yearSelected>=this.options.maxDate.getFullYear()) return;
388 this.constructCalendar();
391 decYear : function() {
392 if (this.yearSelected<=this.options.minDate.getFullYear()) return;
394 this.constructCalendar();
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);
408 var prevNewYear = new Date(year,0,1);
409 var prevOffset = 7 + 1 - prevNewYear.getDay();
410 weeknum = (prevOffset == 2 || prevOffset == 8) ? 53 : 52;
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;
420 if (typeof this.monthSelected!='number' || this.monthSelected>=12 || this.monthSelected<0) {
421 alert('ERROR in calendar: monthSelected='+this.monthSelected);
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();
430 numDaysInMonth = aNumDays[this.monthSelected];
432 var dayPointer = startDate.getDay() - this.options.startAt;
433 if (dayPointer<0) dayPointer+=7;
437 this.bgcolor=Element.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=' ';
444 for ( i=1; i<=dayPointer; i++ ) {
445 this.resetCell(this.tbody.rows[1].cells[i]);
448 for ( var datePointer=1,r=1; datePointer<=numDaysInMonth; datePointer++,dayPointer++ ) {
449 colnum=dayPointer % 7 + 1;
450 if (this.options.showWeekNumber==1 && colnum==1) {
451 this.tbody.rows[r].cells[0].innerHTML=this.WeekNbr(this.yearSelected,this.monthSelected,datePointer);
453 var dateClass=this.styles[colnum];
454 if ((datePointer==this.dateNow)&&(this.monthSelected==this.monthNow)&&(this.yearSelected==this.yearNow)) {
455 dateClass='ricoCalToday';
457 var c=this.tbody.rows[r].cells[colnum];
458 c.innerHTML=" " + datePointer + " ";
459 c.className=dateClass;
460 var bordercolor=(datePointer==this.odateSelected) && (this.monthSelected==this.omonthSelected) && (this.yearSelected==this.oyearSelected) ? this.options.selectedDateBorder : this.bgcolor;
461 c.style.border='1px solid '+bordercolor;
462 var h=this.Holidays[this.holidayKey(this.yearSelected,this.monthSelected,datePointer)];
464 h=this.Holidays[this.holidayKey(0,this.monthSelected,datePointer)];
466 c.style.color=h ? h.txtColor : '';
467 c.style.backgroundColor=h ? h.bgColor : '';
468 c.title=h ? h.desc : '';
471 while (dayPointer<42) {
472 colnum=dayPointer % 7 + 1;
473 this.resetCell(this.tbody.rows[r].cells[colnum]);
478 this.titleMonth.innerHTML = RicoTranslate.monthAbbr(this.monthSelected);
479 this.titleYear.innerHTML = this.yearSelected;
480 if (this.todayCell) {
481 this.todayCell.innerHTML = RicoTranslate.getPhraseById("calToday",this.dateNow,RicoTranslate.monthAbbr(this.monthNow),this.yearNow,this.monthNow+1);
486 resetCell: function(c) {
487 c.innerHTML=" ";
488 c.className='ricoCalEmpty';
489 c.style.border='1px solid '+this.bgcolor;
491 c.style.backgroundColor='';
496 saveAndClose : function(e) {
498 var el=Event.element(e);
499 var s=el.innerHTML.replace(/ /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=d.formatDate(this.dateFmt=='ISO8601' ? 'yyyy-mm-dd' : this.dateFmt);
505 if (this.returnValue) this.returnValue(dateStr);
509 open : function(curval) {
510 if (!this.bPageLoaded) return;
511 var today = new Date();
512 this.dateNow = today.getDate();
513 this.monthNow = today.getMonth();
514 this.yearNow = today.getFullYear();
515 if (typeof curval=='object') {
516 this.dateSelected = curval.getDate();
517 this.monthSelected = curval.getMonth();
518 this.yearSelected = curval.getFullYear();
519 } else if (this.dateFmt=='ISO8601') {
521 d.setISO8601(curval);
522 this.dateSelected = d.getDate();
523 this.monthSelected = d.getMonth();
524 this.yearSelected = d.getFullYear();
525 } else if (this.re.exec(curval)) {
526 var aDate = [ RegExp.$1, RegExp.$3, RegExp.$5 ];
527 this.dateSelected = parseInt(aDate[this.dateParts.dd], 10);
528 this.monthSelected = parseInt(aDate[this.dateParts.mm], 10) - 1;
529 this.yearSelected = parseInt(aDate[this.dateParts.yyyy], 10);
530 if (this.yearSelected < 100) {
531 // apply a century to 2-digit years
532 this.yearSelected+=this.yearNow - (this.yearNow % 100);
533 var maxyr=this.options.maxDate.getFullYear();
534 while (this.yearSelected > maxyr) this.yearSelected-=100;
538 alert('ERROR: invalid date passed to calendar ('+curval+')');
540 this.dateSelected = this.dateNow;
541 this.monthSelected = this.monthNow;
542 this.yearSelected = this.yearNow;
544 this.odateSelected=this.dateSelected;
545 this.omonthSelected=this.monthSelected;
546 this.oyearSelected=this.yearSelected;
547 this.constructCalendar();
552 Rico.includeLoaded('ricoCalendar.js');