OSDN Git Service

管理画面の日付検索をjavascrptのカレンダーで
[elecoma/elecoma.git] / public / javascripts / calendar / protocalendar.js
1 /*  protocalendar.js
2  *  (c) 2009 Spookies
3  * 
4  *  License : MIT-style license.
5  *  Web site: http://labs.spookies.jp/product/protocalendar
6  *
7  *  protocalendar.js - depends on prototype.js 1.6 or later
8  *  http://www.prototypejs.org/
9  *
10 /*--------------------------------------------------------------------------*/
11
12 var ProtoCalendar = Class.create();
13 ProtoCalendar.Version = "1.1.8.2";
14
15 ProtoCalendar.LangFile = new Object();
16 ProtoCalendar.LangFile['en'] = {
17   HOUR_MINUTE_ERROR: 'The time is not valid.',
18   NO_DATE_ERROR: 'No day has been selected.',
19   OK_LABEL: 'OK',
20   DEFAULT_FORMAT: 'mm/dd/yyyy',
21   LABEL_FORMAT: 'ddd mm/dd/yyyy',
22   MONTH_ABBRS: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
23   MONTH_NAMES: ['January','February','March','April','May','June','July','August','September','October','November','December'],
24   YEAR_LABEL: ' ',
25   MONTH_LABEL: ' ',
26   WEEKDAY_ABBRS: ['Sun','Mon','Tue','Wed','Thr','Fri','Sat'],
27   WEEKDAY_NAMES: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
28   YEAR_AND_MONTH: false
29 };
30
31 ProtoCalendar.LangFile.defaultLang = 'en';
32 ProtoCalendar.LangFile.defaultLangFile = function() { return ProtoCalendar.LangFile[defaultLang]; };
33
34 ProtoCalendar.newDate = function() {
35   var d = new Date();
36   d.setDate(1);
37   return d;
38 }
39
40 //Only check vertically
41 ProtoCalendar.withinViewport = function(element) {
42   var dimensions = ProtoCalendar.callWithVisibility(element, function() { return element.getDimensions(); });
43   var width = dimensions.width;
44   var height = dimensions.height;
45   var offsets = ProtoCalendar.callWithVisibility(element, function() { return element.viewportOffset(); });
46   var offsetX = offsets.left;
47   var offsetY = offsets.top;
48   return (offsetY >=0) && (offsetY + height <= document.viewport.getHeight());
49 }
50
51 ProtoCalendar.callWithVisibility = function(element, func) {
52   element = $(element);
53   var display = $(element).getStyle('display');
54   if (display != 'none' && display != null) {// Safari bug 
55     return func();
56   }
57   var els = element.style;
58   var originalVisibility = els.visibility;
59   var originalPosition = els.position;
60   var originalDisplay = els.display;
61   els.visibility = 'hidden';
62   els.position = 'absolute';
63   els.display = 'block';
64   var result = func();
65   els.display = originalDisplay;
66   els.position = originalPosition;
67   els.visibility = originalVisibility;
68   return result;
69 }
70
71 Object.extend(ProtoCalendar, {
72                 JAN: 0,
73                 FEB: 1,
74                 MAR: 2,
75                 APR: 3,
76                 MAY: 4,
77                 JUNE: 5,
78                 JULY: 6,
79                 AUG: 7,
80                 SEPT: 8,
81                 OCT: 9,
82                 NOV: 10,
83                 DEC: 11,
84
85                 SUNDAY: 0,
86                 MONDAY: 1,
87                 TUESDAY: 2,
88                 WEDNESDAY: 3,
89                 THURSDAY: 4,
90                 FRIDAY: 5,
91                 SATURDAY: 6,
92
93                 getNumDayOfMonth: function(year, month){
94                   return 32 - new Date(year, month, 32).getDate();
95                 },
96
97                 getDayOfWeek: function(year, month, day) {
98                   return new Date(year, month, day).getDay();
99                 }
100               });
101
102 ProtoCalendar.prototype = {
103   initialize: function(options) {
104     var date = ProtoCalendar.newDate();
105     this.options = Object.extend({
106                                    month: date.getMonth(),
107                                    year: date.getFullYear(),
108                                    lang: ProtoCalendar.LangFile.defaultLang
109                                  }, options || { });
110     var getHolidays = ProtoCalendar.LangFile[this.options.lang]['getHolidays'];
111     if (getHolidays) {
112       this.initializeHolidays = getHolidays.bind(top, this);
113     } else {
114       this.initializeHolidays = function() { this.holidays = []; };
115     }
116     this.date = new Date(this.options.year, this.options.month, 1);
117   },
118
119   getMonth: function() {
120     return this.date.getMonth();
121   },
122
123   getYear: function() {
124     return this.date.getFullYear();
125   },
126
127   invalidate: function() {
128     this.holidays = undefined;
129   },
130
131   setMonth: function(month) {
132     if (month != this.getMonth()) {
133       this.invalidate();
134     }
135     return this.date.setMonth(month);
136   },
137
138   setYear: function(year) {
139     if (year != this.getYear()) {
140       this.invalidate();
141     }
142     return this.date.setFullYear(year);
143   },
144
145   getDate: function() {
146     return this.date;
147   },
148
149   setDate: function(date) {
150     this.invalidate();
151     this.date = date;
152   },
153
154   setYearByOffset: function(offset) {
155     if (offset != 0) {
156       this.invalidate();
157     }
158     this.date.setFullYear(this.date.getFullYear() + offset);
159   },
160
161   setMonthByOffset: function(offset) {
162     if (offset != 0) {
163       this.invalidate();
164     }
165     this.date.setMonth(this.date.getMonth() + offset);
166   },
167
168   getNumDayOfMonth: function() {
169     return ProtoCalendar.getNumDayOfMonth(this.getYear(), this.getMonth());
170   },
171
172   getDayOfWeek: function(day) {
173     return ProtoCalendar.getDayOfWeek(this.getYear(), this.getMonth(), day);
174   },
175
176   clone: function() {
177     return new ProtoCalendar({year: this.getYear(), month: this.getMonth()});
178   },
179
180   getHoliday: function(day) {
181     if(!this.holidays) { this.initializeHolidays();}
182     var holiday = this.holidays[day];
183     return holiday? holiday : false;
184   },
185
186   initializeHolidays: function() {
187   }
188 };
189
190 var AbstractProtoCalendarRender = Class.create();
191 Object.extend(AbstractProtoCalendarRender, {
192                 id: 1,
193                 WEEK_DAYS_SUNDAY: [ 0, 1, 2, 3, 4, 5, 6 ],
194                 WEEK_DAYS_MONDAY: [ 1, 2, 3, 4, 5, 6, 0 ],
195                 WEEK_DAYS_INDEX_SUNDAY: [ 0, 1, 2, 3, 4, 5, 6 ],
196                 WEEK_DAYS_INDEX_MONDAY: [ 6, 0, 1, 2, 3, 4, 5 ],
197
198                 getId: function() {
199                   var id = AbstractProtoCalendarRender.id;
200                   AbstractProtoCalendarRender.id += 1;
201                   return id;
202                 }
203                });
204
205 AbstractProtoCalendarRender.prototype = {
206   initialize: function(options) {
207     this.id = AbstractProtoCalendarRender.getId();
208     this.options = Object.extend({
209                                    weekFirstDay : ProtoCalendar.MONDAY,
210                                    containerClass: 'cal-container',
211                                    tableClass: 'cal-table',
212                                    headerTopClass: 'cal-header-top',
213                                    headerClass: 'cal-header',
214                                    headerBottomClass: 'cal-header-bottom',
215                                    bodyTopClass: 'cal-body-top',
216                                    bodyClass: 'cal-body',
217                                    bodyBottomClass: 'cal-body-bottom',
218                                    bodyId: this.getIdPrefix() + '-body',
219                                    footerTopClass: 'cal-footer-top',
220                                    footerClass: 'cal-footer',
221                                    footerBottomClass: 'cal-footer-bottom',
222                                    footerId: this.getIdPrefix() + '-footer',
223                                    yearSelectClass: 'cal-select-year',
224                                    yearSelectId: this.getIdPrefix() + '-select-year',
225                                    monthSelectClass: 'cal-select-month',
226                                    monthSelectId: this.getIdPrefix() + '-select-month',
227                                    borderClass: 'cal-border',
228                                    hourMinuteInputClass: 'cal-input-hour-minute',
229                                    hourMinuteInputId: this.getIdPrefix() + '-input-hour-minute',
230                                    hourInputClass: 'cal-input-hour',
231                                    hourInputId: this.getIdPrefix() + '-input-hour',
232                                    minuteInputClass: 'cal-input-minute',
233                                    minuteInputId: this.getIdPrefix() + '-input-minute',
234                                    secondInputClass: 'cal-input-second',
235                                    secondInputId: this.getIdPrefix() + '-input-second',
236                                    okButtonClass: 'cal-ok-button',
237                                    okButtonId: this.getIdPrefix() + '-ok-button',
238                                    errorDivClass: 'cal-error-list',
239                                    errorDivId: this.getIdPrefix() + '-error-list',
240                                    labelRowClass: 'cal-label-row',
241                                    labelCellClass: 'cal-label-cell',
242                                    nextButtonClass: 'cal-next-btn',
243                                    prevButtonClass: 'cal-prev-btn',
244                                    dayCellClass: 'cal-day-cell',
245                                    dayClass: 'cal-day',
246                                    weekdayClass: 'cal-weekday',
247                                    sundayClass: 'cal-sunday',
248                                    saturdayClass: 'cal-saturday',
249                                    holidayClass: 'cal-holiday',
250                                    otherdayClass: 'cal-otherday',
251                                    disabledDayClass: 'cal-disabled',
252                                    selectedDayClass: 'cal-selected',
253                                    nextBtnId: this.getIdPrefix() + '-next-btn',
254                                    prevBtnId: this.getIdPrefix() + '-prev-btn',
255                                    lang: ProtoCalendar.LangFile.defaultLang,
256                                    showEffect: 'Appear',
257                                    hideEffect: 'Fade',
258                                    ifInvisible: 'Flip', /* None | Scroll | Flip */
259                                    scrollMargin: 20
260                                  }, options || {});
261     this.langFile = ProtoCalendar.LangFile[this.options.lang];
262     this.weekFirstDay = this.options.weekFirstDay;
263     this.initWeekData();
264     this.container = this.createContainer();
265     this.alignTo = $(this.options.alignTo);
266     this.alignOrient = 'Below';
267     if (navigator.appVersion.match(/\bMSIE\b/)) {
268       this.iframe = this.createIframe();
269     }
270     this.resizeHandler = this.setPosition.bind(this);
271   },
272
273   createContainer: function() {
274     var container = $(document.createElement('div'));
275     container.addClassName(this.options.containerClass);
276     container.setStyle({position:'absolute',
277                         top: "0px",
278                         left: "0px",
279                         zindex:1,
280                         display: 'none'});
281     container.hide();
282     document.body.appendChild(container);
283     return container;
284   },
285
286   createIframe: function() {
287     var iframe = document.createElement("iframe");
288     iframe.setAttribute("src", "javascript:false;");
289     iframe.setAttribute("frameBorder", "0");
290     iframe.setAttribute("scrolling", "no");
291     Element.setStyle(iframe, { position:'absolute',
292                                top: "0px",
293                                left: "0px",
294                                zindex:10,
295                                display: 'none',
296                                overflow: 'hidden',
297                                filter: 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)'
298                              });
299     document.body.appendChild(iframe);
300     return $(iframe);
301   },
302
303   getWeekdayLabel: function(weekday) {
304     return this.langFile.WEEKDAY_ABBRS[weekday];
305   },
306
307   getWeekdays: function() {
308     return this.weekdays;
309   },
310
311   initWeekData: function() {
312     if (this.weekFirstDay == ProtoCalendar.SUNDAY) {
313       this.weekLastDay = ProtoCalendar.SATURDAY;
314       this.weekdays = AbstractProtoCalendarRender.WEEK_DAYS_SUNDAY;
315       this.weekdaysIndex = AbstractProtoCalendarRender.WEEK_DAYS_INDEX_SUNDAY;
316     } else {
317       this.weekFirstDay == ProtoCalendar.MONDAY
318       this.weekLastDay = ProtoCalendar.SUNDAY;
319       this.weekdays = AbstractProtoCalendarRender.WEEK_DAYS_MONDAY;
320       this.weekdaysIndex = AbstractProtoCalendarRender.WEEK_DAYS_INDEX_MONDAY;
321     }
322   },
323
324   getCalendarBeginDay: function(calendar) {
325     var offset = this.getDayIndexOfWeek(calendar, 1);
326     var date = new Date(calendar.getYear(), calendar.getMonth(), 1 - offset);
327     return date;
328   },
329
330   getCalendarEndDay: function(calendar) {
331     var lastDayOfMonth = calendar.getNumDayOfMonth();
332     var offset = 6 - this.getDayIndexOfWeek(calendar, lastDayOfMonth);
333     var date = new Date(calendar.getYear(), calendar.getMonth(), lastDayOfMonth + offset + 1);
334     return date;
335   },
336
337   getDayIndexOfWeek: function(calendar, day) {
338     return this.weekdaysIndex[ calendar.getDayOfWeek(day) ];
339   },
340
341   getIdPrefix: function() {
342     return 'cal' + this.id;
343   },
344
345   getDayDivId: function(date) {
346     return this.getIdPrefix() + '-year' + date.getFullYear() + '-month' + date.getMonth() + '-day' + date.getDate();
347   },
348
349   setPosition: function() {
350     if (!this.alignTo) return true;
351     this.setAlignment(this.alignTo, this.container, this.alignOrient);
352     var withinView = ProtoCalendar.withinViewport(this.container);
353     if (!withinView && this.options.ifInvisible == 'Flip') {
354       this.alignOrient = (this.alignOrient == 'Above' ? 'Below' : 'Above');
355       this.setAlignment(this.alignTo, this.container, this.alignOrient);
356     }
357     if (this.iframe) {
358       var dimensions = Element.getDimensions(this.container);
359       this.iframe.setAttribute("width", dimensions.width);
360       this.iframe.setAttribute("height", dimensions.height);
361       this.setAlignment(this.alignTo, this.iframe, this.alignOrient);
362     }
363     if (this.options.ifInvisible == 'Scroll') this.scrollIfInvisible();
364     return true;
365   },
366
367   setAlignment: function(alignTo, element, pos) {
368     var offsets = Position.cumulativeOffset(alignTo);
369     element.setStyle({left: offsets[0] + "px"});
370     if (pos == 'Above') {
371       var elementHeight = ProtoCalendar.callWithVisibility(element, function() { return element.offsetHeight; });
372       element.setStyle({top: (offsets[1] - elementHeight) + "px"});
373     } else if (pos == 'Below') {
374       element.setStyle({top: (offsets[1] + alignTo.offsetHeight) + "px"});
375     } else {
376       //Unknown option
377     }
378   },
379
380   show: function(option) {
381     Event.observe(window, 'resize', this.resizeHandler);
382     this.setPosition();
383     if (typeof Effect != 'undefined') {
384       var effect =  this.options['showEffect'] || 'Appear';
385       if (!this._effect || this._effect.state == 'finished') {
386         this._effect = new Effect[effect](this.container, {duration: 0.5});
387       }
388     } else {
389       this.container.show();
390     }
391     if (this.iframe) this.iframe.show();
392   },
393
394   scrollIfInvisible: function() {
395     var container = this.container;
396     var dimensions = ProtoCalendar.callWithVisibility(container, function() { return container.getDimensions(); });
397     var width = dimensions.width;
398     var height = dimensions.height;
399     var offsets = ProtoCalendar.callWithVisibility(container, function() { return container.viewportOffset(); });
400     var offsetX = offsets.left;
401     var offsetY = offsets.top;
402     var diff = offsetY + height - document.viewport.getHeight();
403     if (diff > 0) {
404       window.scrollBy(0, diff + this.options.scrollMargin);
405     }
406   },    
407
408   hide: function(option) {
409     Event.stopObserving(window, 'resize', this.resizeHandler);
410     if (!this.container.visible()) {
411       return ;
412     }
413     if (typeof Effect != 'undefined') {
414       var effect =  this.options['hideEffect'] || 'Fade';
415       if (!this._effect || this._effect.state == 'finished') {
416         this._effect = new Effect[effect](this.container, {duration: 0.3});
417       }
418     } else {
419       this.container.hide();
420     }
421     if (this.iframe) this.iframe.hide();
422   },
423
424   hideImmediately: function(option) {
425     if (!this.container.visible()) {
426       return ;
427     }
428     this.container.hide();
429     if (this.iframe) this.iframe.hide();
430   },
431
432   toggle: function(element) {
433     this.container.visible() ? this.hide() : this.show();
434   },
435
436   render: function(calendar) { },
437
438   rerender: function(calendar) { },
439
440   getContainer: function() {
441     return this.container;
442   },
443
444   getPrevButton: function() {
445     return $(this.options.prevBtnId);
446   },
447
448   getNextButton: function() {
449     return $(this.options.nextBtnId);
450   },
451
452   getYearSelect: function() {
453     return $(this.options.yearSelectId);
454   },
455
456   getMonthSelect: function() {
457     return $(this.options.monthSelectId);
458   },
459
460   getHourInput: function() {
461     return $(this.options.hourInputId);
462   },
463
464   getMinuteInput: function() {
465     return $(this.options.minuteInputId);
466   },
467
468   getSecondInput: function() {
469     return $(this.options.secondInputId);
470   },
471
472   getOkButton: function() {
473     return $(this.options.okButtonId);
474   },
475
476   getBody: function() {
477     return $(this.options.bodyId);
478   },
479
480   getDayDivs: function() {
481     //Good Performance for IE
482     var divEls = [];
483     var dayDivs = this.dayDivs;
484     for (var i = 0; i < dayDivs.length; i++) {
485       divEls.push(document.getElementById(dayDivs[i]));
486     }
487     return divEls;
488     //return this.container.getElementsBySelector("a." + this.options.dayClass);
489   },
490
491   getDateFromEl: function(el) {
492     var element = $(el);
493     return new Date(element.readAttribute('year'), element.readAttribute('month'), element.readAttribute('day'));
494   },
495
496   //Set hour minute (second) to date from the input, if invalid return undefined.
497   //The input date parameter will not be tainted even if it fails, because the return result is a copy.
498   injectHourMinute: function(date) {
499     if (!date || isNaN(date.getTime())) return undefined;
500     var rd = new Date(date.getFullYear(), date.getMonth(), date.getDate());
501     rd.setHours(parseInt(this.getHourInput().value, 10));
502     rd.setMinutes(this.getMinuteInput().value);
503     if (this.options.enableSecond) rd.setSeconds(this.getSecondInput().value);
504     return isNaN(rd.getTime()) ? undefined : rd;
505   },
506
507   selectDate: function(date) {
508     var dayEl = $(this.getDayDivId(date));
509     if (dayEl) dayEl.addClassName(this.options.selectedDayClass);
510   },
511
512   selectTime: function(date) {
513     if (!date) return;
514     this.getHourInput().value = date.getHours();
515     this.getMinuteInput().value = date.getMinutes();
516     if (this.options.enableSecond) this.getSecondInput().value = date.getSeconds();
517   },
518
519   deselectDate: function(date) {
520     if (date) {
521       var dateEl = $(this.getDayDivId(date));
522       if (dateEl) dateEl.removeClassName(this.options.selectedDayClass);
523     }
524   },
525
526   evaluateWithOptions: function(html) {
527     var template = new Template(html);
528     return template.evaluate(this.options);
529   },
530
531   defaultOnError: function(msg) {
532     this.ensureErrorDiv();
533     this.errorDiv.show();
534     this.errorDiv.innerHTML += '<li>' + this.langFile[msg] + '</li>';
535   },
536
537   hideError: function() {
538     this.ensureErrorDiv();
539     this.errorDiv.innerHTML = '';
540     this.errorDiv.hide();
541   },
542
543   ensureErrorDiv: function() {
544     if (!this.errorDiv) {
545       var errorDivHtml = '<div class="#{errorDivClass}" id="#{errorDivId}"><ul></ul></div>';
546       new Insertion.Before($(this.options.footerId), this.evaluateWithOptions(errorDivHtml));
547       this.errorDiv = $(this.options.errorDivId);
548     }
549     },
550
551   isSelectable: function(date) {
552       return (this.options.minDate - date) <= 0 && (date - this.options.maxDate) <= 0;
553     }
554   
555 };
556
557 var ProtoCalendarRender = Class.create();
558 Object.extend(ProtoCalendarRender.prototype, AbstractProtoCalendarRender.prototype);
559
560 Object.extend(
561   ProtoCalendarRender.prototype,
562   {
563     render: function(calendar) {
564       var html = '';
565       html += this.renderHeader(calendar);
566       html += '<div class="#{bodyTopClass}"></div><div class="#{bodyClass}" id="#{bodyId}">';
567       html += '</div><div class="#{bodyBottomClass}"></div>';
568       if (this.options.enableHourMinute) html += this.renderHourMinute();
569       html += this.renderFooter(calendar);
570       this.container.innerHTML = this.evaluateWithOptions(html);
571       this.rerender(calendar);
572     },
573
574     rerender: function(calendar) {
575       this.getBody().innerHTML = this.evaluateWithOptions(this.renderBody(calendar));
576       SelectCalendar.selectOption(this.getMonthSelect(), calendar.getMonth());
577       SelectCalendar.selectOption(this.getYearSelect(), calendar.getYear());
578       if (this.container.visible()) this.setPosition();
579     },
580
581     renderHeader: function(calendar) {
582       var html = '';
583       // required 'href'
584       html += '<div class="#{headerTopClass}"></div><div class="#{headerClass}">' +
585         '<a href="#" id="#{prevBtnId}" class="#{prevButtonClass}">&lt;&lt;</a>' +
586         this.createSelect(calendar.getYear(), calendar.getMonth()) +
587         '<a href="#" id="#{nextBtnId}" class="#{nextButtonClass}">&gt;&gt;</a>' +
588         '</div><div class="#{headerBottomClass}"></div>';
589       return html;
590     },
591
592     renderFooter: function(calendar) {
593       return '<div class="#{footerTopClass}"></div><div class="#{footerClass}" id="#{footerId}"></div><div class="#{footerBottomClass}"></div>';
594     },
595
596     renderHourMinute: function() {
597       if (!this.hourMinuteHtml) {
598         var html = '<div class="#{borderClass}"></div><div id="#{hourMinuteInputId}" class="#{hourMinuteInputClass}">';
599         html += '<input type="text" name="hour" size="2" maxlength="2" class="#{hourInputClass}" id="#{hourInputId}" />' +
600           ':<input type="text" name="minute" size="2" maxlength="2" class="#{minuteInputClass}" id="#{minuteInputId}" />';
601         if (this.options.enableSecond) {
602           html += ':<input type="text" name="second" size="2" maxlength="2" class="#{secondInputClass}" id="#{secondInputId}"/>';
603         }
604         html += '<input type="button" value="' + this.langFile['OK_LABEL'] + '" class="#{okButtonClass}" name="ok_button" id="#{okButtonId}"/>';
605         html += '</div>';
606         this.hourMinuteHtml =  html;
607       }
608       return this.hourMinuteHtml;
609     },
610
611     createSelect: function(year, month) {
612       var yearPart = this.createYearSelect(year) + this.langFile.YEAR_LABEL;
613       var monthPart = this.createMonthSelect(month) + this.langFile.MONTH_LABEL;
614       if (this.langFile.YEAR_AND_MONTH) {
615         return  yearPart + monthPart;
616       } else {
617         return monthPart + yearPart;
618       }
619     },
620
621     createYearSelect: function(year) {
622       var html = '';
623       html += '<select id="#{yearSelectId}" class="#{yearSelectClass}">';
624       for (var y = this.options.startYear, endy = this.options.endYear; y <= endy; y += 1) {
625         html += '<option value="' + y + '"' + (y == year ? ' selected' : '') + '>' + y + '</option>';
626       }
627       html += '</select>';
628       return html;
629     },
630
631     createMonthSelect: function(month) {
632       if (!this.monthSelectHtml) {
633         var html = '';
634         html += '<select id="#{monthSelectId}" class="#{monthSelectClass}">';
635         for (var m = ProtoCalendar.JAN; m <= ProtoCalendar.DEC; m += 1) {
636           html += '<option value="' + m + '"' + (m == month ? ' selected' : '') + '>' + this.langFile['MONTH_ABBRS'][m] + '</option>';
637         }
638         html += '</select>';
639         this.monthSelectHtml = html;
640       }
641       return this.monthSelectHtml;
642     },
643
644     renderBody: function(calendar) {
645       this.dayDivs = [];
646       var html = '<table class="#{tableClass}" cellspacing="0">';
647       html += '<tr class="#{labelRowClass}">';
648       var othis = this;
649       if (!this.headHtml) {
650         this.headHtml = '';
651         $A(this.getWeekdays()).each(function(weekday) {
652                                       var exClassName = '';
653                                       if (weekday == ProtoCalendar.SUNDAY) { exClassName = ' #{sundayClass}'; }
654                                       if (weekday == ProtoCalendar.SATURDAY) { exClassName = ' #{saturdayClass}'; }
655                                       othis.headHtml += '<th class="#{labelCellClass}' + exClassName + '">' +
656                                         othis.getWeekdayLabel(weekday) +
657                                         '</th>';
658                                     });
659       }
660       html += this.headHtml;
661       var curDay = this.getCalendarBeginDay(calendar);
662       var calEndDay = this.getCalendarEndDay(calendar);
663       html += '<tbody>';
664       var dayNum = Math.round((calEndDay - curDay) / 1000 / 60 / 60 / 24);
665       for(var i = 0; i < dayNum; i += 1, curDay.setDate(curDay.getDate() + 1)) {
666         var divClassName;
667         var holiday = calendar.getHoliday(curDay.getDate());
668         if(curDay.getMonth() != calendar.getMonth()) {
669           divClassName = this.options.otherdayClass;
670         } else if (holiday) {
671           divClassName = this.options.holidayClass;
672         } else if (curDay.getDay() == ProtoCalendar.SUNDAY) {
673           divClassName = this.options.sundayClass;
674         } else if (curDay.getDay() == ProtoCalendar.SATURDAY) {
675           divClassName = this.options.saturdayClass;
676         } else {
677           divClassName = this.options.weekdayClass;
678         }
679
680         if (curDay.getDay() == this.weekFirstDay) { html += '<tr>'; }
681         var dayId = this.getDayDivId(curDay);
682         var dayHtml = '';
683         if (this.isSelectable(curDay)) {
684           dayHtml = '<a class="#{dayClass}" href="#" id="' + dayId +
685             (holiday ? '" title="' + holiday : '') +
686             '" year="' + curDay.getFullYear() +
687             '" month="' + curDay.getMonth() +
688             '" day="' + curDay.getDate() +
689             '">' + curDay.getDate() + '</a>';
690           this.dayDivs.push(dayId);
691         } else {
692           divClassName += ' ' + this.options.disabledDayClass;
693           dayHtml = curDay.getDate();
694         }
695         html += '<td class="' + divClassName + ' #{dayCellClass}">' + dayHtml + '</td>';
696         if (curDay.getDay() == this.weekLastDay) { html += '</tr>'; }
697       }
698       html += '</tbody></table>';
699       return html;
700     }
701
702   });
703
704 var ProtoCalendarController = Class.create();
705 ProtoCalendarController.prototype = {
706   initialize: function(calendarRender, options) {
707     this.options = Object.extend({
708                                    onHourMinuteError: this.defaultOnHourMinuteError.bind(this),
709                                    onNoDateError: this.defaultOnNoDateError.bind(this)
710                                  }, options);
711     this.calendarRender = calendarRender;
712     this.initializeDate();
713     this.calendar = new ProtoCalendar(this.options);
714     this.calendarRender.render(this.calendar);
715     if (options.year && options.month && options.day) {
716       var date = new Date(this.options.year, this.options.month, this.options.day);
717       if (options.hour && options.minute && options.second) {
718         date.setHours(options.hour, options.minute && options.second);
719       }
720       this.selectDate(date, true);
721     } else {
722       this.selectDate(null);
723     }
724     this.observeEventsOnce();
725     this.observeEvents();
726     this.onChangeHandlers = [];
727   },
728
729   initializeDate: function() {
730     var date = ProtoCalendar.newDate();
731     if (!this.options.year) {
732       if (date.getFullYear() >= this.options.startYear && date.getFullYear() <= this.options.endYear) { 
733         this.options.year = date.getFullYear();
734       } else {
735         this.options.year = this.options.startYear;
736       }
737     }
738     if (!this.options.month) {
739       this.options.month = date.getMonth();
740     }
741     if (!this.options.day) {
742       this.options.day = date.getDate();
743     }
744   },
745
746   observeEventsOnce: function() {
747     var calrndr = this.calendarRender;
748     calrndr.getPrevButton().observe('click', this.showPrevMonth.bindAsEventListener(this));
749     calrndr.getNextButton().observe('click', this.showNextMonth.bindAsEventListener(this));
750     var othis = this;
751     var yearSelect = calrndr.getYearSelect();
752     var monthSelect = calrndr.getMonthSelect();
753     var year = this.calendar.getYear();
754     var month = this.calendar.getMonth();
755     yearSelect.observe('change', function() {
756                          othis.setMonth(parseInt(yearSelect[yearSelect.selectedIndex].value, 10), parseInt(monthSelect[monthSelect.selectedIndex].value, 10));
757                        });
758     monthSelect.observe('change', function() {
759                           othis.setMonth(parseInt(yearSelect[yearSelect.selectedIndex].value, 10), parseInt(monthSelect[monthSelect.selectedIndex].value, 10));
760                        });
761     // add auto focus
762     if (this.options.enableHourMinute) {
763       var hour = calrndr.getHourInput();
764       var minute = calrndr.getMinuteInput();
765       hour.observe('keyup', this._autoFocus.bindAsEventListener(hour, minute));
766       hour.observe('keydown', this._disablePaste.bindAsEventListener(hour));
767       hour.observe('contextmenu', this._disableContextMenu.bindAsEventListener(hour));
768       var nextEl = this.options.enableSecond ? calrndr.getSecondInput() : calrndr.getOkButton();
769       minute.observe('keyup', this._autoFocus.bindAsEventListener(minute, nextEl));
770       minute.observe('keydown', this._disablePaste.bindAsEventListener(minute));
771       minute.observe('contextmenu', this._disableContextMenu.bindAsEventListener(minute));
772       if (navigator.appVersion.match(/\bMSIE\b/)) {
773         hour.setStyle({'imeMode': 'disabled'});
774         minute.setStyle({'imeMode': 'disabled'});
775       }
776     }
777     if (this.options.enableSecond) {
778       var second = calrndr.getSecondInput();
779       second.observe('keyup', this._autoFocus.bindAsEventListener(second, calrndr.getOkButton()));
780       second.observe('keydown', this._disablePaste.bindAsEventListener(second));
781       second.observe('contextmenu', this._disableContextMenu.bindAsEventListener(second));
782       if (navigator.appVersion.match(/\bMSIE\b/)) {
783         second.setStyle({'imeMode': 'disabled'});
784       }
785     }
786     if (this.options.enableHourMinute) calrndr.getOkButton().observe('click', this.onSubmit.bind(this));
787   },
788   _disableContextMenu: function(event) {
789     Event.stop(event);
790     return false;
791   },
792   _disablePaste: function(event) {
793     // ctrl + v || shift + insert
794     if ((event.keyCode == 86 && event.ctrlKey) || (event.keyCode == 45 && event.shiftKey)) {
795       Event.stop(event);
796       return false;
797     }
798   },
799   _autoFocus: function(event, nextEl) {
800     // shift || tab
801     if (event.keyCode == 16 || event.keyCode == 9 || (event.keyCode == 9 && event.shiftKey)) {
802       Event.stop(event);
803       return false;
804     }
805     var v = this.value;
806     if (v.length && v.length == 2) {
807       nextEl.focus();
808       nextEl.select();
809     }
810     return true;
811   },
812   observeEvents: function() {
813     var othis = this;
814     this.calendarRender.getDayDivs().each(function(el) {
815                                             Event.observe(el, 'click', othis.onClickHandler.bindAsEventListener(othis));
816                                           });
817   },
818
819   onClickHandler: function(event) {
820     Event.stop(event);
821     var date = this.calendarRender.getDateFromEl(Event.element(event));
822     if (date) {
823       this.selectDate(date);
824       if (!this.options.enableHourMinute) {
825         this.onChangeHandler();
826         setTimeout(this.hideCalendar.bind(this), 150);
827       }
828     }
829   },
830
831   onSubmit: function() {
832     this.hideError();
833     var date = this.selectedDate;
834     if (!date) return this.options.onNoDateError();
835     date = this.calendarRender.injectHourMinute(date);
836     if (!date) {
837       this.options.onHourMinuteError();
838     } else {
839       this.selectDate(date, true);
840       if (this.options.enableHourMinute) this.calendarRender.selectTime(date);
841       this.onChangeHandler();
842       this.hideCalendar();
843     }
844   },
845
846   selectDate: function(date, redraw) {
847     this.calendarRender.deselectDate(this.selectedDate);
848     this.selectedDate = date;
849     if (!date) return;
850     if (redraw && (date.getFullYear() != this.calendar.getYear() || date.getMonth() != this.calendar.getMonth())) {
851       this.setMonth(date.getFullYear(), date.getMonth());
852     }
853     this.calendarRender.selectDate(this.selectedDate);
854   },
855
856   getSelectedDate: function() {
857     return this.selectedDate;
858   },
859
860   addChangeHandler: function(func) {
861     this.onChangeHandlers.push(func);
862   },
863
864   onChangeHandler: function() {
865     this.onChangeHandlers.each(function(f) { f(); });
866   },
867
868   showCalendar: function() {
869     this.calendarRender.show();
870   },
871
872   hideCalendar: function() {
873     this.calendarRender.hide();
874   },
875
876   blurCalendar: function(event) {
877     if (event.keyCode == 9) {
878       this.hideImmediatelyCalendar();
879     }
880   },
881
882   hideImmediatelyCalendar: function() {
883     this.calendarRender.hideImmediately();
884   },
885
886   toggleCalendar: function() {
887     this.calendarRender.toggle();
888   },
889
890   showPrevMonth: function(event) {
891     this.shiftMonthByOffset(-1);
892     if (event) Event.stop(event);
893   },
894
895   showNextMonth: function(event) {
896     this.shiftMonthByOffset(1);
897     if (event) Event.stop(event);
898   },
899
900   shiftMonthByOffset: function(offset) {
901     if (offset == 0) return;
902     var newDate = new Date(this.calendar.getDate().getTime());
903     newDate.setMonth(newDate.getMonth() + offset);
904     if (this.options.startYear > newDate.getFullYear() || this.options.endYear < newDate.getFullYear()) return;
905     this.calendar.setMonthByOffset(offset);
906     this.afterSet();
907   },
908
909   setMonth: function(year, month) {
910     if (this.calendar.getYear() == year && this.calendar.getMonth() == month) return;
911     this.calendar.setYear(year);
912     this.calendar.setMonth(month);
913     this.afterSet();
914   },
915
916   afterSet: function() {
917     this.calendarRender.rerender(this.calendar);
918     this.selectDate(this.selectedDate);
919     this.observeEvents();
920   },
921
922   getContainer: function() {
923     return this.calendarRender.getContainer();
924   },
925
926   defaultOnHourMinuteError: function() {
927     this.calendarRender.defaultOnError('HOUR_MINUTE_ERROR');
928   },
929
930   defaultOnNoDateError: function() {
931     this.calendarRender.defaultOnError('NO_DATE_ERROR');
932   },
933
934   hideError: function() {
935     this.calendarRender.hideError();
936   }
937 };
938
939 //Don't instantiate this, extend BaseCalendar
940 var BaseCalendar = Class.create();
941 BaseCalendar.bindOnLoad = function(f) {
942   if (document.observe) {
943     document.observe('dom:loaded', f);
944   } else {
945     Event.observe(window, 'load', f);
946   }
947 };
948
949
950 BaseCalendar.prototype = {
951   initialize: function(options) {
952     throw "Cannot instantiate BaseCalendar.";
953   },
954
955   initializeOptions: function(options) {
956     if (!options) options = {};
957     this.options = Object.extend({
958                                    startYear: ProtoCalendar.newDate().getFullYear() - 10,
959                                    endYear: ProtoCalendar.newDate().getFullYear() + 10,
960                                    minDate: new Date(1900, 0, 1),
961                                    maxDate: new Date(3000, 0, 1),
962                                    format: ProtoCalendar.LangFile[options.lang || ProtoCalendar.LangFile.defaultLang]['DEFAULT_FORMAT'],
963                                    enableHourMinute: false,
964                                    enableSecond: false,
965                                    lang: ProtoCalendar.LangFile.defaultLang,
966                                    triggers: []
967                                  }, options);
968   },
969
970   initializeBase: function() {
971     this.calendarController = new ProtoCalendarController(new ProtoCalendarRender(this.options), this.options);
972     this.langFile = ProtoCalendar.LangFile[this.options.lang] || ProtoCalendar.LangFile.defaultLangFile();
973     this.triggers = [];
974     this.options.triggers.each(this.addTrigger.bind(this));
975     this.changeHandlers = [];
976     this.observeEvents();
977   },
978
979   addTrigger: function(el) {
980     this.triggers.push($(el));
981     $(el).setStyle({'cursor': 'pointer'});
982   },
983
984   observeEvents: function() {
985     Event.observe(document.body, 'click', this.windowClickHandler.bindAsEventListener(this));
986     this.calendarController.addChangeHandler(this.onCalendarChange.bind(this));
987     this.doObserveEvents();
988   },
989
990   doObserveEvents: function() {
991     //Override this
992   },
993
994   windowClickHandler: function(event) {
995     var target = $(Event.element(event));
996     if (this.triggers.include(target)) {
997       this.calendarController.toggleCalendar();
998     } else if (target != this.input && !Element.descendantOf(target, this.calendarController.getContainer())) {
999       this.calendarController.hideCalendar();
1000     }
1001   },
1002
1003   addChangeHandler: function(f) {
1004     this.changeHandlers.push(f);
1005   },
1006
1007   onCalendarChange: function() {
1008     this.changeHandlers.each(function(f) { f(); });
1009   }
1010 };
1011
1012 var InputCalendar = Class.create();
1013 InputCalendar.createOnLoaded = function(input, options) {
1014   BaseCalendar.bindOnLoad(function() {
1015     new InputCalendar(input, options);
1016   });
1017 };
1018 InputCalendar.initCalendars = function(inputs, options) {
1019   if (document.observe) {
1020     document.observe('dom:loaded', function() {
1021       $$(inputs).each(function(input) {
1022         new InputCalendar(input, options);
1023       });
1024     });
1025   } else {
1026     Event.observe(window, 'load', function() {
1027       $$(inputs).each(function(input) {
1028         new InputCalendar(input, options);
1029       });
1030     });
1031   }
1032 };
1033 Object.extend(InputCalendar.prototype, BaseCalendar.prototype);
1034 Object.extend(
1035   InputCalendar.prototype,
1036   {
1037     initialize: function(input, options) {
1038       this.input = $(input); // used in doObserveEvents()
1039       this.initializeOptions(options);
1040       this.options = Object.extend({
1041                                      alignTo: input,
1042                                      inputReadOnly: false,
1043                                      labelFormat: undefined,
1044                                      labelEl: undefined
1045                                    }, this.options);
1046       this.initializeBase();
1047       this.initializeInput();
1048       this.initializeLabel();
1049     },
1050
1051     initializeInput: function() {
1052       this.dateFormat = new ProtoCalendar.DateFormat(this.options.format);
1053       if (this.input.value && this.dateFormat.parse(this.input.value)) {
1054         this.onInputChange();
1055       } else {
1056         this.onCalendarChange();
1057       }
1058       if (this.options.enableHourMinute) {
1059         this.calendarController.calendarRender.selectTime(this.calendarController.selectedDate);
1060       }
1061       if (this.options.inputReadOnly) {
1062         this.input.setAttribute('readOnly', this.options.inputReadOnly);
1063       }
1064     },
1065
1066     initializeLabel: function() {
1067       this.labelFormat = new ProtoCalendar.DateFormat(this.options.labelFormat || this.langFile['LABEL_FORMAT']);
1068       var labelElm = $(this.options.labelEl);
1069       if ((! labelElm) && this.options.labelFormat) {
1070         var labelId = this.input.id + '_label';
1071         new Insertion.After(this.input, "<div id='" + labelId + "'></div>");
1072         labelElm = $(labelId);
1073       }
1074       this.labelEl = labelElm;
1075       this.changeLabel();
1076     },
1077
1078     changeLabel: function() {
1079       if (!this.labelEl) return;
1080       if (this.calendarController.getSelectedDate()) {
1081         this.labelEl.innerHTML = this.labelFormat.format(this.calendarController.getSelectedDate(), this.options.lang);
1082       }
1083     },
1084
1085     doObserveEvents: function() {
1086       this.input.observe('change', this.onInputChange.bind(this));
1087       this.input.observe('focus', this.calendarController.showCalendar.bind(this.calendarController));
1088       this.input.observe('keydown', this.calendarController.blurCalendar.bindAsEventListener(this.calendarController));
1089       this.addChangeHandler(this.changeInputValue.bind(this));
1090       this.addChangeHandler(this.changeLabel.bind(this));
1091     },
1092
1093     onInputChange: function() {
1094       var date = this.dateFormat.parse(this.input.value);
1095       if (date) {
1096         this.calendarController.selectDate(date, true);
1097         if (this.options.enableHourMinute) this.calendarController.calendarRender.selectTime(date);
1098       } else {
1099         var inputValue = this.input.value.toLowerCase();
1100         var date;
1101         if (this.langFile['today'] && this.langFile['today'] == inputValue || inputValue == 'today') {
1102           date = ProtoCalendar.newDate();
1103         } else if (this.langFile['tomorrow'] && this.langFile['tomorrow'] == inputValue || inputValue == 'tomorrow') {
1104           date = ProtoCalendar.newDate();
1105           date.setDate(date.getDate() + 1);
1106         } else if (this.langFile['yesterday'] && this.langFile['yesterday'] == inputValue || inputValue == 'yesterday') {
1107           date = ProtoCalendar.newDate();
1108           date.setDate(date.getDate() - 1);
1109         } else if (this.langFile.parseDate && (date = this.langFile.parseDate(inputValue))) {
1110           //done is parseDate
1111         } else {
1112           date = undefined;
1113         }
1114         this.calendarController.selectDate(date, true);
1115         this.onCalendarChange();
1116       }
1117       this.changeLabel();
1118     },
1119
1120     changeInputValue: function() {
1121       this.input.value = this.dateFormat.format(this.calendarController.getSelectedDate(), this.options.lang);
1122     }
1123   });
1124
1125
1126 ProtoCalendar.DateFormat = Class.create();
1127 Object.extend(ProtoCalendar.DateFormat,
1128               {
1129                 MONTH_ABBRS: ProtoCalendar.LangFile.en.MONTH_ABBRS,
1130                 MONTH_NAMES: ProtoCalendar.LangFile.en.MONTH_NAMES,
1131                 WEEKDAY_ABBRS: ProtoCalendar.LangFile.en.WEEKDAY_ABBRS,
1132                 WEEKDAY_NAMES: ProtoCalendar.LangFile.en.WEEKDAY_NAMES,
1133                 formatRegexp: /(?:d{3,4}i|d{1,4}|m{1,4}|yy(?:yy)?|([hHMs])\1?|TT|tt|[lL])|.+?/g,
1134                 zeroize: function (value, length) {
1135                   if (!length) length = 2;
1136                   value = String(value);
1137                   for (var i = 0, zeros = ''; i < (length - value.length); i++) {
1138                     zeros += '0';
1139                   }
1140                   return zeros + value;
1141                 }
1142               });
1143
1144 ProtoCalendar.DateFormat.prototype =  {
1145   initialize: function(format) {
1146     this.dateFormat = format;
1147     this.parserInited = false;
1148     this.formatterInited = false;
1149   },
1150
1151   format: function(date, lang) {
1152     if (!this.formatterInited) this.initFormatter();
1153     if (!date) return '';
1154     var langFile = ProtoCalendar.LangFile[lang || ProtoCalendar.LangFile.defaultLang];
1155     var str = '';
1156     this.formatHandlers.each(function(f) {
1157                                str += f(date, langFile);
1158                              });
1159     return str;
1160   },
1161
1162   initFormatter: function() {
1163     var handlers = [];
1164     var matches = this.dateFormat.match(ProtoCalendar.DateFormat.formatRegexp);
1165     for (var i = 0, n = matches.length; i < n; i++) {
1166       switch(matches[i]) {
1167       case 'd':       handlers.push(function(date, lf) { return date.getDate(); }); break;
1168       case 'dd':      handlers.push(function(date, lf) { return ProtoCalendar.DateFormat.zeroize(date.getDate()) }); break;
1169       case 'ddd':     handlers.push(function(date, lf) { return ProtoCalendar.DateFormat.WEEKDAY_ABBRS[date.getDay()]; }); break;
1170       case 'dddd':    handlers.push(function(date, lf) { return ProtoCalendar.DateFormat.WEEKDAY_NAMES[date.getDay()]; }); break;
1171       case 'dddi':    handlers.push(function(date, lf) { return lf.WEEKDAY_ABBRS[date.getDay()]; }); break;
1172       case 'ddddi':   handlers.push(function(date, lf) { return lf.WEEKDAY_NAMES[date.getDay()]; }); break;
1173       case 'm':       handlers.push(function(date, lf) { return date.getMonth() + 1; }); break;
1174       case 'mm':      handlers.push(function(date, lf) { return ProtoCalendar.DateFormat.zeroize(date.getMonth() + 1); }); break;
1175       case 'mmm':     handlers.push(function(date, lf) { return lf.MONTH_ABBRS[date.getMonth()]; }); break;
1176       case 'mmmm':    handlers.push(function(date, lf) { return (lf.MONTH_NAMES || ProtoCalendar.DateFormat)[date.getMonth()]; }); break;
1177       case 'yy':      handlers.push(function(date, lf) { return String(date.getFullYear()).substr(2); }); break;
1178       case 'yyyy':    handlers.push(function(date, lf) { return date.getFullYear(); }); break;
1179       case 'h':       handlers.push(function(date, lf) { return date.getHours() % 12 || 12; }); break;
1180       case 'hh':      handlers.push(function(date, lf) { return ProtoCalendar.DateFormat.zeroize(date.getHours() % 12 || 12); }); break;
1181       case 'H':       handlers.push(function(date, lf) { return date.getHours(); }); break;
1182       case 'HH':      handlers.push(function(date, lf) { return ProtoCalendar.DateFormat.zeroize(date.getHours()); }); break;
1183       case 'M':       handlers.push(function(date, lf) { return date.getMinutes(); }); break;
1184       case 'MM':      handlers.push(function(date, lf) { return ProtoCalendar.DateFormat.zeroize(date.getMinutes()); }); break;
1185       case 's':       handlers.push(function(date, lf) { return date.getSeconds(); }); break;
1186       case 'ss':      handlers.push(function(date, lf) { return ProtoCalendar.DateFormat.zeroize(date.getSeconds()); }); break;
1187       case 'l':       handlers.push(function(date, lf) { return ProtoCalendar.DateFormat.zeroize(date.getMilliseconds(), 3); }); break;
1188       case 'tt':      handlers.push(function(date, lf) { return date.getHours() < 12 ? 'am' : 'pm'; }); break;
1189       case 'TT':      handlers.push(function(date, lf) { return date.getHours() < 12 ? 'AM' : 'PM'; }); break;
1190       default:        handlers.push(ProtoCalendar.createIdentity(matches[i]));
1191       }
1192     };
1193     this.formatHandlers = handlers;
1194     this.formatterInited = true;
1195   },
1196
1197   parse: function(str) {
1198     if (!this.parserInited) this.initParser();
1199     if (!str) return undefined;
1200     var results = str.match(this.parserRegexp);
1201     if (!results) return undefined;
1202     var date = ProtoCalendar.newDate();
1203     for (var i = 0, n = this.parseHandlers.length; i < n; i++) {
1204       if (this.parseHandlers[i] != undefined) {
1205         (this.parseHandlers[i])(date, results[i+1]);
1206       }
1207     }
1208     this.parseCallback(date);
1209     return date;
1210   },
1211
1212   initParser: function() {
1213     var handlers = [];
1214     var regstr = '';
1215     var matches = this.dateFormat.match(ProtoCalendar.DateFormat.formatRegexp);
1216     var hour, ampm;
1217
1218     for (var i = 0, n = matches.length; i < n; i++) {
1219       regstr += '(';
1220       switch(matches[i]) {
1221       case 'd':
1222       case 'dd':      regstr += '\\d{1,2}';
1223                       handlers.push(function(date, value) { date.setDate(value); });
1224                       break;
1225       case 'm':
1226       case 'mm':      regstr += '\\d{1,2}';
1227                       handlers.push(function(date, value) { 
1228                                       var m = parseInt(value, 10) - 1;
1229                                       date.setMonth(m); 
1230                                     });
1231                       break;
1232 //       case 'mmm':     regstr += ProtoCalendar.DateFormat.MONTH_ABBRS.join('|');
1233 //                       handlers.push(function(date, value) {
1234 //                                       date.setMonth(ProtoCalendar.DateFormat.MONTH_ABBRS.indexOf(value)); });
1235 //                       break;
1236 //       case 'mmmm':    regstr += ProtoCalendar.DateFormat.MONTH_NAMES.join('|');
1237 //                       handlers.push(function(date, value) {
1238 //                                       date.setMonth(ProtoCalendar.DateFormat.MONTH_NAMES.indexOf(value)); });
1239 //                       break;
1240       case 'yy':      regstr += '\\d{2}';
1241                       handlers.push(function(date, value) {
1242                                       var year = parseInt(value, 10);
1243                                       year = year < 70 ? 2000 + year : 1900 + year;
1244                                       date.setFullYear(year); });
1245                       break;
1246       case 'yyyy':    regstr += '\\d{4}';
1247                       handlers.push(function(date, value) { date.setFullYear(value); });
1248                       break;
1249       case 'h':
1250       case 'hh':      hour = true;
1251                       regstr += '\\d{1,2}';
1252                       handlers.push(function(date, value) {
1253                                       value = value % 12 || 0;
1254                                       date.setHours(value);
1255                                       });
1256                       break;
1257       case 'H':
1258       case 'HH':      regstr += '\\d{1,2}';
1259                       handlers.push(function(date, value) { date.setHours(value); });
1260                       break;
1261       case 'M':
1262       case 'MM':      regstr += '\\d{1,2}';
1263                       handlers.push(function(date, value) { date.setMinutes(value); });
1264                       break;
1265       case 's':
1266       case 'ss':      regstr += '\\d{1,2}';
1267                       handlers.push(function(date, value) { date.setSeconds(value); });
1268                       break;
1269       case 'l':       regstr += '\\d{1,3}';
1270                       handlers.push(function(date, value) { date.setMilliSeconds(value); });
1271                       break;
1272       case 'tt':      regstr += 'am|pm';
1273                       handlers.push(function(date, value) { ampm = value; });
1274                       break;
1275       case 'TT':      regstr += 'AM|PM';
1276                       handlers.push(function(date, value) { ampm = value.toLowerCase(); });
1277                       break;
1278       case 'mmm':
1279       case 'mmmm':
1280       case 'ddd':
1281       case 'dddd':
1282       case 'dddi':
1283       case 'ddddi':   regstr += '.+?';
1284                       handlers.push(undefined);
1285                       break;
1286
1287       default:        regstr += matches[i];
1288                       handlers.push(undefined);
1289       }
1290       regstr += ')';
1291     }
1292     this.parserRegexp = new RegExp(regstr);
1293     this.parseHandlers = handlers;
1294
1295     if (ampm == 'pm' && hour) {
1296       this.parseCallback = this.normalizeHour.bind(this);
1297     } else {
1298       this.parseCallback = function() {};
1299     }
1300     this.parserInited = true;
1301   },
1302
1303   normalizeHour: function(date) {
1304     var hour = date.getHours();
1305     hour = hour == 12 ? 0 : hour + 12;
1306     date.setHours(hour);
1307   }
1308 };
1309
1310 ProtoCalendar.createIdentity = function(v) {
1311   return function() { return v; }
1312 }
1313
1314 var SelectCalendar = Class.create();
1315 SelectCalendar.selectTimeOption = function(select, value) {
1316   var newValue = value - 0;
1317   newValue = newValue < 10 ? "0" + newValue : newValue;
1318   SelectCalendar.selectOption(select, newValue);
1319 }
1320
1321 SelectCalendar.selectOption = function(select, value) {
1322   var selectEl = $(select);
1323   var options = selectEl.options;
1324   for (var i = 0; i < options.length; i++) {
1325     if (options[i].value === value.toString()) {
1326       options[i].selected = true;
1327       return;
1328     }
1329   }
1330 }
1331
1332 SelectCalendar.createOnLoaded = function(select, options) {
1333   BaseCalendar.bindOnLoad(function() { new SelectCalendar(select, options); });
1334 };
1335 Object.extend(SelectCalendar.prototype, BaseCalendar.prototype);
1336 Object.extend(
1337   SelectCalendar.prototype,
1338   {
1339     initialize: function(select, options) {
1340       this.yearSelect = $(select.yearSelect);
1341       this.monthSelect = $(select.monthSelect);
1342       this.daySelect = $(select.daySelect);
1343       this.initializeOptions(options);
1344       if (this.options.enableHourMinute) {
1345         this.hourSelect = $(select.hourSelect);
1346         this.minuteSelect = $(select.minuteSelect);
1347         if (this.options.enableSecond) {
1348           this.secondSelect = $(select.secondSelect);
1349         }
1350       }
1351       this.options = Object.extend({alignTo: select.yearSelect}, this.options);
1352       this.initializeBase();
1353       this.initializeSelect();
1354     },
1355
1356     initializeSelect: function() {
1357       if (this.getSelectedDate()) {
1358         this.onSelectChange();
1359       } else {
1360         this.onCalendarChange();
1361       }
1362     },
1363
1364     doObserveEvents: function() {
1365       this.yearSelect.observe('change', this.onSelectChange.bind(this));
1366       this.monthSelect.observe('change', this.onSelectChange.bind(this));
1367       this.daySelect.observe('change', this.onSelectChange.bind(this));
1368       if (this.options.enableHourMinute) {
1369         this.hourSelect.observe('change', this.onSelectChange.bind(this));
1370         this.minuteSelect.observe('change', this.onSelectChange.bind(this));
1371         if (this.options.enableSecond) {
1372           this.secondSelect.observe('change', this.onSelectChange.bind(this));
1373         }
1374       }
1375       this.addChangeHandler(this.changeSelectValue.bind(this));
1376     },
1377
1378     onSelectChange: function() {
1379       var date = this.getSelectedDate();
1380       if (!date) return;
1381       this.calendarController.selectDate(date, true);
1382       if (this.options.enableHourMinute) {
1383         this.calendarController.calendarRender.selectTime(date);
1384       }
1385       this.onCalendarChange();
1386     },
1387
1388     changeSelectValue: function() {
1389       var date = this.calendarController.getSelectedDate();
1390       if (date) {
1391         SelectCalendar.selectOption(this.yearSelect, date.getFullYear());
1392         SelectCalendar.selectOption(this.monthSelect, date.getMonth() + 1);
1393         SelectCalendar.selectOption(this.daySelect, date.getDate());
1394         if (this.options.enableHourMinute) {
1395           SelectCalendar.selectTimeOption(this.hourSelect, date.getHours());
1396           SelectCalendar.selectTimeOption(this.minuteSelect, date.getMinutes());
1397           if (this.options.enableSecond) {
1398             SelectCalendar.selectTimeOption(this.secondSelect, date.getSeconds());
1399           }
1400         }
1401       }
1402     },
1403
1404     getSelectedDate: function() {
1405       if (this.yearSelect.value == '' 
1406           || this.monthSelect.value == '' 
1407           || this.daySelect.value == '') {
1408         return undefined;
1409       }
1410       var d = ProtoCalendar.newDate();
1411       d.setFullYear(this.yearSelect.value);
1412       d.setMonth(this.monthSelect.value - 1);
1413       d.setDate(this.daySelect.value);
1414       if (this.options.enableHourMinute) {
1415         if (this.hourSelect.value == '' || this.minuteSelect.value == '') {
1416           return undefined;
1417         }
1418         d.setHours(this.hourSelect.value - 0);
1419         d.setMinutes(this.minuteSelect.value - 0);
1420         if (this.options.enableSecond) {
1421           if (this.secondSelect.value == '') {
1422             return undefined;
1423           }
1424           d.setSeconds(this.secondSelect.value - 0);
1425         }
1426       }
1427       if (isNaN(d.getTime())) {
1428         return undefined;
1429       } else {
1430         return d;
1431       }
1432     }
1433   });
1434