date-picker
Version:
A simple client-side date-picker
671 lines (576 loc) • 15.3 kB
JavaScript
!function() {
var global = this
global.DatePicker = ctor;
ctor.VERSION = '0.4.0';
ctor.prototype =
{ show: show
, hide: hide
, toggle: toggle
, render: render
, on: on
, off: off
, emit: emit
, trigger: emit
, resolveSelector: resolveSelector
, getOffset: getOffset
, rerender: rerender
, renderControls: renderControls
, renderHeaderLabels: renderHeaderLabels
, renderDateCells: renderDateCells
, nextMonth: nextMonth
, prevMonth: prevMonth
, dateCellClicked: dateCellClicked
, _removeFloater: _removeFloater
};
ctor.getCurrentMonth = getCurrentMonth;
ctor.getOverflowNext = getOverflowNext;
ctor.getOverflowPrev = getOverflowPrev;
ctor.parseDate = parseDate;
ctor.formatDate = formatDate;
function ctor(options) {
// It should not matter if "new" keyword is used!
if(this === global) {
return new ctor(options);
}
this.options =
{ weekStart: 1 // 1 == monday
, elm: null
, weekdays: [ 'su', 'mo', 'tu', 'we', 'th', 'fr', 'sa' ]
, months:
[ 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun'
, 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
]
, buttons: { next: 'Next', prev: 'Prev', close: 'x' }
, date: new Date()
, dateFormat: 'y/m/d'
, floatingOverlay: false
};
this._events = {};
this._elms = {};
if(options) {
_(options).keys().each(function(key) {
this.options[key] = options[key];
}, this);
}
_(this).bindAll('nextMonth', 'prevMonth', 'dateCellClicked');
};
function on(event, callback) {
if(typeof(callback) != 'function') {
throw new Error('Second argument needs to be a function');
}
if(!this._events[event]) {
this._events[event] = [];
}
this._events[event].push(callback);
return this;
};
function off(event, callback) {
if(!this._events[event]) {
return;
}
var index
while((index = this._events[event].indexOf(callback)) > -1) {
this._events[event].splice(index, 1);
}
return this;
};
function emit(event) {
var listeners = this._events[event]
, args = Array.prototype.slice.call(arguments, 1)
if(listeners) {
_(listeners).each(function(cb) {
cb.apply(null, args);
});
}
return this;
};
function dateCellClicked(e) {
var elm = e ? e.target : event.srcElement
, date = elm.getAttribute('data-date')
, dateFormat = this.options.dateFormat
if(!date) {
return;
}
date = date.split('/');
this.options.date.setFullYear(date[0]);
this.options.date.setMonth(date[1]-1);
this.options.date.setDate(date[2]);
if(this._elms.input) {
this._elms.input.value = formatDate(this.options.date, dateFormat);
}
this._visibleDate = this.options.date;
this.emit('change', this.options.date, this);
if(this._elms.floater) {
this.hide();
return;
}
this.rerender();
};
function nextMonth() {
this._visibleDate.setDate(1);
this._visibleDate.setMonth(this._visibleDate.getMonth() + 1);
this.rerender();
};
function prevMonth() {
this._visibleDate.setDate(1);
this._visibleDate.setMonth(this._visibleDate.getMonth() - 1);
this.rerender();
};
function rerender() {
if(!this.container) {
return this;
}
var frag = this.render()
this.container.innerHTML = '';
this.container.appendChild(frag);
return this;
};
function render() {
var frag = document.createDocumentFragment()
, now = this._visibleDate || this.options.date
, overlay = this._elms.floater && this.options.floatingOverlay
? createElement('div', { className: 'fzk-dp-overlay' })
: null
, showCloseButton = !!this._elms.floater
, hide = _(this.hide).bind(this)
if(!this._visibleDate) {
this._visibleDate = new Date(now.getTime());
}
if(overlay) {
frag.appendChild(overlay);
}
frag.appendChild(this.renderControls(showCloseButton));
frag.appendChild(this.renderHeaderLabels());
frag.appendChild(this.renderDateCells());
// Closing the overlay
if(overlay) {
overlay.onclick = hide;
}
if(showCloseButton) {
$$('.fzk-dp-btn-cls', frag).onclick = hide;
}
// Navigating between months
$$('.fzk-dp-btn-nxt', frag).onclick = this.nextMonth;
$$('.fzk-dp-btn-prv', frag).onclick = this.prevMonth;
// Selecting a date
$$('.fzk-dp-cells', frag).onclick = this.dateCellClicked;
return frag;
};
function renderControls(showCloseButton) {
var div = createElement('div', { className: 'fzk-dp-ctrls' })
, now = this._visibleDate
div.appendChild(createElement('label',
{ className: 'fzk-dp-month'
, innerHTML: this.options.months[now.getMonth()] + ' ' + now.getFullYear()
}
));
div.appendChild(createElement('button',
{ className: 'fzk-dp-btn-prv'
, innerHTML: this.options.buttons.prev
}
));
div.appendChild(createElement('button',
{ className: 'fzk-dp-btn-nxt'
, innerHTML: this.options.buttons.next
}
));
if(showCloseButton) {
div.appendChild(createElement('button',
{ className: 'fzk-dp-btn-cls'
, innerHTML: this.options.buttons.close
}
));
}
return div;
};
function renderHeaderLabels() {
var div = createElement('div', { className: 'fzk-dp-lbls' })
_(getWeekdays(this.options)).each(function(weekday) {
div.appendChild(createElement('label',
{ className: 'fzk-dp-cell'
, innerHTML: weekday
}
));
});
return div;
};
function renderDateCells() {
var div = createElement('div', { className: 'fzk-dp-cells' })
, date = this._visibleDate
, dateStr =
date.getFullYear() + '/'
+ padDate(date.getMonth() +1) + '/'
+ padDate(date.getDate())
, selected = this.options.date
, selectedStr =
selected.getFullYear() + '/'
+ padDate(selected.getMonth() +1) + '/'
+ padDate(selected.getDate())
, now = new Date()
, nowStr =
now.getFullYear() + '/'
+ padDate(now.getMonth() +1) + '/'
+ padDate(now.getDate())
_(getOverflowPrev(date, this.options)).each(addToDiv('fzk-dp-cell-prv'));
_(getCurrentMonth(date)).each(addToDiv(''));
_(getOverflowNext(date, this.options)).each(addToDiv('fzk-dp-cell-nxt'));
return div;
function addToDiv(className) {
return function(date) {
var opts =
{ className: 'fzk-dp-cell ' + className
, innerHTML: date.date
}
, data = { date: date.fullDate }
if(nowStr === date.fullDate) {
opts.className += ' fzk-dp-cell-today';
}
if(selectedStr === date.fullDate) {
opts.className += ' fzk-dp-cell-current';
}
div.appendChild(createElement('span', opts, data));
};
};
};
/**
* This method is static, and exposed as static
*/
function getWeekdays(opts) {
var days = []
, i
, current = opts.weekStart
for(i = 0; i < 7; i++) {
days[i] = opts.weekdays[current++];
if(current == 7) current = 0;
};
return days;
};
/**
* This method is static, and exposed as static
*/
function getCurrentMonth(now, opts) {
var year = now.getFullYear()
, month = padDate(now.getMonth() + 1)
, day = now.getDate()
// we do not need to subtract one here, since that has already been done
, lastDay = new Date(year, +month, 0).getDate()
, days = []
, i
for(i = 1; i <= lastDay; i++) {
var date = padDate(i)
days.push(
{ date: date
, fullDate: year + '/' + month + '/' + date
, current: day === i
}
);
}
return days;
};
/**
* This method is static, and exposed as static
*/
function getOverflowPrev(now, opts) {
var firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1)
if(firstDayOfMonth.getDay() == opts.weekStart) {
return [];
}
var previousMonth = new Date(now.getFullYear(), now.getMonth(), 0)
, year = previousMonth.getFullYear()
, month = padDate(previousMonth.getMonth() + 1)
, lastDate = previousMonth.getDate()
, currentDay = firstDayOfMonth.getDay()
, results = []
, i
, l = currentDay - opts.weekStart
if(l < 0) l += 7;
for(i = 0; i < l; i++) {
var currentDate = (lastDate - l + i + 1).toString()
results.push(
{ date: currentDate
, fullDate: year + '/' + month + '/' + currentDate
}
);
}
return results;
};
/**
* This method is static, and exposed as static
*/
function getOverflowNext(now, opts) {
var firstDayOfNextMonth = new Date(now.getFullYear(), now.getMonth()+1, 1)
if(firstDayOfNextMonth.getDay() == opts.weekStart) {
return [];
}
var year = firstDayOfNextMonth.getFullYear()
, month = padDate(firstDayOfNextMonth.getMonth() +1)
, result = []
, i = firstDayOfNextMonth.getDay() - opts.weekStart
, l = 7
, current = 1
if(i < 0) {
i += 7
}
for(; i < l; i++) {
var date = padDate(current++)
result.push(
{ date: date
, fullDate: year + '/' + month + '/' + date
}
);
}
return result;
};
/**
* This method is static
*/
function padDate(date) {
date = +date;
return date < 10 ? '0' + date : date.toString();
};
function show(selector) {
this.resolveSelector(selector || this.selector || this.options.elm);
if(this._elms.input) {
this.options.date =
parseDate(this._elms.input.value) || this.options.date;
}
if(!this._visibleDate) {
this._visibleDate = new Date(this.options.date.getTime());
}
ensureClassName(this.container, 'fzk-dp');
this.rerender();
this.showing = true;
return this.emit('show', this);
};
function hide() {
this.container.innerHTML = '';
this._removeFloater();
this.showing = false;
this._visibleDate = null;
this.selector = null;
return this.emit('hide', this);
};
function toggle(selector) {
if(this.showing && (!selector || this.selector === selector)) {
return this.hide();
}
return this.show(selector);
};
function _removeFloater() {
if(this._elms.floater) {
this._elms.floater.parentNode.removeChild(this._elms.floater);
this._elms.floater = null;
}
};
function resolveSelector(sel) {
this.container = (typeof(sel) === 'string')
? $$(sel)
: sel;
if(!this.container) {
throw new Error('"' + sel + '" does not resolve to an element!');
}
this.selector = sel;
if(this.container.tagName.toLowerCase() === 'input') {
this._elms.input = this.container;
}
this._removeFloater();
if(this._elms.input) {
if(this._elms.input.value) {
this._visibleDate = null;
this.options.date =
ctor.parseDate(this._elms.input.value, this.options.dateFormat);
}
this._elms.floater = this.container
= createElement('div', { className: 'fzk-dp-float' });
document.body.appendChild(this._elms.floater);
var offset = getOffset(this._elms.input);
this.container.style.left = offset.left + 'px';
this.container.style.top = (offset.top + offset.height) + 'px';
}
};
/**
* This code is most graciously stolen from jQuery:
* https://github.com/jquery/jquery/blob/7c23b77af2477417205/src/offset.js
*/
function getOffset(elm) {
var box = _(elm.getBoundingClientRect()).clone()
, doc = elm.ownerDocument
, body = doc.body
, docElm = doc.documentElement
, scroll =
{ top: docElm.scrollTop || body.scrollTop
, left: docElm.scrollLeft || body.scrollLeft
}
, client =
{ top: docElm.clientTop || body.clientTop || 0
, left: docElm.clientLeft || body.clientLeft || 0
}
if(box.height === undefined) {
box.height = ie8.height(elm);
}
return (
{ top: box.top + scroll.top - client.top
, left: box.left + scroll.left - client.left
, width: box.width
, height: box.height
}
);
};
var ie8 = {};
ie8.height = function(elm) {
var curStyle = elm.currentStyle
, suffixes = [ 'Top', 'Bottom' ]
, prefixes = [ 'border', 'padding' ]
, height = elm.offsetHeight
, i
, j
for(i = 0; i < 2; i++) {
for(j = 0; j < 2; j++) {
height -= parseFloat(curStyle[prefixes[i] + suffixes[j]]) || 0;
}
}
return height;
};
/**
* This method is static, and exposed as static
*/
function parseDate(str, format) {
if(!str) {
return null;
}
if(!format) {
format = 'y/m/d';
}
var keys = {}
, i = 1
, keyMap =
{ m: 'month', M: 'month'
, d: 'day', D: 'day'
, y: 'year', Y: 'year'
}
, parser = new RegExp(format.replace(/[yYdDmM]/g, parseDate))
, date = str.match(parser)
if(!date || date.length != 4 || !keys.day || !keys.month || !keys.year) {
return null;
}
if(date[keys.year].length == 2) {
date[keys.year] = (date[keys.year] < 25 ? '20' : '19') + date[keys.year];
}
return new Date(date[keys.year], date[keys.month]-1, date[keys.day]);
// This function is responsible for matching capture groups against
// keys
function parseDate(key) {
keys[keyMap[key]] = i++;
return '(\\d+)';
};
};
/**
* This method is static, and exposed as static
*/
function formatDate(date, format) {
return format.replace(/([YyMmDd])/g, function(key) {
switch(key) {
case 'd':
return padDate(date.getDate());
case 'D':
return date.getDate();
case 'm':
return padDate(date.getMonth()+1);
case 'M':
return date.getMonth()+1;
case 'y':
return date.getFullYear();
case 'Y':
return date.getFullYear().toString().substring(2);
}
});
};
function createElement(tag, opts, data) {
var elm = document.createElement(tag);
if(opts) {
_(opts).keys().each(function(key) {
elm[key] = opts[key];
});
}
if(data) {
_(data).keys().each(function(key) {
elm.setAttribute('data-' + key, data[key]);
});
}
return elm;
};
function ensureClassName(elm, className) {
if(!new RegExp('(^| )' + className + '($| )')
.test(elm.className))
{
elm.className += ' ' + className;
}
}
/**
* This is a (very!) raw implementation of underscore.
* It takes plenty of assumptions in order to be lean!
*/
function _(obj) {
return (
{ bind: bind
, bindAll: bindAll
, keys: keys
, each: forEach
, clone: clone
}
);
function clone() {
var cp = {}
, key
for(key in obj) {
cp[key] = obj[key];
}
return cp;
};
// keys() assumes obj to be any object
function keys() {
var keys = []
, key
for(key in obj) {
if(obj.hasOwnProperty(key)) {
keys.push(key);
}
}
return _(keys);
};
// bind() assumes obj to be a function
function bind(ctx) {
return function() {
return obj.apply(ctx, arguments);
};
};
function bindAll(methods) {
methods = Array.prototype.slice.call(arguments)
_(methods).each(function(func) {
obj[func] = _(obj[func]).bind(obj);
});
};
function forEach(func, ctx) {
var i = 0
, l = obj.length
for(; i < l; i++) {
func.call(ctx, obj[i], i, obj);
}
};
};
/**
* These two helpers are inverted from what the webkit console expects
* (which was derived from the Prototype framework) in order to match the
* jQuery behaviour more closely.
*
* $: Acts more or less like jQuery, except it is based on querySelectorAll.
* $$: Basically an optimized version of $(sel, scope)[0].
*/
function $(selector, scope) {
return Array.prototype.slice.call(
(scope || document).querySelectorAll(selector));
};
function $$(selector, scope) {
return (scope || document).querySelector(selector);
};
}();