/* Script: Calendar.js Class: Calendar Allows the user to enter a date in any popuplar format or choose from a calendar. Authors: Paul Anderson Aaron Newton Eric Clemmons Note: I (Eric Clemmons) made some changes so that the calendar isn't reliant on several CNet libraries (to help reduce size). Also, there a couple small fixes with CSS and the addition of transitions for capable browsers. Losing focus on both the input and the calendar automatically closes it, so it's unobtrusive. Also, this script automatically creates a calendar for every input with the class 'calendar'. Parameters: I'm only listing the important ones, as the rest can be found on your own. input - The input field that contains the date. This field is validated via Regular Expressions and shows/hides based on the focus of the input. options - Object of options that are listed below. Options: position - "bottomLeft" (default), "bottomRight", "topLeft", "topRight". offset - Pixel offsets for exact positioning. Defaults to [10, 10]. fadeDuration - Time in milliseconds for fade-in/fade-out. draggable - Defaults to *true*. I don't care if it's set to false, that sucker's gonna be draggable. additionalShowLinks - If you have an image to trigger the calendar, pass the element here. Not the ID or Name, the actual element. Example: (start example) // Upon inclusion of the script, this will automatically have the calendar // added to it. (end example) */ window.addEvent('domready', function() { $$('.calendar').each(function(element) { if(element.getTag() == "input") new Calendar(element); }); $$('img.calendar').each(function(element) { if(element.getPrevious().getTag() == "input") { var input = element.getPrevious(); element.addEvent('click', function() { input.fireEvent('focus'); }); } }); }); var Calendar = new Class({ defaultCss: 'div.calendarHolder{width:210px; height:182px; padding-left:8px; padding-top:1px; '+ 'background:url(http://whitefence.com/scripts/client/images/calendar_back.png) no-repeat} '+ '* html div.calendarHolder {background:url(http://whitefence.com/scripts/client/images/calendar_back.gif) no-repeat}'+ 'table.datePicker * {font-size:11px; line-height:16px;} '+ 'table.datePicker{margin:6px 0px 0px 0px; width:190px; padding:0px 5px 0px 5px} '+ 'table.datePicker td{cursor:pointer; text-align:center} '+ 'table.datePicker img.closebtn{margin-top:2px} '+ 'tr.dateNav{height:22px; margin-top:8px; } '+ 'tr.dayNames td{color:#666; font-weight:bold; border-bottom:1px solid #ddd} '+ 'table.datePicker tr.dayRow td:hover{background:#ccc} '+ 'td.today{color:#bb0904} '+ 'td.otherMonthDate{border:1px solid #fff; color:#666; background:#f3f3f3} '+ 'td.selectedDate{border:1px solid #20397b; background:#dcddef}'+ 'td.invalid{color: #ccc}', fullDay: 86400000, moved: false, visible: false, initialize: function(input, options){ if(!$(input)) return false; this.setOptions({ calendarId: false, months: ["January","February","March","April","May","June","July", "August","September","October","November","December"], days: ["Su","Mo","Tu","We","Th","Fr","Sa"], position: "bottomLeft", offset: {x:10, y:10}, fadeDuration: 400, rangeInDays: null, //[-7, 83] Allows the previous week to be picked, + 3 months draggable: true, dragOptions: {}, additionalShowLinks: null, showOnInputFocus: true, hideOnInputBlur: true, useDefaultCss: true, hideCalendarOnPick: true, available: ['Su','Mo','Tu','We','Th','Fr','Sa'], onPick: Class.empty, onShow: Class.empty, onHide: Class.empty }, options); this.setOptions(options); if(!this.options.calendarId) this.options.calendarId = "popupCalendar" + new Date().getTime(); this.input = $(input); if(this.options.useDefaultCss)this.writeCss(); this.setUpObservers(); this.getCalendar(); }, setUpObservers: function(){ if(this.options.showOnInputFocus) this.input.addEvent('focus', this.show.bind(this)); if(this.options.hideOnInputBlur) this.input.addEvent('blur', this.checkForHide.delay.pass(10, this.checkForHide.bind(this))); try {this.input.addEvent('blur', this.updateInput.bind(this));}catch(e){} //ie sometimes doesn't like this. if($defined(this.options.additionalShowLinks) && !this.input.hasCalendar) { if(this.options.additionalShowLinks.getParent().getTag() != 'a') { var a = new Element('a', { 'href': 'javascript:void(0);'}).injectBefore(this.options.additionalShowLinks); a.adopt(this.options.additionalShowLinks); } this.options.additionalShowLinks.addEvent('click', this.show.bind(this)); } }, checkForHide: function() { if(!this.moved) this.hide(); }, writeCss: function(css) { css = $pick(css,this.defaultCss); if(Client.Engine.ie) css += "table.datePicker * { padding: 0px; margin: 0px; }"; window.addEvent('domready', function(){ try { if($('datePickerStyle')) return; var stylesheet = new Element('style', { 'type': 'text/css', 'media': 'screen', 'id': 'datePickerStyle' }).injectInside($$('head')[0]); if(Client.Engine.ie) { document.styleSheets[document.styleSheets.length-1].cssText = css; } else { stylesheet.appendText(css); } }catch(e){ } }); }, updateInput: function(date){ if(!$type(date) == "string" || (date && !date.getTime)) date = this.input.value; var dateStr = this.formatDate(this.validDate(date)); if($type(dateStr) == "string") { this.input.value = dateStr; return dateStr; } return date; }, validDate: function(val) { val = $pick(val, this.input.value); val = val.replace(/^\s+|\s+$/g,""); var asDate = Date.parse(val); if (isNaN(asDate)) asDate = Date.parse(val.replace(/[^\w\s]/g,"/")); if (isNaN(asDate)) asDate = Date.parse(val.replace(/[^\w\s]/g,"/") + "/" + new Date().getFullYear()); if(this.options.rangeInDays) { var dayDifference = ((asDate - this.today.getTime()) / this.fullDay); if(dayDifference >= this.options.rangeInDays[1] +2 || dayDifference < this.options.rangeInDays[0]) { var earliest = new Date(); earliest.setDate(earliest.getDate()+this.options.rangeInDays[0]); return earliest; //return new Date(); } } if (!isNaN(asDate)) { var newDate = new Date(asDate); if (newDate.getFullYear() < 2000 && val.indexOf(newDate.getFullYear()) < 0) { newDate.setFullYear(newDate.getFullYear() + 100); } return newDate; } else return asDate; }, formatDate: function (date) { try { // always "get" as UTC, without timezone, so there's no confusion over the calendar day var fd = ((date.getUTCMonth() < 9) ? "0" : "") + (date.getUTCMonth()+1) + "/"; fd += ((date.getUTCDate() < 10) ? "0" : "") + date.getUTCDate() + "/"; fd += date.getUTCFullYear(); return fd; } catch(e){return date} }, zeroHourGMT: function(date) { date.setTime(date.getTime() - date.getTime() % 86400000); return date; }, getCalendar: function() { if(!this.calendar) { var cal = new Element("table").setProperties({ 'id': this.options.calendarId, 'border':'0', 'cellpadding':'0', 'cellspacing':'0' }); cal.addClass('datePicker'); $(cal.insertRow(0).insertCell(0)).appendText("x"); for (var c=0;c<6;c++) $(cal.rows[0]).adopt(cal.rows[0].cells[0].cloneNode(true)); for (var r=0;r<7;r++) $(cal.rows[0].parentNode).adopt(cal.rows[0].cloneNode(true)); $(cal.rows[1]).addClass('dayNames'); for (var r=2;r<8;r++) $(cal.rows[r]).addClass('dayRow'); for (var d=0;d<7;d++) cal.rows[1].cells[d].firstChild.data = this.options.days[d]; for (var t=6;t>3;t--) cal.rows[0].deleteCell(t); $(cal.rows[0]).addClass('dateNav'); if(!window.ie6)cal.rows[0].cells[0].firstChild.data=String.fromCharCode(9668); else cal.rows[0].cells[0].firstChild.data="<"; cal.rows[0].cells[1].colSpan=4; if(!window.ie6) cal.rows[0].cells[2].firstChild.data=String.fromCharCode(9658); else cal.rows[0].cells[2].firstChild.data=">"; cal.rows[0].cells[3].firstChild.data=String.fromCharCode(215); $(cal.rows[0].cells[3].setHTML('')).adopt(this.getCloseImg()); //xb.adopt(xb.previousSibling); cal.addEvent('click', this.clickCalendar.bind(this)); this.calendar = cal; this.container = new Element('div').adopt(cal).addClass('calendarHolder'); cal.addEvent('mousedown', function() { self.moved = true; }); var self = this; this.container.makeDraggable({ handle: cal.rows[0].cells[1], onStart: function() { self.moved = true; }, onComplete: function() { self.moved = false; self.input.focus() } }); } return this.calendar; }, getCloseImg: function(){ var closer = new Element("img").setProperty('src', 'http://whitefence.com/scripts/client/images/calendar_close.gif'); closer.addEvents({ 'mouseover': function(){ closer.src = closer.src.replace('.gif', '_over.gif'); }, 'mouseout':function(){ closer.src = closer.src.replace('_over.gif', '.gif'); }, 'click': this.hide.bind(this) }).setStyles({ width: '13px', height: '13px' }).addClass('closebtn'); return closer; }, hide: function(){ if(!Client.Engine.ie) new Fx.Style(this.container, 'opacity', { wait: true, duration: this.options.fadeDuration, onComplete: function() { this.element.setStyle('display', 'none'); } }).start(0); else this.container.setStyle('display', 'none'); this.visible = false; this.fireEvent('onHide'); }, show: function(){ this.input.focus(); if(this.visible) return; this.today = this.zeroHourGMT(new Date()); this.inputDate = new Date(this.updateInput()); this.refDate = isNaN(this.inputDate) ? this.today : this.zeroHourGMT(new Date(this.inputDate)); this.getCalendar(); this.fillCalendar(this.refDate); $$('body')[0].adopt(this.container); this.positionCalendar(); var calendarId = this.options.calendarId; var self = this; if(!Client.Engine.ie) new Fx.Style(this.container, 'opacity', { wait: false, onStart: function() { this.element.setStyle('display', 'block'); }, duration: this.options.fadeDuration }).set(0).start(1); else this.container.setStyle('display', 'block'); this.visible = true; this.input.focus(); this.fireEvent('onShow'); }, positionCalendar: function() { var el = this.input.getCoordinates(); var cal = this.container.getCoordinates(); switch(this.options.position) { case 'bottomLeft': var top = el.top + el.height; var left = el.left; break; case 'bottomRight': var top = el.top + el.height; var left = el.left + el.width - cal.width; break; case 'topLeft': var top = el.top - cal.height; var left = el.left; break; case 'topRight': var top = el.top - cal.height; var left = el.left + el.width - cal.width; break; } top += this.options.offset.y; left += this.options.offset.x; this.container.setStyles({ 'top': top, 'left': left }); }, clickCalendar: function(e) { this.moved = false; e = new Event(e); if (!e.target.firstChild || !e.target.firstChild.data) return; var val = e.target.firstChild.data; if (val.charCodeAt(0) > 9600 || val == "<" || val == ">") { var newRef = this.calendar.rows[2].cells[0].refDate - this.fullDay; if (val.charCodeAt(0) != 9668 && val != "<") newRef = this.calendar.rows[7].cells[6].refDate + this.fullDay; this.fillCalendar(new Date(newRef)); return; } if (e.target.refDate) { if(e.target.hasClass('invalid')) return; var newDate = new Date(e.target.refDate); this.input.value = this.formatDate(newDate); /* trip onchange events in text field */ this.input.fireEvent("change"); this.input.fireEvent("blur"); this.fireEvent('onPick'); window.removeEvents(); if(this.options.hideCalendarOnPick) this.hide(); } }, fillCalendar: function (forDate) { var startDate = new Date(forDate.getTime()); startDate.setUTCDate(1); startDate.setTime(startDate.getTime() - (this.fullDay * startDate.getUTCDay())); this.calendar.rows[0].cells[1].firstChild.data = this.options.months[forDate.getUTCMonth()] + " " + forDate.getUTCFullYear(); var atDate = startDate; this.calendar.getElements('td').each(function (el){ el.removeClass('selectedDate').removeClass('otherMonthDate').removeClass('today'); }); for (var w=2; w<8; w++) for (var d=0; d<7; d++) { var td = this.calendar.rows[w].cells[d]; td.removeClass('invalid'); td.firstChild.data = atDate.getUTCDate(); td.refDate = atDate.getTime(); if(atDate.getTime() == this.today.getTime()) td.addClass('today'); if(atDate.getTime() == this.refDate.getTime()) td.addClass('selectedDate'); if(atDate.getUTCMonth() != forDate.getUTCMonth()) td.addClass('otherMonthDate'); if(!this.options.available.contains(this.options.days[d])) td.addClass('invalid'); if(this.options.rangeInDays) { var dayDifference = ((td.refDate - this.today.getTime()) / this.fullDay); if(dayDifference >= this.options.rangeInDays[1] + 2 || dayDifference < this.options.rangeInDays[0]) td.addClass('invalid'); } atDate.setTime(atDate.getTime() + this.fullDay); } } }); Calendar.implement(new Options); Calendar.implement(new Events);