/******************************************************************** * Kalendae, a framework agnostic javascript date picker * * Copyright(c) 2012 Jarvis Badgley (chipersoft@gmail.com) * * http://github.com/ChiperSoft/Kalendae * * Version 0.2 * ********************************************************************/ (function (undefined) { var today; var Kalendae = function (targetElement, options) { //if the first argument isn't an element and isn't a string, assume that it is the options object if (!(targetElement instanceof Element || typeof targetElement === 'string')) options = targetElement; var self = this, classes = self.classes, opts = self.settings = util.merge(self.defaults, {attachTo:targetElement}, options || {}), $container = self.container = util.make('div', {'class':classes.container}), calendars = self.calendars = [], startDay = moment().day(opts.weekStart), vsd, columnHeaders = [], $cal, $title, $caption, $header, $days, dayNodes = [], $span, i = 0, j = opts.months; if (util.isIE8()) util.addClassName($container, 'ie8'); //generate the column headers (Su, Mo, Tu, etc) i = 7; while (i--) { columnHeaders.push( startDay.format('ddd').substr(0,opts.columnHeaderLength) ); startDay.add('days',1); } //setup publish/subscribe and apply any subscriptions passed in settings MinPubSub(self); if (typeof opts.subscribe === 'object') { for (i in opts.subscribe) if (opts.subscribe.hasOwnProperty(i)) { self.subscribe(i, opts.subscribe[i]); } } //process default selected dates self._sel = []; if (!!opts.selected) self.setSelected(opts.selected, false); //set the view month if (!!opts.viewStartDate) { vsd = moment(opts.viewStartDate, opts.format); } else if (self._sel.length > 0) { vsd = moment(self._sel[0]); } else { vsd = moment(); } self.viewStartDate = vsd.date(1); var viewDelta = ({ 'past' : opts.months-1, 'today-past' : opts.months-1, 'any' : opts.months>2?Math.floor(opts.months/2):0, 'today-future' : 0, 'future' : 0 })[this.settings.direction]; if (viewDelta && moment().month()==moment(self.viewStartDate).month()){ self.viewStartDate = moment(self.viewStartDate).subtract({M:viewDelta}).date(1); } if (typeof opts.blackout === 'function') { self.blackout = opts.blackout; } else if (!!opts.blackout) { var bdates = parseDates(opts.blackout, opts.parseSplitDelimiter); self.blackout = function (input) { input = moment(input).yearDay(); if (input < 1 || !self._sel || self._sel.length < 1) return false; var i = bdates.length; while (i--) if (bdates[i].yearDay() === input) return true; return false; } } else { self.blackout = function () {return false;} } self.direction = self.directions[opts.direction] ? self.directions[opts.direction] : self.directions['any']; //for the total months setting, generate N calendar views and add them to the container j = Math.max(opts.months,1); while (j--) { $cal = util.make('div', {'class':classes.calendar}, $container); $cal.setAttribute('data-cal-index', j); if (opts.months > 1) { if (j == Math.max(opts.months-1,1)) util.addClassName($cal, classes.monthFirst); else if (j === 0) util.addClassName($cal, classes.monthLast); else util.addClassName($cal, classes.monthMiddle); } //title bar $title = util.make('div', {'class':classes.title}, $cal); util.make('a', {'class':classes.previousYear}, $title); //previous button util.make('a', {'class':classes.previousMonth}, $title); //previous button util.make('a', {'class':classes.nextYear}, $title); //next button util.make('a', {'class':classes.nextMonth}, $title); //next button $caption = util.make('span', {'class':classes.caption}, $title); //title caption //column headers $header = util.make('div', {'class':classes.header}, $cal); i = 0; do { $span = util.make('span', {}, $header); $span.innerHTML = columnHeaders[i]; } while (++i < 7) //individual day cells $days = util.make('div', {'class':classes.days}, $cal); i = 0; dayNodes = []; while (i++ < 42) { dayNodes.push(util.make('span', {}, $days)); } //store each calendar view for easy redrawing calendars.push({ caption:$caption, days:dayNodes }); if (j) util.make('div', {'class':classes.monthSeparator}, $container); } self.draw(); util.addEvent($container, 'mousedown', function (event, target) { var clickedDate; if (util.hasClassName(target, classes.nextMonth)) { //NEXT MONTH BUTTON if (!self.disableNext && self.publish('view-changed', self, ['next-month']) !== false) { self.viewStartDate.add('months',1); self.draw(); } return false; } else if (util.hasClassName(target, classes.previousMonth)) { //PREVIOUS MONTH BUTTON if (!self.disablePreviousMonth && self.publish('view-changed', self, ['previous-month']) !== false) { self.viewStartDate.subtract('months',1); self.draw(); } return false; } else if (util.hasClassName(target, classes.nextYear)) { //NEXT MONTH BUTTON if (!self.disableNext && self.publish('view-changed', self, ['next-year']) !== false) { self.viewStartDate.add('years',1); self.draw(); } return false; } else if (util.hasClassName(target, classes.previousYear)) { //PREVIOUS MONTH BUTTON if (!self.disablePreviousMonth && self.publish('view-changed', self, ['previous-year']) !== false) { self.viewStartDate.subtract('years',1); self.draw(); } return false; } else if (util.hasClassName(target.parentNode, classes.days) && util.hasClassName(target, classes.dayActive) && (clickedDate = target.getAttribute('data-date'))) { //DAY CLICK clickedDate = moment(clickedDate, opts.dayAttributeFormat).hours(12); if (self.publish('date-clicked', self, [clickedDate]) !== false) { switch (opts.mode) { case 'multiple': if (!self.addSelected(clickedDate)) self.removeSelected(clickedDate); break; case 'range': self.addSelected(clickedDate); break; case 'single': /* falls through */ default: self.addSelected(clickedDate); break; } } return false; } return false; }); if (!!(opts.attachTo = util.$(opts.attachTo))) { opts.attachTo.appendChild($container); } }; Kalendae.prototype = { defaults : { attachTo: null, /* the element to attach the root container to. can be string or DOMElement */ months: 1, /* total number of months to display side by side */ weekStart: 0, /* day to use for the start of the week. 0 is Sunday */ direction: 'any', /* past, today-past, any, today-future, future */ directionScrolling: true, /* if a direction other than any is defined, prevent scrolling out of range */ viewStartDate: null, /* date in the month to display. When multiple months, this is the left most */ blackout: null, /* array of dates, or function to be passed a date */ selected: null, /* dates already selected. can be string, date, or array of strings or dates. */ mode: 'single', /* single, multiple, range */ format: null, /* string used for parsing dates. */ subscribe: null, /* object containing events to subscribe to */ columnHeaderLength: 2, /* number of characters to show in the column headers */ titleFormat: 'MMMM, YYYY', /* format mask for month titles. See momentjs.com for rules */ dayNumberFormat: 'D', /* format mask for individual days */ dayAttributeFormat: 'YYYY-MM-DD', /* format mask for the data-date attribute set on every span */ parseSplitDelimiter: /,\s*|\s+-\s+/, /* regex to use for splitting multiple dates from a passed string */ rangeDelimiter: ' - ', /* string to use between dates when outputting in range mode */ multipleDelimiter: ', ', /* string to use between dates when outputting in multiple mode */ dateClassMap: {} }, classes : { container :'kalendae', calendar :'k-calendar', monthFirst :'k-first-month', monthMiddle :'k-middle-month', monthLast :'k-last-month', title :'k-title', previousMonth :'k-btn-previous-month', nextMonth :'k-btn-next-month', previousYear :'k-btn-previous-year', nextYear :'k-btn-next-year', caption :'k-caption', header :'k-header', days :'k-days', dayOutOfMonth :'k-out-of-month', dayActive :'k-active', daySelected :'k-selected', dayInRange :'k-range', dayToday :'k-today', monthSeparator :'k-separator', disablePreviousMonth :'k-disable-previous-month-btn', disableNextMonth :'k-disable-next-month-btn', disablePreviousYear :'k-disable-previous-year-btn', disableNextYear :'k-disable-next-year-btn' }, disablePreviousMonth: false, disableNextMonth: false, disablePreviousYear: false, disableNextYear: false, directions: { 'past' :function (date) {return moment(date).yearDay() >= today.yearDay();}, 'today-past' :function (date) {return moment(date).yearDay() > today.yearDay();}, 'any' :function (date) {return false;}, 'today-future' :function (date) {return moment(date).yearDay() < today.yearDay();}, 'future' :function (date) {return moment(date).yearDay() <= today.yearDay();} }, getSelectedAsDates : function () { var out = []; var i=0, c = this._sel.length; for (;i a && input < b) || (a b)) return -1; return false; case 'multiple': var i = this._sel.length; while (i--) { if (this._sel[i].yearDay() === input) { return true; } } return false; case 'single': /* falls through */ default: return (this._sel[0] && (this._sel[0].yearDay() === input)); } return false; }, setSelected : function (input, draw) { this._sel = parseDates(input, this.settings.parseSplitDelimiter, this.settings.format); this._sel.sort(function (a,b) {return a.yearDay() - b.yearDay();}); if (draw !== false) this.draw(); }, addSelected : function (date, draw) { date = moment(date).hours(12); switch (this.settings.mode) { case 'multiple': if (!this.isSelected(date)) this._sel.push(date); else return false; break; case 'range': if (this._sel.length !== 1) this._sel = [date]; else { if (date.yearDay() > this._sel[0].yearDay()) this._sel[1] = date; else this._sel = [date, this._sel[0]]; } break; case 'single': /* falls through */ default: this._sel = [date]; break; } this._sel.sort(function (a,b) {return a.yearDay() - b.yearDay();}); this.publish('change', this); if (draw !== false) this.draw(); return true; }, removeSelected : function (date, draw) { date = moment(date).yearDay(); var i = this._sel.length; while (i--) { if (this._sel[i].yearDay() === date) { this._sel.splice(i,1); this.publish('change', this); if (draw !== false) this.draw(); return true; } } return false; }, draw : function draw() { // return; var month = moment(this.viewStartDate).hours(12), //force middle of the day to avoid any weird date shifts day, classes = this.classes, cal, $span, klass, i=0, c, j=0, k, s, dateString, opts = this.settings; c = this.calendars.length; do { day = moment(month).date(1); day.day( day.day() < this.settings.weekStart ? this.settings.weekStart-7 : this.settings.weekStart); //if the first day of the month is less than our week start, back up a week cal = this.calendars[i]; cal.caption.innerHTML = month.format(this.settings.titleFormat); j = 0; do { $span = cal.days[j]; klass = []; s = this.isSelected(day); if (s) klass.push(({'-1':classes.dayInRange,'1':classes.daySelected, 'true':classes.daySelected})[s]); if (day.month() != month.month()) klass.push(classes.dayOutOfMonth); else if (!(this.blackout(day) || this.direction(day)) || s>0) klass.push(classes.dayActive); if (day.yearDay() === today.yearDay()) klass.push(classes.dayToday); dateString = day.format(this.settings.dayAttributeFormat); if (opts.dateClassMap[dateString]) klass.push(opts.dateClassMap[dateString]); $span.innerHTML = day.format(opts.dayNumberFormat); $span.className = klass.join(' '); $span.setAttribute('data-date', dateString); day.add('days',1); } while (++j < 42); month.add('months',1); } while (++i < c); if (opts.directionScrolling) { var diff = -(moment().diff(month, 'months')); if (opts.direction==='today-past' || opts.direction==='past') { if (diff <= 0) { this.disableNextMonth = false; util.removeClassName(this.container, classes.disableNextMonth); } else { this.disableNextMonth = true; util.addClassName(this.container, classes.disableNextMonth); } } else if (opts.direction==='today-future' || opts.direction==='future') { if (diff > opts.months) { this.disablePreviousMonth = false; util.removeClassName(this.container, classes.disablePreviousMonth); } else { this.disablePreviousMonth = true; util.addClassName(this.container, classes.disablePreviousMonth); } } if (opts.direction==='today-past' || opts.direction==='past') { if (month.add({Y:1}).diff(moment(), 'years') < 0) { this.disableNextYear = false; util.removeClassName(this.container, classes.disableNextYear); } else { this.disableNextYear = true; util.addClassName(this.container, classes.disableNextYear); } } else if (opts.direction==='today-future' || opts.direction==='future') { if (month.subtract({Y:1}).diff(moment(), 'years') > 0) { this.disablePreviousYear = false; util.removeClassName(this.container, classes.disablePreviousYear); } else { this.disablePreviousYear = true; util.addClassName(this.container, classes.disablePreviousYear); } } } } } var parseDates = function (input, delimiter, format) { var output = []; if (typeof input === 'string') { input = input.split(delimiter); } else if (!util.isArray(input)) { input = [input]; } var c = input.length; i = 0; do { if (input[i]) output.push( moment(input[i], format).hours(12) ); } while (++i < c); return output; } window.Kalendae = Kalendae; var util = Kalendae.util = { isIE8: function() { return !!( (/msie 8./i).test(navigator.appVersion) && !(/opera/i).test(navigator.userAgent) && window.ActiveXObject && XDomainRequest && !window.msPerformance ); }, // ELEMENT FUNCTIONS $: function (elem) { return (typeof elem == 'string') ? document.getElementById(elem) : elem; }, $$: function (selector) { return document.querySelectorAll(selector); }, make: function (tagName, attributes, attach) { var k, e = document.createElement(tagName); if (!!attributes) for (k in attributes) if (attributes.hasOwnProperty(k)) e.setAttribute(k, attributes[k]); if (!!attach) attach.appendChild(e); return e; }, // Returns true if the DOM element is visible, false if it's hidden. // Checks if display is anything other than none. isVisible: function (elem) { // shamelessly copied from jQuery return elem.offsetWidth > 0 || elem.offsetHeight > 0; }, getStyle: function (elem, styleProp) { var y; if (elem.currentStyle) { y = elem.currentStyle[styleProp]; } else if (window.getComputedStyle) { y = window.getComputedStyle(elem, null)[styleProp]; } return y; }, domReady:function (f){/in/.test(document.readyState) ? setTimeout(function() {util.domReady(f);},9) : f()}, // Adds a listener callback to a DOM element which is fired on a specified // event. Callback is sent the event object and the element that triggered the event addEvent: function (elem, eventName, callback) { var listener = function (event) { event = event || window.event; var target = event.target || event.srcElement; var block = callback.apply(elem, [event, target]); if (block === false) { if (!!event.preventDefault) event.preventDefault(); else { event.returnValue = false; event.cancelBubble = true; } } return block; }; if (elem.attachEvent) { // IE only. The "on" is mandatory. elem.attachEvent("on" + eventName, listener); } else { // Other browsers. elem.addEventListener(eventName, listener, false); } return listener; }, // Removes a listener callback from a DOM element which is fired on a specified // event. removeEvent: function (elem, event, listener) { if (elem.detachEvent) { // IE only. The "on" is mandatory. elem.detachEvent("on" + event, listener); } else { // Other browsers. elem.removeEventListener(event, listener, false); } }, hasClassName: function(elem, className) { //copied and modified from Prototype.js if (!(elem = util.$(elem))) return false; var eClassName = elem.className; return (eClassName.length > 0 && (eClassName == className || new RegExp("(^|\\s)" + className + "(\\s|$)").test(eClassName))); }, addClassName: function(elem, className) { //copied and modified from Prototype.js if (!(elem = util.$(elem))) return; if (!util.hasClassName(elem, className)) elem.className += (elem.className ? ' ' : '') + className; }, removeClassName: function(elem, className) { //copied and modified from Prototype.js if (!(elem = util.$(elem))) return; elem.className = util.trimString(elem.className.replace(new RegExp("(^|\\s+)" + className + "(\\s+|$)"), ' ')); }, isFixed: function (elem) { do { if (util.getStyle(elem, 'position') === 'fixed') return true; } while ((elem = elem.offsetParent)); return false; }, getPosition: function (elem, isInner) { var x = elem.offsetLeft, y = elem.offsetTop, r = {}; if (!isInner) { while ((elem = elem.offsetParent)) { x += elem.offsetLeft; y += elem.offsetTop; } } r[0] = r.left = x; r[1] = r.top = y; return r; }, getHeight: function (elem) { return elem.offsetHeight || elem.scrollHeight; }, getWidth: function (elem) { return elem.offsetWidth || elem.scrollWidth; }, // TEXT FUNCTIONS trimString: function (input) { return input.replace(/^\s+/, '').replace(/\s+$/, ''); }, // OBJECT FUNCTIONS merge: function () { /* Combines multiple objects into one. * Syntax: util.extend([true], object1, object2, ... objectN) * If first argument is true, function will merge recursively. */ var deep = (arguments[0]===true), d = {}, i = deep?1:0; var _c = function (a, b) { if (typeof b !== 'object') return; for (var k in b) if (b.hasOwnProperty(k)) { //if property is an object or array, merge the contents instead of overwriting, if extend() was called as such if (deep && typeof a[k] === 'object' && typeof b[k] === 'object') _update(a[k], b[k]); else a[k] = b[k]; } return a; } for (; i < arguments.length; i++) { _c(d, arguments[i]); } return d; }, isArray: function (array) { return !( !array || (!array.length || array.length === 0) || typeof array !== 'object' || !array.constructor || array.nodeType || array.item ); } }; //auto-initializaiton code Kalendae.util.domReady(function () { var els = util.$$('.auto-kal'), i = els.length, e; while (i--) { e = els[i]; if (e.tagName === 'INPUT') { //if element is an input, bind a popup calendar to the input. new Kalendae.Input(e); } else { //otherwise, insert a flat calendar into the element. new Kalendae({attachTo:e}); } } }); Kalendae.Input = function (targetElement, options) { var $input = this.input = util.$(targetElement), overwriteInput; if (!$input || $input.tagName !== 'INPUT') throw "First argument for Kalendae.Input must be an element or a valid element id."; var self = this, classes = self.classes opts = self.settings = util.merge(self.defaults, options); //force attachment to the body opts.attachTo = window.document.body; //if no override provided, use the input's contents if (!opts.selected) opts.selected = $input.value; else overwriteInput = true; //call our parent constructor Kalendae.call(self, opts); //create the close button if (opts.closeButton) { var $closeButton = util.make('a', {'class':classes.closeButton}, self.container) util.addEvent($closeButton, 'click', function () { $input.blur(); }); } if (overwriteInput) $input.value = self.getSelected(); var $container = self.container, noclose = false; $container.style.display = 'none'; util.addClassName($container, classes.positioned); util.addEvent($container, 'mousedown', function (event, target) { noclose = true; //IE8 doesn't obey event blocking when it comes to focusing, so we have to do this shit. }); util.addEvent(window.document, 'mousedown', function (event, target) { noclose = false; }); util.addEvent($input, 'focus', function () { self.setSelected(this.value); self.show(); }); util.addEvent($input, 'blur', function () { if (noclose) { noclose = false; $input.focus(); } else self.hide(); }); util.addEvent($input, 'keyup', function (event) { self.setSelected(this.value); }); self.subscribe('change', function () { $input.value = self.getSelected(); }); }; Kalendae.Input.prototype = util.merge(Kalendae.prototype, { defaults : util.merge(Kalendae.prototype.defaults, { format: 'MM/DD/YYYY', side: 'bottom', closeButton: true, offsetLeft: 0, offsetTop: 0 }), classes : util.merge(Kalendae.prototype.classes, { positioned : 'k-floating', closeButton: 'k-btn-close' }), show : function () { var $container = this.container, style = $container.style, $input = this.input, pos = util.getPosition($input); style.display = ''; switch (opts.side) { case 'left': style.left = (pos.left - util.getWidth($container) + this.settings.offsetLeft) + 'px'; style.top = (pos.top + this.settings.offsetTop) + 'px'; break; case 'right': style.left = (pos.left + util.getWidth($input)) + 'px'; style.top = (pos.top + this.settings.offsetTop) + 'px'; break; case 'top': style.left = (pos.left + this.settings.offsetLeft) + 'px'; style.top = (pos.top - util.getHeight($container) + this.settings.offsetTop) + 'px'; break; case 'bottom': /* falls through */ default: style.left = (pos.left + this.settings.offsetLeft) + 'px'; style.top = (pos.top + util.getHeight($input) + this.settings.offsetTop) + 'px'; break; } style.position = util.isFixed($input) ? 'fixed' : 'absolute'; }, hide : function () { this.container.style.display = 'none'; } }); /*! * MinPubSub, modified for use on Kalendae * Copyright(c) 2011 Daniel Lamb * https://github.com/daniellmb/MinPubSub * MIT Licensed */ var MinPubSub = function(d){ if (!d) d = this; // the topic/subscription hash var cache = d.c_ || {}; //check for "c_" cache for unit testing d.publish = function(/* String */ topic, /* Object */ target, /* Array? */ args){ // summary: // Publish some data on a named topic. // topic: String // The channel to publish on // args: Array? // The data to publish. Each array item is converted into an ordered // arguments on the subscribed functions. // // example: // Publish stuff on '/some/topic'. Anything subscribed will be called // with a function signature like: function(a,b,c){ ... } // // publish("/some/topic", ["a","b","c"]); var subs = cache[topic], len = subs ? subs.length : 0, r; //can change loop or reverse array if the order matters while(len--){ r = subs[len].apply(target, args || []); if (typeof r === 'boolean') return r; } }; d.subscribe = function(/* String */ topic, /* Function */ callback, /* Boolean */ topPriority){ // summary: // Register a callback on a named topic. // topic: String // The channel to subscribe to // callback: Function // The handler event. Anytime something is publish'ed on a // subscribed channel, the callback will be called with the // published array as ordered arguments. // // returns: Array // A handle which can be used to unsubscribe this particular subscription. // // example: // subscribe("/some/topic", function(a, b, c){ /* handle data */ }); if(!cache[topic]){ cache[topic] = []; } if (topPriority) cache[topic].push(callback); else cache[topic].unshift(callback); return [topic, callback]; // Array }; d.unsubscribe = function(/* Array */ handle){ // summary: // Disconnect a subscribed function for a topic. // handle: Array // The return value from a subscribe call. // example: // var handle = subscribe("/some/topic", function(){}); // unsubscribe(handle); var subs = cache[handle[0]], callback = handle[1], len = subs ? subs.length : 0; while(len--){ if(subs[len] === callback){ subs.splice(len, 1); } } }; };// moment.js // Altered slightly for use in Kalendae.js // version : 1.5.0 // author : Tim Wood // license : MIT // momentjs.com var moment = Kalendae.moment = (function (Date, undefined) { var moment, round = Math.round, languages = {}, hasModule = (typeof module !== 'undefined'), paramsToParse = 'months|monthsShort|monthsParse|weekdays|weekdaysShort|longDateFormat|calendar|relativeTime|ordinal|meridiem'.split('|'), i, jsonRegex = /^\/?Date\((\-?\d+)/i, charactersToReplace = /(\[[^\[]*\])|(\\)?(Mo|MM?M?M?|Do|DDDo|DD?D?D?|dddd?|do?|w[o|w]?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|zz?|ZZ?|LT|LL?L?L?)/g, nonuppercaseLetters = /[^A-Z]/g, timezoneRegex = /\([A-Za-z ]+\)|:[0-9]{2} [A-Z]{3} /g, tokenCharacters = /(\\)?(MM?M?M?|dd?d?d|DD?D?D?|YYYY|YY|a|A|hh?|HH?|mm?|ss?|ZZ?|T)/g, inputCharacters = /(\\)?([0-9]+|([a-zA-Z\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF])+|([\+\-]\d\d:?\d\d))/gi, isoRegex = /\d{4}.\d\d.\d\d(T(\d\d(.\d\d(.\d\d)?)?)?([\+\-]\d\d:?\d\d)?)?/, isoFormat = 'YYYY-MM-DDTHH:mm:ssZ', isoTimes = [ ['HH:mm:ss', /T\d\d:\d\d:\d\d/], ['HH:mm', /T\d\d:\d\d/], ['HH', /T\d\d/] ], timezoneParseRegex = /([\+\-]|\d\d)/gi, VERSION = "1.5.0", shortcuts = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|'); // Moment prototype object function Moment(date, isUTC) { this._d = date; this._isUTC = !!isUTC; } // left zero fill a number // see http://jsperf.com/left-zero-filling for performance comparison function leftZeroFill(number, targetLength) { var output = number + ''; while (output.length < targetLength) { output = '0' + output; } return output; } // helper function for _.addTime and _.subtractTime function dateAddRemove(date, _input, adding, val) { var isString = (typeof _input === 'string'), input = isString ? {} : _input, ms, d, M, currentDate; if (isString && val) { input[_input] = +val; } ms = (input.ms || input.milliseconds || 0) + (input.s || input.seconds || 0) * 1e3 + // 1000 (input.m || input.minutes || 0) * 6e4 + // 1000 * 60 (input.h || input.hours || 0) * 36e5; // 1000 * 60 * 60 d = (input.d || input.days || 0) + (input.w || input.weeks || 0) * 7; M = (input.M || input.months || 0) + (input.y || input.years || 0) * 12; if (ms) { date.setTime(+date + ms * adding); } if (d) { date.setDate(date.getDate() + d * adding); } if (M) { currentDate = date.getDate(); date.setDate(1); date.setMonth(date.getMonth() + M * adding); date.setDate(Math.min(new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate(), currentDate)); } return date; } // check if is an array function isArray(input) { return Object.prototype.toString.call(input) === '[object Array]'; } // convert an array to a date. // the array should mirror the parameters below // note: all values past the year are optional and will default to the lowest possible value. // [year, month, day , hour, minute, second, millisecond] function dateFromArray(input) { return new Date(input[0], input[1] || 0, input[2] || 1, input[3] || 0, input[4] || 0, input[5] || 0, input[6] || 0); } // format date using native date object function formatMoment(m, inputString) { var currentMonth = m.month(), currentDate = m.date(), currentYear = m.year(), currentDay = m.day(), currentHours = m.hours(), currentMinutes = m.minutes(), currentSeconds = m.seconds(), currentZone = -m.zone(), ordinal = moment.ordinal, meridiem = moment.meridiem; // check if the character is a format // return formatted string or non string. // // uses switch/case instead of an object of named functions (like http://phpjs.org/functions/date:380) // for minification and performance // see http://jsperf.com/object-of-functions-vs-switch for performance comparison function replaceFunction(input) { // create a couple variables to be used later inside one of the cases. var a, b; switch (input) { // MONTH case 'M' : return currentMonth + 1; case 'Mo' : return (currentMonth + 1) + ordinal(currentMonth + 1); case 'MM' : return leftZeroFill(currentMonth + 1, 2); case 'MMM' : return moment.monthsShort[currentMonth]; case 'MMMM' : return moment.months[currentMonth]; // DAY OF MONTH case 'D' : return currentDate; case 'Do' : return currentDate + ordinal(currentDate); case 'DD' : return leftZeroFill(currentDate, 2); // DAY OF YEAR case 'DDD' : a = new Date(currentYear, currentMonth, currentDate); b = new Date(currentYear, 0, 1); return ~~ (((a - b) / 864e5) + 1.5); case 'DDDo' : a = replaceFunction('DDD'); return a + ordinal(a); case 'DDDD' : return leftZeroFill(replaceFunction('DDD'), 3); // WEEKDAY case 'd' : return currentDay; case 'do' : return currentDay + ordinal(currentDay); case 'ddd' : return moment.weekdaysShort[currentDay]; case 'dddd' : return moment.weekdays[currentDay]; // WEEK OF YEAR case 'w' : a = new Date(currentYear, currentMonth, currentDate - currentDay + 5); b = new Date(a.getFullYear(), 0, 4); return ~~ ((a - b) / 864e5 / 7 + 1.5); case 'wo' : a = replaceFunction('w'); return a + ordinal(a); case 'ww' : return leftZeroFill(replaceFunction('w'), 2); // YEAR case 'YY' : return leftZeroFill(currentYear % 100, 2); case 'YYYY' : return currentYear; // AM / PM case 'a' : return currentHours > 11 ? meridiem.pm : meridiem.am; case 'A' : return currentHours > 11 ? meridiem.PM : meridiem.AM; // 24 HOUR case 'H' : return currentHours; case 'HH' : return leftZeroFill(currentHours, 2); // 12 HOUR case 'h' : return currentHours % 12 || 12; case 'hh' : return leftZeroFill(currentHours % 12 || 12, 2); // MINUTE case 'm' : return currentMinutes; case 'mm' : return leftZeroFill(currentMinutes, 2); // SECOND case 's' : return currentSeconds; case 'ss' : return leftZeroFill(currentSeconds, 2); // TIMEZONE case 'zz' : // depreciating 'zz' fall through to 'z' case 'z' : return (m._d.toString().match(timezoneRegex) || [''])[0].replace(nonuppercaseLetters, ''); case 'Z' : return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(Math.abs(currentZone) / 60), 2) + ':' + leftZeroFill(~~(Math.abs(currentZone) % 60), 2); case 'ZZ' : return (currentZone < 0 ? '-' : '+') + leftZeroFill(~~(10 * Math.abs(currentZone) / 6), 4); // LONG DATES case 'L' : case 'LL' : case 'LLL' : case 'LLLL' : case 'LT' : return formatMoment(m, moment.longDateFormat[input]); // DEFAULT default : return input.replace(/(^\[)|(\\)|\]$/g, ""); } } return inputString.replace(charactersToReplace, replaceFunction); } // date from string and format string function makeDateFromStringAndFormat(string, format) { var inArray = [0, 0, 1, 0, 0, 0, 0], timezoneHours = 0, timezoneMinutes = 0, isUsingUTC = false, inputParts = string.match(inputCharacters), formatParts = format.match(tokenCharacters), len = Math.min(inputParts.length, formatParts.length), i, isPm; // function to convert string input to date function addTime(format, input) { var a; switch (format) { // MONTH case 'M' : // fall through to MM case 'MM' : inArray[1] = ~~input - 1; break; case 'MMM' : // fall through to MMMM case 'MMMM' : for (a = 0; a < 12; a++) { if (moment.monthsParse[a].test(input)) { inArray[1] = a; break; } } break; // DAY OF MONTH case 'D' : // fall through to DDDD case 'DD' : // fall through to DDDD case 'DDD' : // fall through to DDDD case 'DDDD' : inArray[2] = ~~input; break; // YEAR case 'YY' : input = ~~input; inArray[0] = input + (input > 70 ? 1900 : 2000); break; case 'YYYY' : inArray[0] = ~~Math.abs(input); break; // AM / PM case 'a' : // fall through to A case 'A' : isPm = (input.toLowerCase() === 'pm'); break; // 24 HOUR case 'H' : // fall through to hh case 'HH' : // fall through to hh case 'h' : // fall through to hh case 'hh' : inArray[3] = ~~input; break; // MINUTE case 'm' : // fall through to mm case 'mm' : inArray[4] = ~~input; break; // SECOND case 's' : // fall through to ss case 'ss' : inArray[5] = ~~input; break; // TIMEZONE case 'Z' : // fall through to ZZ case 'ZZ' : isUsingUTC = true; a = (input || '').match(timezoneParseRegex); if (a && a[1]) { timezoneHours = ~~a[1]; } if (a && a[2]) { timezoneMinutes = ~~a[2]; } // reverse offsets if (a && a[0] === '+') { timezoneHours = -timezoneHours; timezoneMinutes = -timezoneMinutes; } break; } } for (i = 0; i < len; i++) { addTime(formatParts[i], inputParts[i]); } // handle am pm if (isPm && inArray[3] < 12) { inArray[3] += 12; } // if is 12 am, change hours to 0 if (isPm === false && inArray[3] === 12) { inArray[3] = 0; } // handle timezone inArray[3] += timezoneHours; inArray[4] += timezoneMinutes; // return return isUsingUTC ? new Date(Date.UTC.apply({}, inArray)) : dateFromArray(inArray); } // compare two arrays, return the number of differences function compareArrays(array1, array2) { var len = Math.min(array1.length, array2.length), lengthDiff = Math.abs(array1.length - array2.length), diffs = 0, i; for (i = 0; i < len; i++) { if (~~array1[i] !== ~~array2[i]) { diffs++; } } return diffs + lengthDiff; } // date from string and array of format strings function makeDateFromStringAndArray(string, formats) { var output, inputParts = string.match(inputCharacters), scores = [], scoreToBeat = 99, i, curDate, curScore; for (i = 0; i < formats.length; i++) { curDate = makeDateFromStringAndFormat(string, formats[i]); curScore = compareArrays(inputParts, formatMoment(new Moment(curDate), formats[i]).match(inputCharacters)); if (curScore < scoreToBeat) { scoreToBeat = curScore; output = curDate; } } return output; } // date from iso format function makeDateFromString(string) { var format = 'YYYY-MM-DDT', i; if (isoRegex.exec(string)) { for (i = 0; i < 3; i++) { if (isoTimes[i][1].exec(string)) { format += isoTimes[i][0]; break; } } return makeDateFromStringAndFormat(string, format + 'Z'); } return new Date(string); } // helper function for _date.from() and _date.fromNow() function substituteTimeAgo(string, number, withoutSuffix) { var rt = moment.relativeTime[string]; return (typeof rt === 'function') ? rt(number || 1, !!withoutSuffix, string) : rt.replace(/%d/i, number || 1); } function relativeTime(milliseconds, withoutSuffix) { var seconds = round(Math.abs(milliseconds) / 1000), minutes = round(seconds / 60), hours = round(minutes / 60), days = round(hours / 24), years = round(days / 365), args = seconds < 45 && ['s', seconds] || minutes === 1 && ['m'] || minutes < 45 && ['mm', minutes] || hours === 1 && ['h'] || hours < 22 && ['hh', hours] || days === 1 && ['d'] || days <= 25 && ['dd', days] || days <= 45 && ['M'] || days < 345 && ['MM', round(days / 30)] || years === 1 && ['y'] || ['yy', years]; args[2] = withoutSuffix; return substituteTimeAgo.apply({}, args); } moment = function (input, format) { if (input === null || input === '') { return null; } var date, matched; // parse Moment object if (input && input._d instanceof Date) { date = new Date(+input._d); // parse string and format } else if (format) { if (isArray(format)) { date = makeDateFromStringAndArray(input, format); } else { date = makeDateFromStringAndFormat(input, format); } // evaluate it as a JSON-encoded date } else { matched = jsonRegex.exec(input); date = input === undefined ? new Date() : matched ? new Date(+matched[1]) : input instanceof Date ? input : isArray(input) ? dateFromArray(input) : typeof input === 'string' ? makeDateFromString(input) : new Date(input); } return new Moment(date); }; // creating with utc moment.utc = function (input, format) { if (isArray(input)) { return new Moment(new Date(Date.UTC.apply({}, input)), true); } return (format && input) ? moment(input + ' 0', format + ' Z').utc() : moment(input).utc(); }; // humanizeDuration moment.humanizeDuration = function (num, type, withSuffix) { var difference = +num, rel = moment.relativeTime, output; switch (type) { case "seconds" : difference *= 1000; // 1000 break; case "minutes" : difference *= 60000; // 60 * 1000 break; case "hours" : difference *= 3600000; // 60 * 60 * 1000 break; case "days" : difference *= 86400000; // 24 * 60 * 60 * 1000 break; case "weeks" : difference *= 604800000; // 7 * 24 * 60 * 60 * 1000 break; case "months" : difference *= 2592000000; // 30 * 24 * 60 * 60 * 1000 break; case "years" : difference *= 31536000000; // 365 * 24 * 60 * 60 * 1000 break; default : withSuffix = !!type; break; } output = relativeTime(difference, !withSuffix); return withSuffix ? (difference <= 0 ? rel.past : rel.future).replace(/%s/i, output) : output; }; // version number moment.version = VERSION; // default format moment.defaultFormat = isoFormat; // language switching and caching moment.lang = function (key, values) { var i, param, req, parse = []; if (values) { for (i = 0; i < 12; i++) { parse[i] = new RegExp('^' + values.months[i] + '|^' + values.monthsShort[i].replace('.', ''), 'i'); } values.monthsParse = values.monthsParse || parse; languages[key] = values; } if (languages[key]) { for (i = 0; i < paramsToParse.length; i++) { param = paramsToParse[i]; moment[param] = languages[key][param] || moment[param]; } } else { if (hasModule) { req = require('./lang/' + key); moment.lang(key, req); } } }; // set default language moment.lang('en', { months : "January_February_March_April_May_June_July_August_September_October_November_December".split("_"), monthsShort : "Jan_Feb_Mar_Apr_May_Jun_Jul_Aug_Sep_Oct_Nov_Dec".split("_"), weekdays : "Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_"), weekdaysShort : "Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_"), longDateFormat : { LT : "h:mm A", L : "MM/DD/YYYY", LL : "MMMM D YYYY", LLL : "MMMM D YYYY LT", LLLL : "dddd, MMMM D YYYY LT" }, meridiem : { AM : 'AM', am : 'am', PM : 'PM', pm : 'pm' }, calendar : { sameDay : '[Today at] LT', nextDay : '[Tomorrow at] LT', nextWeek : 'dddd [at] LT', lastDay : '[Yesterday at] LT', lastWeek : '[last] dddd [at] LT', sameElse : 'L' }, relativeTime : { future : "in %s", past : "%s ago", s : "a few seconds", m : "a minute", mm : "%d minutes", h : "an hour", hh : "%d hours", d : "a day", dd : "%d days", M : "a month", MM : "%d months", y : "a year", yy : "%d years" }, ordinal : function (number) { var b = number % 10; return (~~ (number % 100 / 10) === 1) ? 'th' : (b === 1) ? 'st' : (b === 2) ? 'nd' : (b === 3) ? 'rd' : 'th'; } }); // compare moment object moment.isMoment = function (obj) { return obj instanceof Moment; }; // shortcut for prototype moment.fn = Moment.prototype = { clone : function () { return moment(this); }, valueOf : function () { return +this._d; }, 'native' : function () { return this._d; }, toString : function () { return this._d.toString(); }, toDate : function () { return this._d; }, utc : function () { this._isUTC = true; return this; }, local : function () { this._isUTC = false; return this; }, format : function (inputString) { return formatMoment(this, inputString ? inputString : moment.defaultFormat); }, add : function (input, val) { this._d = dateAddRemove(this._d, input, 1, val); return this; }, subtract : function (input, val) { this._d = dateAddRemove(this._d, input, -1, val); return this; }, diff : function (input, val, asFloat) { var inputMoment = moment(input), zoneDiff = (this.zone() - inputMoment.zone()) * 6e4, diff = this._d - inputMoment._d - zoneDiff, year = this.year() - inputMoment.year(), month = this.month() - inputMoment.month(), date = this.date() - inputMoment.date(), output; if (val === 'months') { output = year * 12 + month + date / 30; } else if (val === 'years') { output = year + month / 12; } else { output = val === 'seconds' ? diff / 1e3 : // 1000 val === 'minutes' ? diff / 6e4 : // 1000 * 60 val === 'hours' ? diff / 36e5 : // 1000 * 60 * 60 val === 'days' ? diff / 864e5 : // 1000 * 60 * 60 * 24 val === 'weeks' ? diff / 6048e5 : // 1000 * 60 * 60 * 24 * 7 diff; } return asFloat ? output : round(output); }, from : function (time, withoutSuffix) { return moment.humanizeDuration(this.diff(time), !withoutSuffix); }, fromNow : function (withoutSuffix) { return this.from(moment(), withoutSuffix); }, calendar : function () { var diff = this.diff(moment().sod(), 'days', true), calendar = moment.calendar, allElse = calendar.sameElse, format = diff < -6 ? allElse : diff < -1 ? calendar.lastWeek : diff < 0 ? calendar.lastDay : diff < 1 ? calendar.sameDay : diff < 2 ? calendar.nextDay : diff < 7 ? calendar.nextWeek : allElse; return this.format(typeof format === 'function' ? format.apply(this) : format); }, isLeapYear : function () { var year = this.year(); return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0; }, isDST : function () { return (this.zone() < moment([this.year()]).zone() || this.zone() < moment([this.year(), 5]).zone()); }, day : function (input) { var day = this._d.getDay(); return (typeof input === 'undefined') ? day : this.add({ d : input - day }); }, sod: function () { return this.clone() .hours(0) .minutes(0) .seconds(0) .milliseconds(0); }, eod: function () { // end of day = start of day plus 1 day, minus 1 millisecond return this.sod().add({ d : 1, ms : -1 }); }, zone : function () { return this._isUTC ? 0 : this._d.getTimezoneOffset(); }, daysInMonth : function () { return this.clone().month(this.month() + 1).date(0).date(); } }; // helper for adding shortcuts function makeShortcut(name, key) { moment.fn[name] = function (input) { var utc = this._isUTC ? 'UTC' : ''; if (typeof input !== 'undefined') { this._d['set' + utc + key](input); return this; } else { return this._d['get' + utc + key](); } }; } // loop through and add shortcuts (Month, Date, Hours, Minutes, Seconds, Milliseconds) for (i = 0; i < shortcuts.length; i ++) { makeShortcut(shortcuts[i].toLowerCase(), shortcuts[i]); } // add shortcut for year (uses different syntax than the getter/setter 'year' == 'FullYear') makeShortcut('year', 'FullYear'); return moment; })(Date); //function to reset the date object to 00:00 GMT moment.fn.stripTime = function () { this._d = new Date(Math.floor(this._d.valueOf() / 86400000) * 86400000); return this; } //function to get the total number of days since the epoch. moment.fn.yearDay = function (input) { var yearday = Math.floor(this._d / 86400000); return (typeof input === 'undefined') ? yearday : this.add({ d : input - yearday }); } today = moment().stripTime(); if (typeof jQuery !== 'undefined') { jQuery.fn.kalendae = function (options) { this.each(function (i, e) { if (e.tagName === 'INPUT') { //if element is an input, bind a popup calendar to the input. $(e).data('kalendae', new Kalendae.Input(e, options)); } else { //otherwise, insert a flat calendar into the element. $(e).data('kalendae', new Kalendae($.extend({}, {attachTo:e}, options))); } }); return this; } } })();