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 };