1 /** 2 * @fileoverview Calendar Widget 3 * @author Garrett Smith 4 * 5 * @example <pre> 6 * var calendar = APE.widget.Calendar.getById( "cal" ); 7 * calendar.create(); 8 * </pre> 9 */ 10 11 APE.namespace("APE.widget"); 12 13 /** 14 * @constructor 15 * @param {String} id the id of the input element. 16 */ 17 APE.widget.Calendar = function( id ) { 18 this.id = (typeof id == "string" ? id : id.id); 19 var input = document.getElementById(this.id); 20 this.displayDate = new Date(input.value); 21 if(isNaN(this.displayDate.valueOf())) 22 this.displayDate = new Date; 23 this.initEvents(); 24 }; 25 26 APE.widget.Calendar.getByNode = APE.getByNode; 27 APE.widget.Calendar.getById = APE.getById; 28 29 /** 30 * @param {Event} e event parameter for focus event. 31 * @description called when the input receives focus or click events. 32 */ 33 APE.widget.Calendar.focusHandler = function(e) { 34 APE.widget.Calendar.getById(this.id)._show(e); 35 }; 36 37 APE.widget.Calendar.documentMouseDownHandler = function(e){ 38 var dom = APE.dom, 39 target = dom.Event.getTarget(e), 40 Calendar = APE.widget.Calendar, 41 activeCalendar = Calendar.activeCalendar, 42 calendarEl; 43 if(activeCalendar) { 44 calendarEl = document.getElementById(activeCalendar.calendarId); 45 46 // If the innerHTML was wiped or something. 47 // This is a document event handler. Anything can happen. 48 if(!calendarEl) { 49 Calendar.activeCalendar = null; 50 return; 51 } 52 if(dom.contains(calendarEl, target) || target === calendarEl) return; 53 54 activeCalendar._hasFocus = false; 55 activeCalendar._hide(e); 56 } 57 }; 58 59 /** 60 * @param {Event} e event parameter for blur event. 61 * @description called when the document receives a mousedown event. 62 */ 63 APE.widget.Calendar.blurHandler = function(e) { 64 var calendar = APE.widget.Calendar.getByNode(this); 65 66 // A delay window here to cancel focus from the mousedown handler. 67 // ideally, we could check e.explicitOriginalTarget, 68 // and that actually works in Mozilla! But it doesn't 69 // work in any other browsers. Even IE's toElement 70 // doesn't contain the non-focusable toElement for blur. 71 calendar.hideTimer = setTimeout(function blurHandler(){calendar._hide(e||event);}, 10); 72 }; 73 74 /** 75 * @param {Event} e event parameter for click event. 76 * @description called when calendar is clicked. 77 */ 78 APE.widget.Calendar.mousedownHandler = function(e) { 79 e = e||event; 80 var dom = APE.dom, 81 target = dom.Event.getTarget(e), 82 calendarDiv, 83 calendarObject, 84 calId; 85 86 if(target.className == "ape-calendar") { 87 calendarDiv = target; 88 } 89 else { 90 calendarDiv = dom.findAncestorWithClass(target, "ape-calendar"); 91 } 92 calendarObject = APE.widget.Calendar.getById(calendarDiv.id.replace(/-calendar$/,"")); 93 calId = calendarObject.id; 94 95 // Canceled the hide action that will be caused by blur(). 96 calendarObject._hasFocus = true; 97 clearTimeout(calendarObject.hideTimer); 98 99 selectedId = calId + "-selected-day"; 100 101 if(target.tagName.toLowerCase() == "b") { 102 if(target.id === selectedId) return; 103 104 var selectedIndex = parseInt(target.firstChild.data); 105 106 if(!selectedIndex) return; 107 108 var selected = document.getElementById(selectedId); 109 if(selected) { 110 dom.removeClass(selected, "ape-calendar-selected-day"); 111 selected.id = ""; 112 } 113 target.id = selectedId; 114 dom.addClass(target, "ape-calendar-selected-day"); 115 calendarObject.setDateOfMonth(selectedIndex); 116 117 calendarObject.onselect(); 118 119 if(calendarObject.hideOnSelect) { 120 setTimeout(function(){ 121 calendarObject._hide(e); 122 calendarObject._hasFocus = false; 123 calendarObject = null; 124 }, 115); 125 } 126 } 127 else { 128 var newDate = new Date(calendarObject.displayDate); 129 if(target.id === calId + "-next-year") { 130 newDate.setYear(newDate.getFullYear() + 1); 131 calendarObject.setDate(newDate); 132 } 133 else if(target.id === calId + "-prev-year") { 134 newDate.setYear(newDate.getFullYear() - 1); 135 calendarObject.setDate(newDate); 136 } 137 else if(target.id === calId + "-next-month") { 138 newDate.setMonth(newDate.getMonth() + 1); 139 calendarObject.setDate(newDate); 140 } 141 else if(target.id === calId + "-prev-month") { 142 newDate.setMonth(newDate.getMonth() - 1); 143 calendarObject.setDate(newDate); 144 } 145 } 146 }; 147 148 APE.widget.Calendar.prototype = { 149 150 /** 151 * Days in months. JavaScript Date object does not provide this. 152 * @type {[number]} 153 */ 154 days : [31,28,31,30,31,30,31,31,30,31,30,31], 155 156 /** 157 * @type {boolean} 158 * @description set to <code>true</code> to hide the calendar when a 159 * date is selected. 160 */ 161 hideOnSelect : true, 162 163 /** 164 * This can generally be ignored. 165 * Initializes events for calendar. If Calendar's html is regenerated (via innerHTML) 166 * then call this method when calendar HTML is generated. 167 */ 168 initEvents : function() { 169 170 var d = document, input = d.getElementById(this.id), 171 Calendar = APE.widget.Calendar, 172 EventPublisher = APE.EventPublisher; 173 174 EventPublisher.add(input, "onfocus", Calendar.focusHandler); 175 EventPublisher.add(input, "onblur", Calendar.blurHandler); 176 177 // IE needs this because if calendar is 178 // not shown at pg load time, and input has focus, 179 // onfocus won't fire when user clicks input. 180 EventPublisher.add(input, "onclick", Calendar.focusHandler); 181 EventPublisher.add(d, "onmousedown", Calendar.documentMouseDownHandler); 182 }, 183 184 /** 185 * @type {String} 186 * @description the <code>id</code> of the Calendar instance (also the 187 * same as the html <code>input</code> element. 188 */ 189 id : "", 190 calendarId : "", 191 192 hiddenDayClass : 'ape-calendar-empty-day', 193 calendarClass : 'ape-calendar', 194 195 /** @internal */ 196 _isHidden : undefined, 197 198 /** 199 * Shows the calendar. The first time this is called, 200 * the selectedDate is initialized. 201 * @param {Event} e the DOM Event the triggered the action. 202 * @fires onshow 203 * @private 204 */ 205 _show : function(e) { 206 if(!this.calendarId) this.create(); 207 var calendar = document.getElementById(this.calendarId), 208 calStyle = calendar.style, 209 activeCal = this.constructor.activeCalendar; 210 211 this._isHidden = false; 212 213 if(activeCal) { 214 if(activeCal === this) return; 215 else 216 activeCal._hide(); 217 } 218 219 this.position(calStyle); 220 this.constructor.activeCalendar = this; 221 this.show(calStyle); 222 this.onshow(e); 223 224 this.setDate(this.displayDate); 225 }, 226 227 /** Positions the calendar just below the 228 * input using APE.dom.getOffsetCoords. 229 * @param {CSSStyleDeclaration} calStyle the caledar element's style. 230 */ 231 position : function(calStyle) { 232 var input = document.getElementById(this.id), 233 coords = APE.dom.getOffsetCoords(input); 234 calStyle.left = coords.x + "px"; 235 calStyle.top = coords.y + input.offsetHeight + "px"; 236 }, 237 238 /** 239 * Shows the calendar by setting visibility to 'visible'. 240 * @param {CSSStyleDeclaration} calStyle the caledar element's style. 241 */ 242 show : function(calStyle) { 243 calStyle.visibility = "visible"; 244 }, 245 246 /** 247 * Hides the calendar. 248 * @param {Event} e the DOM Event the triggered the action. 249 * @fires onhide(e) 250 */ 251 _hide : function(e) { 252 if(this._isHidden) return; 253 if(this._hasFocus) return; 254 255 this.onhide(e); 256 if(this.constructor.activeCalendar === this) 257 this.constructor.activeCalendar = null; 258 this.hide(document.getElementById(this.calendarId).style); 259 this._isHidden = true; 260 }, 261 262 /** 263 * Hides the calendar by setting visibility to "hidden" 264 * @param {CSSStyleDeclaration} calStyle the calendar element's style. 265 * @private 266 */ 267 hide : function(calStyle) { 268 calStyle.visibility = "hidden"; 269 }, 270 271 /** 272 * @event 273 * @description fires immediately after visibility has been set to "visible" 274 * in show(); 275 */ 276 onshow : function(){}, 277 278 /** 279 * @event 280 * @description fires immediately after visibility has been set to "hidden" 281 * in hide(); 282 */ 283 onhide : function(){}, 284 285 /** 286 * @event 287 * @description a date was selected. 288 */ 289 onselect : function(){}, 290 291 /** 292 * creates the HTML used for the calendar. 293 */ 294 create : function() { 295 if(this.calendarId) return; 296 297 this.calendarId = this.id + "-calendar"; 298 299 var d = document; 300 if(d.getElementById(this.calendarId)) return; 301 302 var calendar = d.createElement("div"), 303 join = Array.prototype.join, 304 td = join.call({length:7+1}, "<td><b> </b></td>"), 305 trs = join.call({length:6+1}, "<tr>"+td+"</tr>\n"), 306 widget = APE.widget, 307 Locale = widget.CalendarLocale, 308 dayNames = Locale.days.abbr, 309 calendarBody = "<tbody>" + trs + "</tbody>", 310 calendarHead = "<thead>" 311 +"<tr class='calendar-button-row'><th id='"+this.id+ "-prev-year' title='" 312 + Locale.previousYear + "'>«</th>" 313 +"<th id='"+this.id+ "-prev-month' title='" + Locale.previousMonth + "'>‹</th>" 314 +"<th colspan='3' class='ape-calendar-header'><div id='"+this.calendarId+"-header'" 315 +"> </div></th>" 316 +"<th id='"+this.id+ "-next-month' title='" + Locale.nextMonth 317 +"' class='"+this.calendarClass+"-days'>›</th>" 318 +"<th id='"+this.id+ "-next-year' title='" + Locale.nextYear + "'>»</th></tr>" 319 +"<tr><th>" 320 + dayNames.join("</th><th>") 321 + "</th></tr>" 322 +"</thead>"; 323 324 calendar.onselectstart = this.returnFalse; 325 calendar.innerHTML = "<table>" 326 + calendarHead + calendarBody 327 + "</table>"; 328 calendar.id = this.calendarId; 329 calendar.className = this.calendarClass; 330 calendar.onmousedown = calendar.onfocus = widget.Calendar.mousedownHandler; 331 d.body.appendChild(calendar); 332 }, 333 334 returnFalse : function(e) {return false;}, 335 /** 336 * Sets the date of the month. 337 */ 338 setDateOfMonth : function(dateOfMonth) { 339 this.displayDate.setDate(dateOfMonth); 340 var formatted = this.formatDate(); 341 document.getElementById(this.id).value = formatted; 342 }, 343 344 /** 345 * @return {Date} a copy of the internal Date 346 * representing calendar's currently selected date. 347 */ 348 getDate : function() { 349 return new Date(this.displayDate); 350 }, 351 352 /** 353 * formats the date in default of MM dd, yyyy. Looks like 354 * January 4, 2009. 355 * Override this to format <code>this.selectedDate</code>. 356 */ 357 formatDate : function() { 358 return APE.widget.CalendarLocale.months.abbr[this.displayDate.getMonth()] 359 + " " + this.displayDate.getDate() 360 +", " 361 + this.displayDate.getFullYear(); 362 }, 363 364 /** 365 * Sets the internal date object represented by the calendar. 366 * @internal 367 */ 368 setDate : function(date) { 369 if(!this.calendarId) this.create(); 370 var APE = window.APE, 371 CalendarLocale = APE.widget.CalendarLocale; 372 if(!CalendarLocale) throw Error("Missing Resource: APE.widget.CalendarLocale"); 373 374 var curDate = new Date, 375 d = document, 376 year = date.getFullYear(), 377 month = date.getMonth(), 378 monthName = CalendarLocale.months.full[month], 379 daysInMonth = this.days[month], 380 firstDayOfMonth, 381 i = 0, 382 j = 0, 383 isLeapYear = (0 == (year%4)) && ( (0 != (year%100)) || (0 == (year%400))), 384 calendar = d.getElementById(this.calendarId), 385 tbody = calendar.getElementsByTagName("table")[0].tBodies[0], 386 dayElements = tbody.getElementsByTagName("b"), 387 textContent = "textContent"in calendar ? "textContent" : "innerText", 388 calendarHeader = d.getElementById(this.calendarId+"-header"), 389 dom = APE.dom, 390 dayElement; 391 392 firstDayOfMonth = new Date(date); 393 firstDayOfMonth.setDate(1); 394 firstDayOfMonth = firstDayOfMonth.getDay(); 395 396 calendarHeader.firstChild.data = year + ", " + monthName; 397 398 if(month === 1 && isLeapYear) 399 daysInMonth += 1; 400 401 while(i < firstDayOfMonth) { 402 dayElement = dayElements[i++]; 403 dayElement[textContent] = ' '; 404 dayElement.className = this.hiddenDayClass; 405 } 406 // Fill in days. 407 while(j++ < daysInMonth) { 408 dayElement = dayElements[i++]; 409 dayElement[textContent] = j; 410 dayElement.className = ''; 411 } 412 413 // alert(dayElements.length + ", " + i + ", " + j); 414 415 for(i = firstDayOfMonth + daysInMonth, j = dayElements.length; i < j; i++){ 416 dayElement = dayElements[i]; 417 dayElement[textContent] = ' '; 418 dayElement.className = this.hiddenDayClass; 419 } 420 421 var selected = d.getElementById(this.id + "-selected-day"); 422 if(selected) { 423 selected.id = ""; 424 } 425 426 // Need to hilite current day. 427 if(curDate.getYear() == date.getYear() && curDate.getMonth() == date.getMonth()) { 428 var currentDay = firstDayOfMonth + curDate.getDate()-1; 429 this.currentDayIndex = currentDay; 430 var day = dayElements[currentDay]; 431 dom.addClass(day, this.calendarClass + "-today"); 432 } 433 if(date.getYear() == date.getYear() 434 && date.getMonth() == date.getMonth()) { 435 var selectedDayIndex = firstDayOfMonth + date.getDate(); 436 dayElement = dayElements[selectedDayIndex - 1]; 437 dom.addClass(dayElement, this.calendarClass + "-selected-day"); 438 dayElement.id = this.id + "-selected-day"; 439 } 440 this.displayDate = new Date(date); 441 } 442 };