/**
* 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(scope) {
var key = Calendar.Options.relName;
var rel_id_re = new RegExp(key+'\\[(.+?)\\]');
($(scope)||document).select(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 ''+name+' | ';}).join('')+
'
'+
'123456'.split('').map(function() {return ' | | | | | | |
'}).join('')+
'
'
);
},
// 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(function() { Calendar.rescan(); });
document.write("");