/ Published in: JavaScript
Expand |
Embed | Plain Text
Copy this code and paste it in your HTML
// Calendar: a Javascript class for Mootools that adds accessible and unobtrusive date pickers to your form elements <http://electricprism.com/aeron/calendar> // Calendar RC4, Copyright (c) 2007 Aeron Glemann <http://electricprism.com/aeron>, MIT Style License. // Mootools 1.2 compatibility by Davorin Ã… ego var Calendar = new Class({ Implements: Options, options: { blocked: [], // blocked dates classes: [], // ['calendar', 'prev', 'next', 'month', 'year', 'today', 'invalid', 'valid', 'inactive', 'active', 'hover', 'hilite'] days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'], // days of the week starting at sunday direction: 0, // -1 past, 0 past + future, 1 future draggable: true, months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'], navigation: 1, // 0 = no nav; 1 = single nav for month; 2 = dual nav for month and year offset: 0, // first day of the week: 0 = sunday, 1 = monday, etc.. onHideStart: Class.empty, onHideComplete: Class.empty, onShowStart: Class.empty, onShowComplete: Class.empty, pad: 1, // padding between multiple calendars tweak: {x: 0, y: 0} // tweak calendar positioning }, // initialize: calendar constructor // @param obj (obj) a js object containing the form elements and format strings { id: 'format', id: 'format' etc } // @param props (obj) optional properties initialize: function(obj, options) { // basic error checking if (!obj) { return false; } this.setOptions(options); // create our classes array var keys = ['calendar', 'prev', 'next', 'month', 'year', 'today', 'invalid', 'valid', 'inactive', 'active', 'hover', 'hilite']; var values = keys.map(function(key, i) { if (this.options.classes[i]) { if (this.options.classes[i].length) { key = this.options.classes[i]; } } return key; }, this); this.classes = values.associate(keys); // create cal element with css styles required for proper cal functioning this.calendar = new Element('div', { 'styles': { left: '-1000px', opacity: 0, position: 'absolute', top: '-1000px', zIndex: 1000 } }).addClass(this.classes.calendar).injectInside(document.body); // iex 6 needs a transparent iframe underneath the calendar in order to not allow select elements to render through if (window.ie6) { this.iframe = new Element('iframe', { 'styles': { left: '-1000px', position: 'absolute', top: '-1000px', zIndex: 999 } }).injectInside(document.body); this.iframe.style.filter = 'progid:DXImageTransform.Microsoft.Alpha(style=0,opacity=0)'; } // initialize fade method this.fx = new Fx.Tween(this.calendar, { onStart: function() { if (this.calendar.getStyle('opacity') == 0) { // show if (window.ie6) { this.iframe.setStyle('display', 'block'); } this.calendar.setStyle('display', 'block'); this.fireEvent('onShowStart', this.element); } else { // hide this.fireEvent('onHideStart', this.element); } }.bind(this), onComplete: function() { if (this.calendar.getStyle('opacity') == 0) { // hidden this.calendar.setStyle('display', 'none'); if (window.ie6) { this.iframe.setStyle('display', 'none'); } this.fireEvent('onHideComplete', this.element); } else { // shown this.fireEvent('onShowComplete', this.element); } }.bind(this) }); // initialize drag method if (window.Drag && this.options.draggable) { this.drag = new Drag.Move(this.calendar, { onDrag: function() { if (window.ie6) { this.iframe.setStyles({ left: this.calendar.style.left, top: this.calendar.style.top }); } }.bind(this) }); } // create calendars array this.calendars = []; var id = 0; var d = new Date(); // today d.setDate(d.getDate() + this.options.direction.toInt()); // correct today for directional offset for (var i in obj) { var cal = { button: new Element('a', { 'type': 'button' }), el: $(i), els: [], id: id++, month: d.getMonth(), visible: false, year: d.getFullYear() }; // fix for bad element (naughty, naughty element!) if (!this.element(i, obj[i], cal)) { continue; } cal.el.addClass(this.classes.calendar); // create cal button cal.button.addClass(this.classes.calendar).addEvent('click', function(cal) { this.toggle(cal); }.pass(cal, this)).injectAfter(cal.el); // read in default value cal.val = this.read(cal); $extend(cal, this.bounds(cal)); // abs bounds of calendar $extend(cal, this.values(cal)); // valid days, months, years this.rebuild(cal); this.calendars.push(cal); // add to cals array } }, // blocked: returns an array of blocked days for the month / year // @param cal (obj) // @returns blocked days (array) blocked: function(cal) { var blocked = []; var offset = new Date(cal.year, cal.month, 1).getDay(); // day of the week (offset) var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month this.options.blocked.each(function(date){ var values = date.split(' '); // preparation for (var i = 0; i <= 3; i++){ if (!values[i]){ values[i] = (i == 3) ? '' : '*'; } // make sure blocked date contains values for at least d, m and y values[i] = values[i].contains(',') ? values[i].split(',') : new Array(values[i]); // split multiple values var count = values[i].length - 1; for (var j = count; j >= 0; j--){ if (values[i][j].contains('-')){ // a range var val = values[i][j].split('-'); for (var k = val[0]; k <= val[1]; k++){ if (!values[i].contains(k)){ values[i].push(k + ''); } } values[i].splice(j, 1); } } } // execution if (values[2].contains(cal.year + '') || values[2].contains('*')){ if (values[1].contains(cal.month + 1 + '') || values[1].contains('*')){ values[0].each(function(val){ // if blocked value indicates this month / year if (val > 0){ blocked.push(val.toInt()); } // add date to blocked array }); if (values[3]){ // optional value for day of week for (var i = 0; i < last; i++){ var day = (i + offset) % 7; if (values[3].contains(day + '')){ blocked.push(i + 1); // add every date that corresponds to the blocked day of the week to the blocked array } } } } } }, this); return blocked; }, // bounds: returns the start / end bounds of the calendar // @param cal (obj) // @returns obj bounds: function(cal) { // 1. first we assume the calendar has no bounds (or a thousand years in either direction) // by default the calendar will accept a millennium in either direction var start = new Date(1000, 0, 1); // jan 1, 1000 var end = new Date(2999, 11, 31); // dec 31, 2999 // 2. but if the cal is one directional we adjust accordingly var date = new Date().getDate() + this.options.direction.toInt(); if (this.options.direction > 0) { start = new Date(); start.setDate(date + this.options.pad * cal.id); } if (this.options.direction < 0) { end = new Date(); end.setDate(date - this.options.pad * (this.calendars.length - cal.id - 1)); } // 3. then we can further filter the limits by using the pre-existing values in the selects cal.els.each(function(el) { if (el.get('tag') == 'select') { if (el.format.test('(y|Y)')) { // search for a year select var years = []; el.getChildren().each(function(option) { // get options var values = this.unformat(option.value, el.format); if (!years.contains(values[0])) { years.push(values[0]); } // add to years array }, this); years.sort(this.sort); if (years[0] > start.getFullYear()) { d = new Date(years[0], start.getMonth() + 1, 0); // last day of new month if (start.getDate() > d.getDate()) { start.setDate(d.getDate()); } start.setYear(years[0]); } if (years.getLast() < end.getFullYear()) { d = new Date(years.getLast(), end.getMonth() + 1, 0); // last day of new month if (end.getDate() > d.getDate()) { end.setDate(d.getDate()); } end.setYear(years.getLast()); } } if (el.format.test('(F|m|M|n)')) { // search for a month select var months_start = []; var months_end = []; el.getChildren().each(function(option) { // get options var values = this.unformat(option.value, el.format); if ($type(values[0]) != 'number' || values[0] == years[0]) { // if it's a year / month combo for curr year, or simply a month select if (!months_start.contains(values[1])) { months_start.push(values[1]); } // add to months array } if ($type(values[0]) != 'number' || values[0] == years.getLast()) { // if it's a year / month combo for curr year, or simply a month select if (!months_end.contains(values[1])) { months_end.push(values[1]); } // add to months array } }, this); months_start.sort(this.sort); months_end.sort(this.sort); if (months_start[0] > start.getMonth()) { d = new Date(start.getFullYear(), months_start[0] + 1, 0); // last day of new month if (start.getDate() > d.getDate()) { start.setDate(d.getDate()); } start.setMonth(months_start[0]); } if (months_end.getLast() < end.getMonth()) { d = new Date(start.getFullYear(), months_end.getLast() + 1, 0); // last day of new month if (end.getDate() > d.getDate()) { end.setDate(d.getDate()); } end.setMonth(months_end.getLast()); } } } }, this); return { 'start': start, 'end': end }; }, // caption: returns the caption element with header and navigation // @param cal (obj) // @returns caption (element) caption: function(cal) { // start by assuming navigation is allowed var navigation = { prev: { 'month': true, 'year': true }, next: { 'month': true, 'year': true } }; // if we're in an out of bounds year if (cal.year == cal.start.getFullYear()) { navigation.prev.year = false; if (cal.month == cal.start.getMonth() && this.options.navigation == 1) { navigation.prev.month = false; } } if (cal.year == cal.end.getFullYear()) { navigation.next.year = false; if (cal.month == cal.end.getMonth() && this.options.navigation == 1) { navigation.next.month = false; } } // special case of improved navigation but months array with only 1 month we can disable all month navigation if ($type(cal.months) == 'array') { if (cal.months.length == 1 && this.options.navigation == 2) { navigation.prev.month = navigation.next.month = false; } } var caption = new Element('caption'); var prev = new Element('a').addClass(this.classes.prev).appendText('\x3c'); // < var next = new Element('a').addClass(this.classes.next).appendText('\x3e'); // > if (this.options.navigation == 2) { var month = new Element('span').addClass(this.classes.month).injectInside(caption); if (navigation.prev.month) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', -1); }.pass(cal, this)).injectInside(month); } month.adopt(new Element('span').appendText(this.options.months[cal.month])); if (navigation.next.month) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', 1); }.pass(cal, this)).injectInside(month); } var year = new Element('span').addClass(this.classes.year).injectInside(caption); if (navigation.prev.year) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'y', -1); }.pass(cal, this)).injectInside(year); } year.adopt(new Element('span').appendText(cal.year)); if (navigation.next.year) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'y', 1); }.pass(cal, this)).injectInside(year); } } else { // 1 or 0 if (navigation.prev.month && this.options.navigation) { prev.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', -1); }.pass(cal, this)).injectInside(caption); } caption.adopt(new Element('span').addClass(this.classes.month).appendText(this.options.months[cal.month])); caption.adopt(new Element('span').addClass(this.classes.year).appendText(cal.year)); if (navigation.next.month && this.options.navigation) { next.clone().addEvent('click', function(cal) { this.navigate(cal, 'm', 1); }.pass(cal, this)).injectInside(caption); } } return caption; }, // changed: run when a select value is changed // @param cal (obj) changed: function(cal) { cal.val = this.read(cal); // update calendar val from inputs $extend(cal, this.values(cal)); // update bounds - based on curr month this.rebuild(cal); // rebuild days select if (!cal.val) { return; } // in case the same date was clicked the cal has no set date we should exit if (cal.val.getDate() < cal.days[0]) { cal.val.setDate(cal.days[0]); } if (cal.val.getDate() > cal.days.getLast()) { cal.val.setDate(cal.days.getLast()); } cal.els.each(function(el) { // then we can set the value to the field el.value = this.format(cal.val, el.format); }, this); this.check(cal); // checks other cals this.calendars.each(function(kal) { // update cal graphic if visible if (kal.visible) { this.display(kal); } }, this); }, // check: checks other calendars to make sure no overlapping values // @param cal (obj) check: function(cal) { this.calendars.each(function(kal, i) { if (kal.val) { // if calendar has value set var change = false; if (i < cal.id) { // preceding calendar var bound = new Date(Date.parse(cal.val)); bound.setDate(bound.getDate() - (this.options.pad * (cal.id - i))); if (bound < kal.val) { change = true; } } if (i > cal.id) { // following calendar var bound = new Date(Date.parse(cal.val)); bound.setDate(bound.getDate() + (this.options.pad * (i - cal.id))); if (bound > kal.val) { change = true; } } if (change) { if (kal.start > bound) { bound = kal.start; } if (kal.end < bound) { bound = kal.end; } kal.month = bound.getMonth(); kal.year = bound.getFullYear(); $extend(kal, this.values(kal)); // TODO - IN THE CASE OF SELECT MOVE TO NEAREST VALID VALUE // IN THE CASE OF INPUT DISABLE // if new date is not valid better unset cal value // otherwise it would mean incrementally checking to find the nearest valid date which could be months / years away kal.val = kal.days.contains(bound.getDate()) ? bound : null; this.write(kal); if (kal.visible) { this.display(kal); } // update cal graphic if visible } } else { kal.month = cal.month; kal.year = cal.year; } }, this); }, // clicked: run when a valid day is clicked in the calendar // @param cal (obj) clicked: function(td, day, cal) { cal.val = (this.value(cal) == day) ? null : new Date(cal.year, cal.month, day); // set new value - if same then disable this.write(cal); // ok - in the special case that it's all selects and there's always a date no matter what (at least as far as the form is concerned) // we can't let the calendar undo a date selection - it's just not possible!! if (!cal.val) { cal.val = this.read(cal); } if (cal.val) { this.check(cal); // checks other cals this.toggle(cal); // hide cal } else { // remove active class and replace with valid td.addClass(this.classes.valid); td.removeClass(this.classes.active); } }, // display: create calendar element // @param cal (obj) display: function(cal) { // 1. header and navigation this.calendar.empty(); // init div this.calendar.className = this.classes.calendar + ' ' + this.options.months[cal.month].toLowerCase(); var div = new Element('div').injectInside(this.calendar); // a wrapper div to help correct browser css problems with the caption element var table = new Element('table').injectInside(div).adopt(this.caption(cal)); // 2. day names var thead = new Element('thead').injectInside(table); var tr = new Element('tr').injectInside(thead); for (var i = 0; i <= 6; i++) { var th = this.options.days[(i + this.options.offset) % 7]; tr.adopt(new Element('th', { 'title': th }).appendText(th.substr(0, 1))); } // 3. day numbers var tbody = new Element('tbody').injectInside(table); var tr = new Element('tr').injectInside(tbody); var d = new Date(cal.year, cal.month, 1); var offset = ((d.getDay() - this.options.offset) + 7) % 7; // day of the week (offset) var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of this month var prev = new Date(cal.year, cal.month, 0).getDate(); // last day of previous month var active = this.value(cal); // active date (if set and within curr month) var valid = cal.days; // valid days for curr month var inactive = []; // active dates set by other calendars var hilited = []; this.calendars.each(function(kal, i) { if (kal != cal && kal.val) { if (cal.year == kal.val.getFullYear() && cal.month == kal.val.getMonth()) { inactive.push(kal.val.getDate()); } if (cal.val) { for (var day = 1; day <= last; day++) { d.setDate(day); if ((i < cal.id && d > kal.val && d < cal.val) || (i > cal.id && d > cal.val && d < kal.val)) { if (!hilited.contains(day)) { hilited.push(day); } } } } } }, this); var d = new Date(); var today = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime(); // today obv for (var i = 1; i < 43; i++) { // 1 to 42 (6 x 7 or 6 weeks) if ((i - 1) % 7 == 0) { tr = new Element('tr').injectInside(tbody); } // each week is it's own table row var td = new Element('td').injectInside(tr); var day = i - offset; var date = new Date(cal.year, cal.month, day); var cls = ''; if (day === active) { cls = this.classes.active; } // active else if (inactive.contains(day)) { cls = this.classes.inactive; } // inactive else if (valid.contains(day)) { cls = this.classes.valid; } // valid else if (day >= 1 && day <= last) { cls = this.classes.invalid; } // invalid if (date.getTime() == today) { cls = cls + ' ' + this.classes.today; } // adds class for today if (hilited.contains(day)) { cls = cls + ' ' + this.classes.hilite; } // adds class if hilited td.addClass(cls); if (valid.contains(day)) { // if it's a valid - clickable - day we add interaction td.setProperty('title', this.format(date, 'D M jS Y')); td.addEvents({ 'click': function(td, day, cal) { this.clicked(td, day, cal); }.pass([td, day, cal], this), 'mouseover': function(td, cls) { td.addClass(cls); }.pass([td, this.classes.hover]), 'mouseout': function(td, cls) { td.removeClass(cls); }.pass([td, this.classes.hover]) }); } // pad calendar with last days of prev month and first days of next month if (day < 1) { day = prev + day; } else if (day > last) { day = day - last; } td.appendText(day); } }, // element: helper function // @param el (string) element id // @param f (string) format string // @param cal (obj) element: function(el, f, cal) { if ($type(f) == 'object') { // in the case of multiple inputs per calendar for (var i in f) { if (!this.element(i, f[i], cal)) { return false; } } return true; } el = $(el); if (!el) { return false; } el.format = f; if (el.get('tag') == 'select') { // select elements allow the user to manually set the date via select option el.addEvent('change', function(cal) { this.changed(cal); }.pass(cal, this)); } else { // input (type text) elements restrict the user to only setting the date via the calendar el.readOnly = true; el.addEvent('focus', function(cal) { this.toggle(cal); }.pass(cal, this)); } cal.els.push(el); return true; }, // format: formats a date object according to passed in instructions // @param date (obj) // @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y // @returns string format: function(date, format) { var str = ''; if (date) { var j = date.getDate(); // 1 - 31 var w = date.getDay(); // 0 - 6 var l = this.options.days[w]; // Sunday - Saturday var n = date.getMonth() + 1; // 1 - 12 var f = this.options.months[n - 1]; // January - December var y = date.getFullYear() + ''; // 19xx - 20xx for (var i = 0, len = format.length; i < len; i++) { var cha = format.charAt(i); // format char switch(cha) { // year cases case 'y': // xx - xx y = y.substr(2); case 'Y': // 19xx - 20xx str += y; break; // month cases case 'm': // 01 - 12 if (n < 10) { n = '0' + n; } case 'n': // 1 - 12 str += n; break; case 'M': // Jan - Dec f = f.substr(0, 3); case 'F': // January - December str += f; break; // day cases case 'd': // 01 - 31 if (j < 10) { j = '0' + j; } case 'j': // 1 - 31 str += j; break; case 'D': // Sun - Sat l = l.substr(0, 3); case 'l': // Sunday - Saturday str += l; break; case 'N': // 1 - 7 w += 1; case 'w': // 0 - 6 str += w; break; case 'S': // st, nd, rd or th (works well with j) if (j % 10 == 1 && j != '11') { str += 'st'; } else if (j % 10 == 2 && j != '12') { str += 'nd'; } else if (j % 10 == 3 && j != '13') { str += 'rd'; } else { str += 'th'; } break; default: str += cha; } } } return str; // return format with values replaced }, // navigate: calendar navigation // @param cal (obj) // @param type (str) m or y for month or year // @param n (int) + or - for next or prev navigate: function(cal, type, n) { switch (type) { case 'm': // month if ($type(cal.months) == 'array') { var i = cal.months.indexOf(cal.month) + n; // index of current month if (i < 0 || i == cal.months.length) { // out of range if (this.options.navigation == 1) { // if type 1 nav we'll need to increment the year this.navigate(cal, 'y', n); } i = (i < 0) ? cal.months.length - 1 : 0; } cal.month = cal.months[i]; } else { var i = cal.month + n; if (i < 0 || i == 12) { if (this.options.navigation == 1) { this.navigate(cal, 'y', n); } i = (i < 0) ? 11 : 0; } cal.month = i; } break; case 'y': // year if ($type(cal.years) == 'array') { var i = cal.years.indexOf(cal.year) + n; cal.year = cal.years[i]; } else { cal.year += n; } break; } $extend(cal, this.values(cal)); if ($type(cal.months) == 'array') { // if the calendar has a months select var i = cal.months.indexOf(cal.month); // and make sure the curr months exists for the new year if (i < 0) { cal.month = cal.months[0]; } // otherwise we'll reset the month } this.display(cal); }, // read: compiles cal value based on array of inputs passed in // @param cal (obj) // @returns date (obj) or (null) read: function(cal) { var arr = [null, null, null]; cal.els.each(function(el) { // returns an array which may contain empty values var values = this.unformat(el.value, el.format); values.each(function(val, i) { if ($type(val) == 'number') { arr[i] = val; } }); }, this); // we can update the cals month and year values if ($type(arr[0]) == 'number') { cal.year = arr[0]; } if ($type(arr[1]) == 'number') { cal.month = arr[1]; } var val = null; if (arr.every(function(i) { return $type(i) == 'number'; })) { // if valid date var last = new Date(arr[0], arr[1] + 1, 0).getDate(); // last day of month if (arr[2] > last) { arr[2] = last; } // make sure we stay within the month (ex in case default day of select is 31 and month is feb) val = new Date(arr[0], arr[1], arr[2]); } return (cal.val == val) ? null : val; // if new date matches old return null (same date clicked twice = disable) }, // rebuild: rebuilds days + months selects // @param cal (obj) rebuild: function(cal) { cal.els.each(function(el) { /* if (el.get('tag') == 'select' && el.format.test('^(F|m|M|n)$')) { // special case for months-only select if (!cal.options) { cal.options = el.clone(); } // clone a copy of months select var val = (cal.val) ? cal.val.getMonth() : el.value.toInt(); el.empty(); // initialize select cal.months.each(function(month) { // create an option element var option = new Element('option', { 'selected': (val == month), 'value': this.format(new Date(1, month, 1), el.format); }).appendText(day).injectInside(el); }, this); } */ if (el.get('tag') == 'select' && el.format.test('^(d|j)$')) { // special case for days-only select var d = this.value(cal); if (!d) { d = el.value.toInt(); } // if the calendar doesn't have a set value, try to use value from select el.empty(); // initialize select cal.days.each(function(day) { // create an option element var option = new Element('option', { 'selected': (d == day), 'value': ((el.format == 'd' && day < 10) ? '0' + day : day) }).appendText(day).injectInside(el); }, this); } }, this); }, // sort: helper function for numerical sorting sort: function(a, b) { return a - b; }, // toggle: show / hide calendar // @param cal (obj) toggle: function(cal) { document.removeEvent('mousedown', this.fn); // always remove the current mousedown script first if (cal.visible) { // simply hide curr cal cal.visible = false; cal.button.removeClass(this.classes.active); // active this.fx.start('opacity', 1, 0); } else { // otherwise show (may have to hide others) // hide cal on out-of-bounds click this.fn = function(e, cal) { var e = new Event(e); var el = e.target; var stop = false; while (el != document.body && el.nodeType == 1) { if (el == this.calendar) { stop = true; } this.calendars.each(function(kal) { if (kal.button == el || kal.els.contains(el)) { stop = true; } }); if (stop) { e.stop(); return false; } else { el = el.parentNode; } } this.toggle(cal); }.create({ 'arguments': cal, 'bind': this, 'event': true }); document.addEvent('mousedown', this.fn); this.calendars.each(function(kal) { if (kal == cal) { kal.visible = true; kal.button.addClass(this.classes.active); // css c-icon-active } else { kal.visible = false; kal.button.removeClass(this.classes.active); // css c-icon-active } }, this); var size = window.getScrollSize(); var coord = cal.button.getCoordinates(); var x = coord.right + this.options.tweak.x; var y = coord.top + this.options.tweak.y; // make sure the calendar doesn't open off screen if (!this.calendar.coord) { this.calendar.coord = this.calendar.getCoordinates(); } if (x + this.calendar.coord.width > size.x) { x -= (x + this.calendar.coord.width - size.x); } if (y + this.calendar.coord.height > size.y) { y -= (y + this.calendar.coord.height - size.y); } this.calendar.setStyles({ left: x + 'px', top: y + 'px' }); if (window.ie6) { this.iframe.setStyles({ height: this.calendar.coord.height + 'px', left: x + 'px', top: y + 'px', width: this.calendar.coord.width + 'px' }); } this.display(cal); this.fx.start('opacity', 0, 1); } }, // unformat: takes a value from an input and parses the d, m and y elements // @param val (string) // @param f (string) any combination of punctuation / separators and d, j, D, l, S, m, n, F, M, y, Y // @returns array unformat: function(val, f) { f = f.escapeRegExp(); var re = { d: '([0-9]{2})', j: '([0-9]{1,2})', D: '(' + this.options.days.map(function(day) { return day.substr(0, 3); }).join('|') + ')', l: '(' + this.options.days.join('|') + ')', S: '(st|nd|rd|th)', F: '(' + this.options.months.join('|') + ')', m: '([0-9]{2})', M: '(' + this.options.months.map(function(month) { return month.substr(0, 3); }).join('|') + ')', n: '([0-9]{1,2})', Y: '([0-9]{4})', y: '([0-9]{2})' } var arr = []; // array of indexes var g = ''; // convert our format string to regexp for (var i = 0; i < f.length; i++) { var c = f.charAt(i); if (re[c]) { arr.push(c); g += re[c]; } else { g += c; } } // match against date var matches = val.match('^' + g + '$'); var dates = new Array(3); if (matches) { matches = matches.slice(1); // remove first match which is the date arr.each(function(c, i) { i = matches[i]; switch(c) { // year cases case 'y': i = '19' + i; // 2 digit year assumes 19th century (same as JS) case 'Y': dates[0] = i.toInt(); break; // month cases case 'F': i = i.substr(0, 3); case 'M': i = this.options.months.map(function(month) { return month.substr(0, 3); }).indexOf(i) + 1; case 'm': case 'n': dates[1] = i.toInt() - 1; break; // day cases case 'd': case 'j': dates[2] = i.toInt(); break; } }, this); } return dates; }, // value: returns day value of calendar if set // @param cal (obj) // @returns day (int) or null value: function(cal) { var day = null; if (cal.val) { if (cal.year == cal.val.getFullYear() && cal.month == cal.val.getMonth()) { day = cal.val.getDate(); } } return day; }, // values: returns the years, months (for curr year) and days (for curr month and year) for the calendar // @param cal (obj) // @returns obj values: function(cal) { var years, months, days; cal.els.each(function(el) { if (el.get('tag') == 'select') { if (el.format.test('(y|Y)')) { // search for a year select years = []; el.getChildren().each(function(option) { // get options var values = this.unformat(option.value, el.format); if (!years.contains(values[0])) { years.push(values[0]); } // add to years array }, this); years.sort(this.sort); } if (el.format.test('(F|m|M|n)')) { // search for a month select months = []; // 0 - 11 should be el.getChildren().each(function(option) { // get options var values = this.unformat(option.value, el.format); if ($type(values[0]) != 'number' || values[0] == cal.year) { // if it's a year / month combo for curr year, or simply a month select if (!months.contains(values[1])) { months.push(values[1]); } // add to months array } }, this); months.sort(this.sort); } if (el.format.test('(d|j)') && !el.format.test('^(d|j)$')) { // search for a day select, but NOT a days only select days = []; // 1 - 31 el.getChildren().each(function(option) { // get options var values = this.unformat(option.value, el.format); // in the special case of days we dont want the value if its a days only select // otherwise that will screw up the options rebuilding // we will take the values if they are exact dates though if (values[0] == cal.year && values[1] == cal.month) { if (!days.contains(values[2])) { days.push(values[2]); } // add to days array } }, this); } } }, this); // we start with what would be the first and last days were there no restrictions var first = 1; var last = new Date(cal.year, cal.month + 1, 0).getDate(); // last day of the month // if we're in an out of bounds year if (cal.year == cal.start.getFullYear()) { // in the special case of improved navigation but no months array, we'll need to construct one if (months == null && this.options.navigation == 2) { months = []; for (var i = 0; i < 12; i ++) { if (i >= cal.start.getMonth()) { months.push(i); } } } // if we're in an out of bounds month if (cal.month == cal.start.getMonth()) { first = cal.start.getDate(); // first day equals day of bound } } if (cal.year == cal.end.getFullYear()) { // in the special case of improved navigation but no months array, we'll need to construct one if (months == null && this.options.navigation == 2) { months = []; for (var i = 0; i < 12; i ++) { if (i <= cal.end.getMonth()) { months.push(i); } } } if (cal.month == cal.end.getMonth()) { last = cal.end.getDate(); // last day equals day of bound } } // let's get our invalid days var blocked = this.blocked(cal); // finally we can prepare all the valid days in a neat little array if ($type(days) == 'array') { // somewhere there was a days select days = days.filter(function(day) { if (day >= first && day <= last && !blocked.contains(day)) { return day; } }); } else { // no days select we'll need to construct a valid days array days = []; for (var i = first; i <= last; i++) { if (!blocked.contains(i)) { days.push(i); } } } days.sort(this.sort); // sorting our days will give us first and last of month return { 'days': days, 'months': months, 'years': years }; }, // write: sets calendars value to form elements // @param cal (obj) write: function(cal) { this.rebuild(cal); // in the case of options, we'll need to make sure we have the correct number of days available cal.els.each(function(el) { // then we can set the value to the field el.value = this.format(cal.val, el.format); }, this); } }); Calendar.implement(new Events, new Options);