/** * The calendar widget implemented with RightJS * * Home page: http://rightjs.org/ui/calendar * * @copyright (C) 2009 Nikolay V. Nemshilov aka St. */ if (!RightJS) { throw "Gimme RightJS. Please." }; /** * The calendar widget for RightJS * * * Copyright (C) 2009 Nikolay V. Nemshilov aka St. */ var Calendar = new Class(Observer, { extend: { EVENTS: $w('show hide select done'), Options: { format: 'ISO', // a key out of the predefined formats or a format string showTime: null, // null for automatic, or true|false to enforce showButtons: false, minDate: null, maxDate: null, firstDay: 1, // 1 for Monday, 0 for Sunday fxName: 'fade', // set to null if you don't wanna any fx fxDuration: 200, numberOfMonths: 1, // a number or [x, y] greed definition timePeriod: 1, // the timepicker minimal periods (in minutes, might be bigger than 60) checkTags: '*', relName: 'calendar', twentyFourHour: null, // null for automatic, or true|false to enforce listYears: false // show/hide the years listing buttons }, Formats: { ISO: '%Y-%m-%d', POSIX: '%Y/%m/%d', EUR: '%d-%m-%Y', US: '%m/%d/%Y' }, i18n: { Done: 'Done', Now: 'Now', Next: 'Next Month', Prev: 'Previous Month', NextYear: 'Next Year', PrevYear: 'Prev Year', dayNames: $w('Sunday Monday Tuesday Wednesday Thursday Friday Saturday'), dayNamesShort: $w('Sun Mon Tue Wed Thu Fri Sat'), dayNamesMin: $w('Su Mo Tu We Th Fr Sa'), monthNames: $w('January February March April May June July August September October November December'), monthNamesShort: $w('Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec') }, // scans for the auto-discoverable calendar inputs rescan: function() { var key = Calendar.Options.relName; var rel_id_re = new RegExp(key+'\\[(.+?)\\]'); $$(Calendar.Options.checkTags+'[rel*='+key+']').each(function(element) { var data = element.get('data-'+key+'-options'); var calendar = new Calendar(eval('('+data+')') || {}); var rel_id = element.get('rel').match(rel_id_re); if (rel_id) { var input = $(rel_id[1]); if (input) { calendar.assignTo(input, element); } } else { calendar.assignTo(element); } }); } }, /** * Basic constructor * * @param Object options */ initialize: function(options) { this.$super(options); this.element = $E('div', {'class': 'right-calendar'}); this.build().connectEvents().setDate(new Date()); }, /** * additional options processing * * @param Object options * @return Calendar this */ setOptions: function(user_options) { this.$super(user_options); var klass = this.constructor; var options = this.options; with (this.options) { // merging the i18n tables options.i18n = {}; for (var key in klass.i18n) { i18n[key] = isArray(klass.i18n[key]) ? klass.i18n[key].clone() : klass.i18n[key]; } $ext(i18n, (user_options || {}).i18n); // defining the current days sequence options.dayNames = i18n.dayNamesMin; if (firstDay) { dayNames.push(dayNames.shift()); } // the monthes table cleaning up if (!isArray(numberOfMonths)) { numberOfMonths = [numberOfMonths, 1]; } // min/max dates preprocessing if (minDate) minDate = this.parse(minDate); if (maxDate) { maxDate = this.parse(maxDate); maxDate.setDate(maxDate.getDate() + 1); } // format catching up format = (klass.Formats[format] || format).trim(); // setting up the showTime option if (showTime === null) { showTime = format.search(/%[HkIl]/) > -1; } // setting up the 24-hours format if (twentyFourHour === null) { twentyFourHour = format.search(/%[Il]/) < 0; } // enforcing the 24 hours format if the time threshold is some weird number if (timePeriod > 60 && 12 % (timePeriod/60).ceil()) { twentyFourHour = true; } } return this; }, /** * Sets the date on the calendar * * @param Date date or String date * @return Calendar this */ setDate: function(date) { this.date = this.prevDate = this.parse(date); return this.update(); }, /** * Returns the current date on the calendar * * @return Date currently selected date on the calendar */ getDate: function() { return this.date; }, /** * Hides the calendar * * @return Calendar this */ hide: function() { this.element.hide(this.options.fxName, {duration: this.options.fxDuration}); Calendar.current = null; return this; }, /** * Shows the calendar * * @param Object {x,y} optional position * @return Calendar this */ show: function(position) { this.element.show(this.options.fxName, {duration: this.options.fxDuration}); Calendar.current = this; return this; }, /** * Inserts the calendar into the element making it inlined * * @param Element element or String element id * @param String optional position top/bottom/before/after/instead, 'bottom' is default * @return Calendar this */ insertTo: function(element, position) { this.element.addClass('right-calendar-inline').insertTo(element, position); return this; } }); /** * This module handles the calendar elemnts building/updating processes * * Copyright (C) 2009 Nikolay V. Nemshilov aka St. */ Calendar.include({ // protected // updates the calendar view update: function(date) { var date = new Date(date || this.date), options = this.options; var monthes = this.element.select('div.right-calendar-month'); var monthes_num = monthes.length; for (var i=-(monthes_num - monthes_num/2).ceil()+1; i < (monthes_num - monthes_num/2).floor()+1; i++) { var month_date = new Date(date); month_date.setMonth(date.getMonth() + i); this.updateMonth(monthes.shift(), month_date); } this.updateNextPrevMonthButtons(date, monthes_num); if (options.showTime) { this.hours.value = options.timePeriod < 60 ? date.getHours() : (date.getHours()/(options.timePeriod/60)).round() * (options.timePeriod/60); this.minutes.value = (date.getMinutes() / (options.timePeriod % 60)).round() * options.timePeriod; } return this; }, // updates a single month-block with the given date updateMonth: function(element, date) { // getting the number of days in the month date.setDate(32); var days_number = 32 - date.getDate(); date.setMonth(date.getMonth()-1); var cur_day = (this.date.getTime() / 86400000).ceil(); // collecting the elements to update var rows = element.select('tbody tr'); var cells = rows.shift().select('td'); element.select('tbody td').each(function(td) { td.innerHTML = ''; td.className = 'right-calendar-day-blank'; }); var options = this.options; for (var i=1; i <= days_number; i++) { date.setDate(i); var day_num = date.getDay(); if (this.options.firstDay) { day_num = day_num ? day_num-1 : 6; } cells[day_num].innerHTML = ''+i; cells[day_num].className = cur_day == (date.getTime() / 86400000).ceil() ? 'right-calendar-day-selected' : ''; if ((options.minDate && options.minDate > date) || (options.maxDate && options.maxDate < date)) cells[day_num].className = 'right-calendar-day-disabled'; cells[day_num].date = new Date(date); if (day_num == 6) { cells = rows.shift().select('td'); } } var caption = (options.listYears ? options.i18n.monthNamesShort[date.getMonth()] + ',' : options.i18n.monthNames[date.getMonth()])+' '+date.getFullYear(); element.first('div.right-calendar-month-caption').update(caption); }, updateNextPrevMonthButtons: function(date, monthes_num) { var options = this.options; if (options.minDate) { var beginning = new Date(date.getFullYear(),0,1,0,0,0); var min_date = new Date(options.minDate.getFullYear(),0,1,0,0,0); this.hasPrevYear = beginning > min_date; beginning.setMonth(date.getMonth() - (monthes_num - monthes_num/2).ceil()); min_date.setMonth(options.minDate.getMonth()); this.hasPrevMonth = beginning >= min_date; } else { this.hasPrevMonth = this.hasPrevYear = true; } if (options.maxDate) { var end = new Date(date); var max_date = new Date(options.maxDate); [end, max_date].each(function(date) { date.setDate(32); date.setMonth(date.getMonth() - 1); date.setDate(32 - date.getDate()); date.setHours(0); date.setMinutes(0); date.setSeconds(0); date.setMilliseconds(0); }); this.hasNextMonth = end < max_date; // checking the next year [end, max_date].each('setMonth', 0); this.hasNextYear = end < max_date; } else { this.hasNextMonth = this.hasNextYear = true; } this.nextButton[this.hasNextMonth ? 'removeClass':'addClass']('right-ui-button-disabled'); this.prevButton[this.hasPrevMonth ? 'removeClass':'addClass']('right-ui-button-disabled'); if (this.nextYearButton) { this.nextYearButton[this.hasNextYear ? 'removeClass':'addClass']('right-ui-button-disabled'); this.prevYearButton[this.hasPrevYear ? 'removeClass':'addClass']('right-ui-button-disabled'); } }, // builds the calendar build: function() { this.buildSwaps(); // building the calendars greed var greed = tbody = $E('table', {'class': 'right-calendar-greed'}).insertTo(this.element); var options = this.options; if (Browser.OLD) tbody = $E('tbody').insertTo(greed); for (var y=0; y < options.numberOfMonths[1]; y++) { var row = $E('tr').insertTo(tbody); for (var x=0; x < options.numberOfMonths[0]; x++) { $E('td').insertTo(row).insert(this.buildMonth()); } } if (options.showTime) this.buildTime(); this.buildButtons(); return this; }, // builds the monthes swapping buttons buildSwaps: function() { var i18n = this.options.i18n; this.prevButton = $E('div', {'class': 'right-ui-button right-calendar-prev-button', html: '‹', title: i18n.Prev}).insertTo(this.element); this.nextButton = $E('div', {'class': 'right-ui-button right-calendar-next-button', html: '›', title: i18n.Next}).insertTo(this.element); if (this.options.listYears) { this.prevYearButton = $E('div', {'class': 'right-ui-button right-calendar-prev-year-button', html: '«', title: i18n.PrevYear}).insertTo(this.prevButton, 'after'); this.nextYearButton = $E('div', {'class': 'right-ui-button right-calendar-next-year-button', html: '»', title: i18n.NextYear}).insertTo(this.nextButton, 'before'); } }, // builds a month block buildMonth: function() { return $E('div', {'class': 'right-calendar-month'}).insert( '
'+ ''+ this.options.dayNames.map(function(name) {return '';}).join('')+ ''+ '123456'.split('').map(function() {return ''}).join('')+ '
'+name+'
' ); }, // builds the time selection block buildTime: function() { var options = this.options; var time_picker = $E('div', {'class': 'right-calendar-time', html: ':'}).insertTo(this.element); this.hours = $E('select').insertTo(time_picker, 'top'); this.minutes = $E('select').insertTo(time_picker); var minutes_threshold = options.timePeriod < 60 ? options.timePeriod : 60; var hours_threshold = options.timePeriod < 60 ? 1 : (options.timePeriod / 60).ceil(); (60).times(function(i) { var caption = (i < 10 ? '0' : '') + i; if (i < 24 && i % hours_threshold == 0) { if (options.twentyFourHour) this.hours.insert($E('option', {value: i, html: caption})); else if (i < 12) { this.hours.insert($E('option', {value: i, html: i == 0 ? 12 : i})); } } if (i % minutes_threshold == 0) { this.minutes.insert($E('option', {value: i, html: caption})); } }, this); // adding the meridian picker if it's a 12 am|pm picker if (!options.twentyFourHour) { this.meridian = $E('select').insertTo(time_picker); (options.format.includes(/%P/) ? ['am', 'pm'] : ['AM', 'PM']).each(function(value) { this.meridian.insert($E('option', {value: value.toLowerCase(), html: value})); }, this); } }, // builds the bottom buttons block buildButtons: function() { if (!this.options.showButtons) return; this.nowButton = $E('div', {'class': 'right-ui-button right-calendar-now-button', html: this.options.i18n.Now}); this.doneButton = $E('div', {'class': 'right-ui-button right-calendar-done-button', html: this.options.i18n.Done}); $E('div', {'class': 'right-ui-buttons right-calendar-buttons'}) .insert([this.doneButton, this.nowButton]).insertTo(this.element); } }); /** * This module handles the events connection * * Copyright (C) 2009 Nikolay V. Nemshilov aka St. */ // the document keybindings hookup document.onKeydown(function(event) { if (Calendar.current) { var name; switch(event.keyCode) { case 27: name = 'hide'; break; case 37: name = 'prevDay'; break; case 39: name = 'nextDay'; break; case 38: name = 'prevWeek'; break; case 40: name = 'nextWeek'; break; case 34: name = 'nextMonth'; break; case 33: name = 'prevMonth'; break; case 13: Calendar.current.select(Calendar.current.date); name = 'done'; break; } if (name) { Calendar.current[name](); event.stop(); } } }); Calendar.include({ /** * Initiates the 'select' event on the object * * @param Date date * @return Calendar this */ select: function(date) { this.date = date; return this.fire('select', date); }, /** * Covers the 'done' event fire * * @return Calendar this */ done: function() { if (!this.element.hasClass('right-calendar-inline')) this.hide(); return this.fire('done', this.date); }, nextDay: function() { return this.changeDate({'Date': 1}); }, prevDay: function() { return this.changeDate({'Date': -1}); }, nextWeek: function() { return this.changeDate({'Date': 7}); }, prevWeek: function() { return this.changeDate({'Date': -7}); }, nextMonth: function() { return this.changeDate({Month: 1}); }, prevMonth: function() { return this.changeDate({Month: -1}); }, nextYear: function() { return this.changeDate({FullYear: 1}); }, prevYear: function() { return this.changeDate({FullYear: -1}); }, // protected // changes the current date according to the hash changeDate: function(hash) { var date = new Date(this.date); for (var key in hash) { date['set'+key](date['get'+key]() + hash[key]); } // checking the date range constrains if (!( (this.options.minDate && this.options.minDate > date) || (this.options.maxDate && this.options.maxDate < date) )) this.date = date; return this.update(this.date); }, connectEvents: function() { // connecting the monthes swapping this.prevButton.onClick(this.prevMonth.bind(this)); this.nextButton.onClick(this.nextMonth.bind(this)); if (this.nextYearButton) { this.prevYearButton.onClick(this.prevYear.bind(this)); this.nextYearButton.onClick(this.nextYear.bind(this)); } // connecting the calendar day-cells this.element.select('div.right-calendar-month table tbody td').each(function(cell) { cell.onClick(function() { if (cell.innerHTML != '') { var prev = this.element.first('.right-calendar-day-selected'); if (prev) prev.removeClass('right-calendar-day-selected'); cell.addClass('right-calendar-day-selected'); this.setTime(cell.date); } }.bind(this)); }, this); // connecting the time picker events if (this.hours) { this.hours.on('change', this.setTime.bind(this)); this.minutes.on('change', this.setTime.bind(this)); if (!this.options.twentyFourHour) { this.meridian.on('change', this.setTime.bind(this)); } } // connecting the bottom buttons if (this.nowButton) { this.nowButton.onClick(this.setDate.bind(this, new Date())); this.doneButton.onClick(this.done.bind(this)); } // blocking all the events from the element this.element.onClick(function(e) {e.stop();}); return this; }, // sets the date without nucking the time setTime: function(date) { // from clicking a day in a month table if (date instanceof Date) { this.date.setYear(date.getFullYear()); this.date.setMonth(date.getMonth()); this.date.setDate(date.getDate()); } if (this.hours) { this.date.setHours(this.hours.value.toInt() + (!this.options.twentyFourHour && this.meridian.value == 'pm' ? 12 : 0)); this.date.setMinutes(this.minutes.value); } return this.select(this.date); } }); /** * This module handles the calendar assignment to an input field * * Copyright (C) 2009 Nikolay V. Nemshilov aka St. */ Calendar.include({ /** * Assigns the calendar to serve the given input element * * If no trigger element specified, then the calendar will * appear and disappear with the element haveing its focus * * If a trigger element is specified, then the calendar will * appear/disappear only by clicking on the trigger element * * @param Element input field * @param Element optional trigger * @return Calendar this */ assignTo: function(input, trigger) { var input = $(input), trigger = $(trigger); if (trigger) { trigger.onClick(function(e) { e.stop(); this.showAt(input.focus()); }.bind(this)); } else { input.on({ focus: this.showAt.bind(this, input), click: function(e) { e.stop(); if (this.element.hidden()) this.showAt(input); }.bind(this), keyDown: function(e) { if (e.keyCode == 9 && this.element.visible()) this.hide(); }.bind(this) }); } document.onClick(this.hide.bind(this)); return this; }, /** * Shows the calendar at the given element left-bottom corner * * @param Element element or String element id * @return Calendar this */ showAt: function(element) { var element = $(element), dims = element.dimensions(); this.setDate(this.parse(element.value)); this.element.setStyle({ position: 'absolute', margin: '0', left: (dims.left)+'px', top: (dims.top + dims.height)+'px' }).insertTo(document.body); this.stopObserving('select').stopObserving('done'); this.on(this.doneButton ? 'done' : 'select', function() { element.value = this.format(); }.bind(this)); return this.hideOthers().show(); }, /** * Toggles the calendar state at the associated element position * * @param Element input * @return Calendar this */ toggleAt: function(input) { if (this.element.parentNode && this.element.visible()) { this.hide(); } else { this.showAt(input); } return this; }, // protected // hides all the other calendars on the page hideOthers: function() { $$('div.right-calendar').each(function(element) { if (!element.hasClass('right-calendar-inline')) { if (element != this.element) { element.hide(); } } }); return this; } }); /** * This module handles the dates parsing/formatting processes * * To format dates and times this scripts use the GNU (C/Python/Ruby) strftime * function formatting principles * * %a - The abbreviated weekday name (``Sun'') * %A - The full weekday name (``Sunday'') * %b - The abbreviated month name (``Jan'') * %B - The full month name (``January'') * %d - Day of the month (01..31) * %e - Day of the month without leading zero (1..31) * %m - Month of the year (01..12) * %y - Year without a century (00..99) * %Y - Year with century * %H - Hour of the day, 24-hour clock (00..23) * %k - Hour of the day, 24-hour clock without leading zero (0..23) * %I - Hour of the day, 12-hour clock (01..12) * %l - Hour of the day, 12-hour clock without leading zer (0..12) * %p - Meridian indicator (``AM'' or ``PM'') * %P - Meridian indicator (``pm'' or ``pm'') * %M - Minute of the hour (00..59) * %S - Second of the minute (00..60) * %% - Literal ``%'' character * * Copyright (C) 2009 Nikolay V. Nemshilov aka St. */ Calendar.include({ /** * Parses out the given string based on the current date formatting * * @param String string date * @return Date parsed date or null if it wasn't parsed */ parse: function(string) { var date; if (string instanceof Date || Date.parse(string)) { date = new Date(string); } else if (isString(string) && string) { var tpl = RegExp.escape(this.options.format); var holders = tpl.match(/%[a-z]/ig).map('match', /[a-z]$/i).map('first').without('%'); var re = new RegExp('^'+tpl.replace(/%p/i, '(pm|PM|am|AM)').replace(/(%[a-z])/ig, '(.+?)')+'$'); var match = string.trim().match(re); if (match) { match.shift(); var year = null, month = null, date = null, hour = null, minute = null, second = null, meridian; while (match.length) { var value = match.shift(); var key = holders.shift(); if (key.toLowerCase() == 'b') { month = this.options.i18n[key=='b' ? 'monthNamesShort' : 'monthNames'].indexOf(value); } else if (key.toLowerCase() == 'p') { meridian = value.toLowerCase(); } else { value = value.toInt(); switch(key) { case 'd': case 'e': date = value; break; case 'm': month = value-1; break; case 'y': case 'Y': year = value; break; case 'H': case 'k': case 'I': case 'l': hour = value; break; case 'M': minute = value; break; case 'S': second = value; break; } } } // converting 1..12am|pm into 0..23 hours marker if (meridian) { hour = hour == 12 ? 0 : hour; hour = (meridian == 'pm' ? hour + 12 : hour); } date = new Date(year, month, date, hour, minute, second); } } else { date = new Date(); } return date; }, /** * Formats the current date into a string depend on the current or given format * * @param String optional format * @return String formatted data */ format: function(format) { var i18n = this.options.i18n; var day = this.date.getDay(); var month = this.date.getMonth(); var date = this.date.getDate(); var year = this.date.getFullYear(); var hour = this.date.getHours(); var minute = this.date.getMinutes(); var second = this.date.getSeconds(); var hour_ampm = (hour == 0 ? 12 : hour < 13 ? hour : hour - 12); var values = { a: i18n.dayNamesShort[day], A: i18n.dayNames[day], b: i18n.monthNamesShort[month], B: i18n.monthNames[month], d: (date < 10 ? '0' : '') + date, e: ''+date, m: (month < 9 ? '0' : '') + (month+1), y: (''+year).substring(2,4), Y: ''+year, H: (hour < 10 ? '0' : '')+ hour, k: '' + hour, I: (hour > 0 && (hour < 10 || (hour > 12 && hour < 22)) ? '0' : '') + hour_ampm, l: '' + hour_ampm, p: hour < 12 ? 'AM' : 'PM', P: hour < 12 ? 'am' : 'pm', M: (minute < 10 ? '0':'')+minute, S: (second < 10 ? '0':'')+second, '%': '%' }; var result = format || this.options.format; for (var key in values) { result = result.replace('%'+key, values[key]); } return result; } }); /** * Calendar fields autodiscovery via the rel="calendar" attribute * * Copyright (C) 2009 Nikolay V. Nemshilov aka St. */ document.onReady(Calendar.rescan); document.write("");