Skip to content

Instantly share code, notes, and snippets.

@sirvine
Created June 18, 2012 15:13
Show Gist options
  • Save sirvine/2948855 to your computer and use it in GitHub Desktop.
Save sirvine/2948855 to your computer and use it in GitHub Desktop.
Kalendae 0.2
/********************************************************************
* 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<c;i++) {
out.push(this._sel[i].nativeDate());
}
return out;
},
getSelectedAsText : function (format) {
var out = [];
var i=0, c = this._sel.length;
for (;i<c;i++) {
out.push(this._sel[i].format(format || this.settings.format || 'YYYY-MM-DD'))
}
return out;
},
getSelectedRaw : function () {
var out = [];
var i=0, c = this._sel.length;
for (;i<c;i++) {
out.push(moment(this._sel[i]))
}
return out;
},
getSelected : function (format) {
var sel = this.getSelectedAsText(format);
switch (this.settings.mode) {
case 'range':
sel.splice(2); //shouldn't be more than two, but lets just make sure.
return sel.join(this.settings.rangeDelimiter);
case 'multiple':
return sel.join(this.settings.multipleDelimiter);
case 'single':
/* falls through */
default:
return sel[0];
}
},
isSelected : function (input) {
input = moment(input).yearDay();
if (input < 1 || !this._sel || this._sel.length < 1) return false;
switch (this.settings.mode) {
case 'range':
var a = this._sel[0] ? this._sel[0].yearDay() : 0,
b = this._sel[1] ? this._sel[1].yearDay() : 0;
if (a === input || b === input) return 1;
if (!a || !b) return 0;
if ((input > a && input < b) || (a<b && input < a && input > 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 <input> 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 <daniellmb.com>
* 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
//
// (c) 2011 Tim Wood
// Moment.js is freely distributable under the terms of the MIT license.
//
// Version 1.3.0
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,
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,
timezoneParseRegex = /([\+\-]|\d\d)/gi,
VERSION = "1.3.0",
shortcuts = 'Month|Date|Hours|Minutes|Seconds|Milliseconds'.split('|');
// Moment prototype object
function Moment(date) {
this._d = date;
}
// 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 formatDate(date, inputString) {
var m = new Moment(date),
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 (date.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 formatDate(date, 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),
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[1]) {
timezoneHours = ~~a[1];
}
if (a[2]) {
timezoneMinutes = ~~a[2];
}
// reverse offsets
if (a[0] === '-') {
timezoneHours = -timezoneHours;
timezoneMinutes = -timezoneMinutes;
}
break;
}
}
for (i = 0; i < formatParts.length; 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, formatDate(curDate, formats[i]).match(inputCharacters));
if (curScore < scoreToBeat) {
scoreToBeat = curScore;
output = curDate;
}
}
return output;
}
moment = function (input, format) {
if (input === null) {
return null;
}
var date;
// parse UnderscoreDate 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);
}
// parse everything else
} else {
date = input === undefined ? new Date() :
input instanceof Date ? input :
isArray(input) ? dateFromArray(input) :
new Date(input);
}
return new Moment(date);
};
// version number
moment.version = VERSION;
// 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';
}
});
// 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);
}
// shortcut for prototype
moment.fn = Moment.prototype = {
clone : function () {
return moment(this);
},
valueOf : function () {
return +this._d;
},
nativeDate : function () {
return this._d;
},
toString : function () {
return this._d.toString();
},
toDate : function () {
return this._d;
},
format : function (inputString) {
return formatDate(this._d, inputString);
},
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),
diff = this._d - inputMoment._d,
year = this.year() - inputMoment.year(),
month = this.month() - inputMoment.month(),
day = this.day() - inputMoment.day(),
output;
if (val === 'months') {
output = year * 12 + month + day / 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
val === 'days' ? diff / 3600 : diff;
}
return asFloat ? output : round(output);
},
from : function (time, withoutSuffix) {
var difference = this.diff(time),
rel = moment.relativeTime,
output = relativeTime(difference, withoutSuffix);
return withoutSuffix ? output : (difference <= 0 ? rel.past : rel.future).replace(/%s/i, output);
},
fromNow : function (withoutSuffix) {
return this.from(moment(), withoutSuffix);
},
calendar : function () {
var today = moment(),
todayAtZeroHour = moment([today.year(), today.month(), today.date()]),
diff = this.diff(todayAtZeroHour, '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();
},
day : function (input) {
var day = this._d.getDay();
return (typeof input === 'undefined') ? day :
this.add({ d : input - day });
}
};
// helper for adding shortcuts
function makeShortcut(name, key) {
moment.fn[name] = function (input) {
if (typeof input !== 'undefined') {
this._d['set' + key](input);
return this;
} else {
return this._d['get' + 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');
// add shortcut for timezone offset (no setter)
moment.fn.zone = function () {
return this._d.getTimezoneOffset();
};
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;
}
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment