4 * License : MIT-style license.
5 * Web site: http://labs.spookies.jp/product/protocalendar
7 * protocalendar.js - depends on prototype.js 1.6 or later
8 * http://www.prototypejs.org/
10 /*--------------------------------------------------------------------------*/
12 var ProtoCalendar = Class.create();
13 ProtoCalendar.Version = "1.1.8.2";
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.',
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'],
26 WEEKDAY_ABBRS: ['Sun','Mon','Tue','Wed','Thr','Fri','Sat'],
27 WEEKDAY_NAMES: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
31 ProtoCalendar.LangFile.defaultLang = 'en';
32 ProtoCalendar.LangFile.defaultLangFile = function() { return ProtoCalendar.LangFile[defaultLang]; };
34 ProtoCalendar.newDate = function() {
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());
51 ProtoCalendar.callWithVisibility = function(element, func) {
53 var display = $(element).getStyle('display');
54 if (display != 'none' && display != null) {// Safari bug
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';
65 els.display = originalDisplay;
66 els.position = originalPosition;
67 els.visibility = originalVisibility;
71 Object.extend(ProtoCalendar, {
93 getNumDayOfMonth: function(year, month){
94 return 32 - new Date(year, month, 32).getDate();
97 getDayOfWeek: function(year, month, day) {
98 return new Date(year, month, day).getDay();
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
110 var getHolidays = ProtoCalendar.LangFile[this.options.lang]['getHolidays'];
112 this.initializeHolidays = getHolidays.bind(top, this);
114 this.initializeHolidays = function() { this.holidays = []; };
116 this.date = new Date(this.options.year, this.options.month, 1);
119 getMonth: function() {
120 return this.date.getMonth();
123 getYear: function() {
124 return this.date.getFullYear();
127 invalidate: function() {
128 this.holidays = undefined;
131 setMonth: function(month) {
132 if (month != this.getMonth()) {
135 return this.date.setMonth(month);
138 setYear: function(year) {
139 if (year != this.getYear()) {
142 return this.date.setFullYear(year);
145 getDate: function() {
149 setDate: function(date) {
154 setYearByOffset: function(offset) {
158 this.date.setFullYear(this.date.getFullYear() + offset);
161 setMonthByOffset: function(offset) {
165 this.date.setMonth(this.date.getMonth() + offset);
168 getNumDayOfMonth: function() {
169 return ProtoCalendar.getNumDayOfMonth(this.getYear(), this.getMonth());
172 getDayOfWeek: function(day) {
173 return ProtoCalendar.getDayOfWeek(this.getYear(), this.getMonth(), day);
177 return new ProtoCalendar({year: this.getYear(), month: this.getMonth()});
180 getHoliday: function(day) {
181 if(!this.holidays) { this.initializeHolidays();}
182 var holiday = this.holidays[day];
183 return holiday? holiday : false;
186 initializeHolidays: function() {
190 var AbstractProtoCalendarRender = Class.create();
191 Object.extend(AbstractProtoCalendarRender, {
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 ],
199 var id = AbstractProtoCalendarRender.id;
200 AbstractProtoCalendarRender.id += 1;
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',
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',
258 ifInvisible: 'Flip', /* None | Scroll | Flip */
261 this.langFile = ProtoCalendar.LangFile[this.options.lang];
262 this.weekFirstDay = this.options.weekFirstDay;
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();
270 this.resizeHandler = this.setPosition.bind(this);
273 createContainer: function() {
274 var container = $(document.createElement('div'));
275 container.addClassName(this.options.containerClass);
276 container.setStyle({position:'absolute',
282 document.body.appendChild(container);
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',
297 filter: 'progid:DXImageTransform.Microsoft.Alpha(opacity=0)'
299 document.body.appendChild(iframe);
303 getWeekdayLabel: function(weekday) {
304 return this.langFile.WEEKDAY_ABBRS[weekday];
307 getWeekdays: function() {
308 return this.weekdays;
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;
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;
324 getCalendarBeginDay: function(calendar) {
325 var offset = this.getDayIndexOfWeek(calendar, 1);
326 var date = new Date(calendar.getYear(), calendar.getMonth(), 1 - offset);
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);
337 getDayIndexOfWeek: function(calendar, day) {
338 return this.weekdaysIndex[ calendar.getDayOfWeek(day) ];
341 getIdPrefix: function() {
342 return 'cal' + this.id;
345 getDayDivId: function(date) {
346 return this.getIdPrefix() + '-year' + date.getFullYear() + '-month' + date.getMonth() + '-day' + date.getDate();
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);
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);
363 if (this.options.ifInvisible == 'Scroll') this.scrollIfInvisible();
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"});
380 show: function(option) {
381 Event.observe(window, 'resize', this.resizeHandler);
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});
389 this.container.show();
391 if (this.iframe) this.iframe.show();
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();
404 window.scrollBy(0, diff + this.options.scrollMargin);
408 hide: function(option) {
409 Event.stopObserving(window, 'resize', this.resizeHandler);
410 if (!this.container.visible()) {
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});
419 this.container.hide();
421 if (this.iframe) this.iframe.hide();
424 hideImmediately: function(option) {
425 if (!this.container.visible()) {
428 this.container.hide();
429 if (this.iframe) this.iframe.hide();
432 toggle: function(element) {
433 this.container.visible() ? this.hide() : this.show();
436 render: function(calendar) { },
438 rerender: function(calendar) { },
440 getContainer: function() {
441 return this.container;
444 getPrevButton: function() {
445 return $(this.options.prevBtnId);
448 getNextButton: function() {
449 return $(this.options.nextBtnId);
452 getYearSelect: function() {
453 return $(this.options.yearSelectId);
456 getMonthSelect: function() {
457 return $(this.options.monthSelectId);
460 getHourInput: function() {
461 return $(this.options.hourInputId);
464 getMinuteInput: function() {
465 return $(this.options.minuteInputId);
468 getSecondInput: function() {
469 return $(this.options.secondInputId);
472 getOkButton: function() {
473 return $(this.options.okButtonId);
476 getBody: function() {
477 return $(this.options.bodyId);
480 getDayDivs: function() {
481 //Good Performance for IE
483 var dayDivs = this.dayDivs;
484 for (var i = 0; i < dayDivs.length; i++) {
485 divEls.push(document.getElementById(dayDivs[i]));
488 //return this.container.getElementsBySelector("a." + this.options.dayClass);
491 getDateFromEl: function(el) {
493 return new Date(element.readAttribute('year'), element.readAttribute('month'), element.readAttribute('day'));
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;
507 selectDate: function(date) {
508 var dayEl = $(this.getDayDivId(date));
509 if (dayEl) dayEl.addClassName(this.options.selectedDayClass);
512 selectTime: function(date) {
514 this.getHourInput().value = date.getHours();
515 this.getMinuteInput().value = date.getMinutes();
516 if (this.options.enableSecond) this.getSecondInput().value = date.getSeconds();
519 deselectDate: function(date) {
521 var dateEl = $(this.getDayDivId(date));
522 if (dateEl) dateEl.removeClassName(this.options.selectedDayClass);
526 evaluateWithOptions: function(html) {
527 var template = new Template(html);
528 return template.evaluate(this.options);
531 defaultOnError: function(msg) {
532 this.ensureErrorDiv();
533 this.errorDiv.show();
534 this.errorDiv.innerHTML += '<li>' + this.langFile[msg] + '</li>';
537 hideError: function() {
538 this.ensureErrorDiv();
539 this.errorDiv.innerHTML = '';
540 this.errorDiv.hide();
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);
551 isSelectable: function(date) {
552 return (this.options.minDate - date) <= 0 && (date - this.options.maxDate) <= 0;
557 var ProtoCalendarRender = Class.create();
558 Object.extend(ProtoCalendarRender.prototype, AbstractProtoCalendarRender.prototype);
561 ProtoCalendarRender.prototype,
563 render: function(calendar) {
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);
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();
581 renderHeader: function(calendar) {
584 html += '<div class="#{headerTopClass}"></div><div class="#{headerClass}">' +
585 '<a href="#" id="#{prevBtnId}" class="#{prevButtonClass}"><<</a>' +
586 this.createSelect(calendar.getYear(), calendar.getMonth()) +
587 '<a href="#" id="#{nextBtnId}" class="#{nextButtonClass}">>></a>' +
588 '</div><div class="#{headerBottomClass}"></div>';
592 renderFooter: function(calendar) {
593 return '<div class="#{footerTopClass}"></div><div class="#{footerClass}" id="#{footerId}"></div><div class="#{footerBottomClass}"></div>';
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}"/>';
604 html += '<input type="button" value="' + this.langFile['OK_LABEL'] + '" class="#{okButtonClass}" name="ok_button" id="#{okButtonId}"/>';
606 this.hourMinuteHtml = html;
608 return this.hourMinuteHtml;
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;
617 return monthPart + yearPart;
621 createYearSelect: function(year) {
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>';
631 createMonthSelect: function(month) {
632 if (!this.monthSelectHtml) {
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>';
639 this.monthSelectHtml = html;
641 return this.monthSelectHtml;
644 renderBody: function(calendar) {
646 var html = '<table class="#{tableClass}" cellspacing="0">';
647 html += '<tr class="#{labelRowClass}">';
649 if (!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) +
660 html += this.headHtml;
661 var curDay = this.getCalendarBeginDay(calendar);
662 var calEndDay = this.getCalendarEndDay(calendar);
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)) {
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;
677 divClassName = this.options.weekdayClass;
680 if (curDay.getDay() == this.weekFirstDay) { html += '<tr>'; }
681 var dayId = this.getDayDivId(curDay);
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);
692 divClassName += ' ' + this.options.disabledDayClass;
693 dayHtml = curDay.getDate();
695 html += '<td class="' + divClassName + ' #{dayCellClass}">' + dayHtml + '</td>';
696 if (curDay.getDay() == this.weekLastDay) { html += '</tr>'; }
698 html += '</tbody></table>';
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)
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);
720 this.selectDate(date, true);
722 this.selectDate(null);
724 this.observeEventsOnce();
725 this.observeEvents();
726 this.onChangeHandlers = [];
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();
735 this.options.year = this.options.startYear;
738 if (!this.options.month) {
739 this.options.month = date.getMonth();
741 if (!this.options.day) {
742 this.options.day = date.getDate();
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));
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));
758 monthSelect.observe('change', function() {
759 othis.setMonth(parseInt(yearSelect[yearSelect.selectedIndex].value, 10), parseInt(monthSelect[monthSelect.selectedIndex].value, 10));
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'});
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'});
786 if (this.options.enableHourMinute) calrndr.getOkButton().observe('click', this.onSubmit.bind(this));
788 _disableContextMenu: function(event) {
792 _disablePaste: function(event) {
793 // ctrl + v || shift + insert
794 if ((event.keyCode == 86 && event.ctrlKey) || (event.keyCode == 45 && event.shiftKey)) {
799 _autoFocus: function(event, nextEl) {
801 if (event.keyCode == 16 || event.keyCode == 9 || (event.keyCode == 9 && event.shiftKey)) {
806 if (v.length && v.length == 2) {
812 observeEvents: function() {
814 this.calendarRender.getDayDivs().each(function(el) {
815 Event.observe(el, 'click', othis.onClickHandler.bindAsEventListener(othis));
819 onClickHandler: function(event) {
821 var date = this.calendarRender.getDateFromEl(Event.element(event));
823 this.selectDate(date);
824 if (!this.options.enableHourMinute) {
825 this.onChangeHandler();
826 setTimeout(this.hideCalendar.bind(this), 150);
831 onSubmit: function() {
833 var date = this.selectedDate;
834 if (!date) return this.options.onNoDateError();
835 date = this.calendarRender.injectHourMinute(date);
837 this.options.onHourMinuteError();
839 this.selectDate(date, true);
840 if (this.options.enableHourMinute) this.calendarRender.selectTime(date);
841 this.onChangeHandler();
846 selectDate: function(date, redraw) {
847 this.calendarRender.deselectDate(this.selectedDate);
848 this.selectedDate = date;
850 if (redraw && (date.getFullYear() != this.calendar.getYear() || date.getMonth() != this.calendar.getMonth())) {
851 this.setMonth(date.getFullYear(), date.getMonth());
853 this.calendarRender.selectDate(this.selectedDate);
856 getSelectedDate: function() {
857 return this.selectedDate;
860 addChangeHandler: function(func) {
861 this.onChangeHandlers.push(func);
864 onChangeHandler: function() {
865 this.onChangeHandlers.each(function(f) { f(); });
868 showCalendar: function() {
869 this.calendarRender.show();
872 hideCalendar: function() {
873 this.calendarRender.hide();
876 blurCalendar: function(event) {
877 if (event.keyCode == 9) {
878 this.hideImmediatelyCalendar();
882 hideImmediatelyCalendar: function() {
883 this.calendarRender.hideImmediately();
886 toggleCalendar: function() {
887 this.calendarRender.toggle();
890 showPrevMonth: function(event) {
891 this.shiftMonthByOffset(-1);
892 if (event) Event.stop(event);
895 showNextMonth: function(event) {
896 this.shiftMonthByOffset(1);
897 if (event) Event.stop(event);
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);
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);
916 afterSet: function() {
917 this.calendarRender.rerender(this.calendar);
918 this.selectDate(this.selectedDate);
919 this.observeEvents();
922 getContainer: function() {
923 return this.calendarRender.getContainer();
926 defaultOnHourMinuteError: function() {
927 this.calendarRender.defaultOnError('HOUR_MINUTE_ERROR');
930 defaultOnNoDateError: function() {
931 this.calendarRender.defaultOnError('NO_DATE_ERROR');
934 hideError: function() {
935 this.calendarRender.hideError();
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);
945 Event.observe(window, 'load', f);
950 BaseCalendar.prototype = {
951 initialize: function(options) {
952 throw "Cannot instantiate BaseCalendar.";
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,
965 lang: ProtoCalendar.LangFile.defaultLang,
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();
974 this.options.triggers.each(this.addTrigger.bind(this));
975 this.changeHandlers = [];
976 this.observeEvents();
979 addTrigger: function(el) {
980 this.triggers.push($(el));
981 $(el).setStyle({'cursor': 'pointer'});
984 observeEvents: function() {
985 Event.observe(document.body, 'click', this.windowClickHandler.bindAsEventListener(this));
986 this.calendarController.addChangeHandler(this.onCalendarChange.bind(this));
987 this.doObserveEvents();
990 doObserveEvents: function() {
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();
1003 addChangeHandler: function(f) {
1004 this.changeHandlers.push(f);
1007 onCalendarChange: function() {
1008 this.changeHandlers.each(function(f) { f(); });
1012 var InputCalendar = Class.create();
1013 InputCalendar.createOnLoaded = function(input, options) {
1014 BaseCalendar.bindOnLoad(function() {
1015 new InputCalendar(input, options);
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);
1026 Event.observe(window, 'load', function() {
1027 $$(inputs).each(function(input) {
1028 new InputCalendar(input, options);
1033 Object.extend(InputCalendar.prototype, BaseCalendar.prototype);
1035 InputCalendar.prototype,
1037 initialize: function(input, options) {
1038 this.input = $(input); // used in doObserveEvents()
1039 this.initializeOptions(options);
1040 this.options = Object.extend({
1042 inputReadOnly: false,
1043 labelFormat: undefined,
1046 this.initializeBase();
1047 this.initializeInput();
1048 this.initializeLabel();
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();
1056 this.onCalendarChange();
1058 if (this.options.enableHourMinute) {
1059 this.calendarController.calendarRender.selectTime(this.calendarController.selectedDate);
1061 if (this.options.inputReadOnly) {
1062 this.input.setAttribute('readOnly', this.options.inputReadOnly);
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);
1074 this.labelEl = labelElm;
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);
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));
1093 onInputChange: function() {
1094 var date = this.dateFormat.parse(this.input.value);
1096 this.calendarController.selectDate(date, true);
1097 if (this.options.enableHourMinute) this.calendarController.calendarRender.selectTime(date);
1099 var inputValue = this.input.value.toLowerCase();
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))) {
1114 this.calendarController.selectDate(date, true);
1115 this.onCalendarChange();
1120 changeInputValue: function() {
1121 this.input.value = this.dateFormat.format(this.calendarController.getSelectedDate(), this.options.lang);
1126 ProtoCalendar.DateFormat = Class.create();
1127 Object.extend(ProtoCalendar.DateFormat,
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++) {
1140 return zeros + value;
1144 ProtoCalendar.DateFormat.prototype = {
1145 initialize: function(format) {
1146 this.dateFormat = format;
1147 this.parserInited = false;
1148 this.formatterInited = false;
1151 format: function(date, lang) {
1152 if (!this.formatterInited) this.initFormatter();
1153 if (!date) return '';
1154 var langFile = ProtoCalendar.LangFile[lang || ProtoCalendar.LangFile.defaultLang];
1156 this.formatHandlers.each(function(f) {
1157 str += f(date, langFile);
1162 initFormatter: function() {
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]));
1193 this.formatHandlers = handlers;
1194 this.formatterInited = true;
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]);
1208 this.parseCallback(date);
1212 initParser: function() {
1215 var matches = this.dateFormat.match(ProtoCalendar.DateFormat.formatRegexp);
1218 for (var i = 0, n = matches.length; i < n; i++) {
1220 switch(matches[i]) {
1222 case 'dd': regstr += '\\d{1,2}';
1223 handlers.push(function(date, value) { date.setDate(value); });
1226 case 'mm': regstr += '\\d{1,2}';
1227 handlers.push(function(date, value) {
1228 var m = parseInt(value, 10) - 1;
1232 // case 'mmm': regstr += ProtoCalendar.DateFormat.MONTH_ABBRS.join('|');
1233 // handlers.push(function(date, value) {
1234 // date.setMonth(ProtoCalendar.DateFormat.MONTH_ABBRS.indexOf(value)); });
1236 // case 'mmmm': regstr += ProtoCalendar.DateFormat.MONTH_NAMES.join('|');
1237 // handlers.push(function(date, value) {
1238 // date.setMonth(ProtoCalendar.DateFormat.MONTH_NAMES.indexOf(value)); });
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); });
1246 case 'yyyy': regstr += '\\d{4}';
1247 handlers.push(function(date, value) { date.setFullYear(value); });
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);
1258 case 'HH': regstr += '\\d{1,2}';
1259 handlers.push(function(date, value) { date.setHours(value); });
1262 case 'MM': regstr += '\\d{1,2}';
1263 handlers.push(function(date, value) { date.setMinutes(value); });
1266 case 'ss': regstr += '\\d{1,2}';
1267 handlers.push(function(date, value) { date.setSeconds(value); });
1269 case 'l': regstr += '\\d{1,3}';
1270 handlers.push(function(date, value) { date.setMilliSeconds(value); });
1272 case 'tt': regstr += 'am|pm';
1273 handlers.push(function(date, value) { ampm = value; });
1275 case 'TT': regstr += 'AM|PM';
1276 handlers.push(function(date, value) { ampm = value.toLowerCase(); });
1283 case 'ddddi': regstr += '.+?';
1284 handlers.push(undefined);
1287 default: regstr += matches[i];
1288 handlers.push(undefined);
1292 this.parserRegexp = new RegExp(regstr);
1293 this.parseHandlers = handlers;
1295 if (ampm == 'pm' && hour) {
1296 this.parseCallback = this.normalizeHour.bind(this);
1298 this.parseCallback = function() {};
1300 this.parserInited = true;
1303 normalizeHour: function(date) {
1304 var hour = date.getHours();
1305 hour = hour == 12 ? 0 : hour + 12;
1306 date.setHours(hour);
1310 ProtoCalendar.createIdentity = function(v) {
1311 return function() { return v; }
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);
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;
1332 SelectCalendar.createOnLoaded = function(select, options) {
1333 BaseCalendar.bindOnLoad(function() { new SelectCalendar(select, options); });
1335 Object.extend(SelectCalendar.prototype, BaseCalendar.prototype);
1337 SelectCalendar.prototype,
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);
1351 this.options = Object.extend({alignTo: select.yearSelect}, this.options);
1352 this.initializeBase();
1353 this.initializeSelect();
1356 initializeSelect: function() {
1357 if (this.getSelectedDate()) {
1358 this.onSelectChange();
1360 this.onCalendarChange();
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));
1375 this.addChangeHandler(this.changeSelectValue.bind(this));
1378 onSelectChange: function() {
1379 var date = this.getSelectedDate();
1381 this.calendarController.selectDate(date, true);
1382 if (this.options.enableHourMinute) {
1383 this.calendarController.calendarRender.selectTime(date);
1385 this.onCalendarChange();
1388 changeSelectValue: function() {
1389 var date = this.calendarController.getSelectedDate();
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());
1404 getSelectedDate: function() {
1405 if (this.yearSelect.value == ''
1406 || this.monthSelect.value == ''
1407 || this.daySelect.value == '') {
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 == '') {
1418 d.setHours(this.hourSelect.value - 0);
1419 d.setMinutes(this.minuteSelect.value - 0);
1420 if (this.options.enableSecond) {
1421 if (this.secondSelect.value == '') {
1424 d.setSeconds(this.secondSelect.value - 0);
1427 if (isNaN(d.getTime())) {