1459 lines
44 KiB
JavaScript
1459 lines
44 KiB
JavaScript
(function (factory) {
|
|
if (typeof define === 'function' && define.amd) {
|
|
// AMD. Register as anonymous module.
|
|
define('datepicker', ['jquery'], factory);
|
|
} else if (typeof exports === 'object') {
|
|
// Node / CommonJS
|
|
factory(require('jquery'));
|
|
} else {
|
|
// Browser globals.
|
|
factory(jQuery);
|
|
}
|
|
})(function ($) {
|
|
|
|
'use strict';
|
|
|
|
var $window = $(window);
|
|
var document = window.document;
|
|
var $document = $(document);
|
|
var Number = window.Number;
|
|
var NAMESPACE = 'datepicker';
|
|
|
|
// Events
|
|
var EVENT_CLICK = 'click.' + NAMESPACE;
|
|
var EVENT_KEYUP = 'keyup.' + NAMESPACE;
|
|
var EVENT_FOCUS = 'focus.' + NAMESPACE;
|
|
var EVENT_RESIZE = 'resize.' + NAMESPACE;
|
|
var EVENT_SHOW = 'show.' + NAMESPACE;
|
|
var EVENT_HIDE = 'hide.' + NAMESPACE;
|
|
var EVENT_PICK = 'pick.' + NAMESPACE;
|
|
|
|
// RegExps
|
|
var REGEXP_FORMAT = /(y|m|d)+/g;
|
|
var REGEXP_DIGITS = /\d+/g;
|
|
var REGEXP_YEAR = /^\d{2,4}$/;
|
|
|
|
// Classes
|
|
var CLASS_INLINE = NAMESPACE + '-inline';
|
|
var CLASS_DROPDOWN = NAMESPACE + '-dropdown';
|
|
var CLASS_TOP_LEFT = NAMESPACE + '-top-left';
|
|
var CLASS_TOP_RIGHT = NAMESPACE + '-top-right';
|
|
var CLASS_BOTTOM_LEFT = NAMESPACE + '-bottom-left';
|
|
var CLASS_BOTTOM_RIGHT = NAMESPACE + '-bottom-right';
|
|
var CLASS_PLACEMENTS = [
|
|
CLASS_TOP_LEFT,
|
|
CLASS_TOP_RIGHT,
|
|
CLASS_BOTTOM_LEFT,
|
|
CLASS_BOTTOM_RIGHT
|
|
].join(' ');
|
|
var CLASS_HIDE = NAMESPACE + '-hide';
|
|
|
|
// Maths
|
|
var min = Math.min;
|
|
|
|
// Utilities
|
|
var toString = Object.prototype.toString;
|
|
|
|
function typeOf(obj) {
|
|
return toString.call(obj).slice(8, -1).toLowerCase();
|
|
}
|
|
|
|
function isString(str) {
|
|
return typeof str === 'string';
|
|
}
|
|
|
|
function isNumber(num) {
|
|
return typeof num === 'number' && !isNaN(num);
|
|
}
|
|
|
|
function isUndefined(obj) {
|
|
return typeof obj === 'undefined';
|
|
}
|
|
|
|
function isDate(date) {
|
|
return typeOf(date) === 'date';
|
|
}
|
|
|
|
function toArray(obj, offset) {
|
|
var args = [];
|
|
|
|
if (Array.from) {
|
|
return Array.from(obj).slice(offset || 0);
|
|
}
|
|
|
|
// This is necessary for IE8
|
|
if (isNumber(offset)) {
|
|
args.push(offset);
|
|
}
|
|
|
|
return args.slice.apply(obj, args);
|
|
}
|
|
|
|
// Custom proxy to avoid jQuery's guid
|
|
function proxy(fn, context) {
|
|
var args = toArray(arguments, 2);
|
|
|
|
return function () {
|
|
return fn.apply(context, args.concat(toArray(arguments)));
|
|
};
|
|
}
|
|
|
|
function isLeapYear(year) {
|
|
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
}
|
|
|
|
function getDaysInMonth(year, month) {
|
|
return [31, (isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month];
|
|
}
|
|
|
|
function parseFormat(format) {
|
|
var source = String(format).toLowerCase();
|
|
var parts = source.match(REGEXP_FORMAT);
|
|
var length;
|
|
var i;
|
|
|
|
if (!parts || parts.length === 0) {
|
|
throw new Error('Invalid date format.');
|
|
}
|
|
|
|
format = {
|
|
source: source,
|
|
parts: parts
|
|
};
|
|
|
|
length = parts.length;
|
|
|
|
for (i = 0; i < length; i++) {
|
|
switch (parts[i]) {
|
|
case 'dd':
|
|
case 'd':
|
|
format.hasDay = true;
|
|
break;
|
|
|
|
case 'mm':
|
|
case 'm':
|
|
format.hasMonth = true;
|
|
break;
|
|
|
|
case 'yyyy':
|
|
case 'yy':
|
|
format.hasYear = true;
|
|
break;
|
|
|
|
// No default
|
|
}
|
|
}
|
|
|
|
return format;
|
|
}
|
|
|
|
function Datepicker(element, options) {
|
|
options = $.isPlainObject(options) ? options : {};
|
|
|
|
if (options.language) {
|
|
options = $.extend({}, Datepicker.LANGUAGES[options.language], options);
|
|
}
|
|
|
|
this.$element = $(element);
|
|
this.options = $.extend({}, Datepicker.DEFAULTS, options);
|
|
this.isBuilt = false;
|
|
this.isShown = false;
|
|
this.isInput = false;
|
|
this.isInline = false;
|
|
this.initialValue = '';
|
|
this.initialDate = null;
|
|
this.startDate = null;
|
|
this.endDate = null;
|
|
this.init();
|
|
}
|
|
|
|
Datepicker.prototype = {
|
|
constructor: Datepicker,
|
|
|
|
init: function () {
|
|
var options = this.options;
|
|
var $this = this.$element;
|
|
var startDate = options.startDate;
|
|
var endDate = options.endDate;
|
|
var date = options.date;
|
|
|
|
this.$trigger = $(options.trigger || $this);
|
|
this.isInput = $this.is('input') || $this.is('textarea');
|
|
this.isInline = options.inline && (options.container || !this.isInput);
|
|
this.format = parseFormat(options.format);
|
|
this.initialValue = this.getValue();
|
|
date = this.parseDate(date || this.initialValue);
|
|
|
|
if (startDate) {
|
|
startDate = this.parseDate(startDate);
|
|
|
|
if (date.getTime() < startDate.getTime()) {
|
|
date = new Date(startDate);
|
|
}
|
|
|
|
this.startDate = startDate;
|
|
}
|
|
|
|
if (endDate) {
|
|
endDate = this.parseDate(endDate);
|
|
|
|
if (startDate && endDate.getTime() < startDate.getTime()) {
|
|
endDate = new Date(startDate);
|
|
}
|
|
|
|
if (date.getTime() > endDate.getTime()) {
|
|
date = new Date(endDate);
|
|
}
|
|
|
|
this.endDate = endDate;
|
|
}
|
|
|
|
this.date = date;
|
|
this.viewDate = new Date(date);
|
|
this.initialDate = new Date(this.date);
|
|
|
|
this.bind();
|
|
|
|
if (options.autoshow || this.isInline) {
|
|
this.show();
|
|
}
|
|
|
|
if (options.autopick) {
|
|
this.pick();
|
|
}
|
|
},
|
|
|
|
build: function () {
|
|
var options = this.options;
|
|
var $this = this.$element;
|
|
var $picker;
|
|
|
|
if (this.isBuilt) {
|
|
return;
|
|
}
|
|
|
|
this.isBuilt = true;
|
|
|
|
this.$picker = $picker = $(options.template);
|
|
this.$week = $picker.find('[data-view="week"]');
|
|
|
|
// Years view
|
|
this.$yearsPicker = $picker.find('[data-view="years picker"]');
|
|
this.$yearsPrev = $picker.find('[data-view="years prev"]');
|
|
this.$yearsNext = $picker.find('[data-view="years next"]');
|
|
this.$yearsCurrent = $picker.find('[data-view="years current"]');
|
|
this.$years = $picker.find('[data-view="years"]');
|
|
|
|
// Months view
|
|
this.$monthsPicker = $picker.find('[data-view="months picker"]');
|
|
this.$yearPrev = $picker.find('[data-view="year prev"]');
|
|
this.$yearNext = $picker.find('[data-view="year next"]');
|
|
this.$yearCurrent = $picker.find('[data-view="year current"]');
|
|
this.$months = $picker.find('[data-view="months"]');
|
|
|
|
// Days view
|
|
this.$daysPicker = $picker.find('[data-view="days picker"]');
|
|
this.$monthPrev = $picker.find('[data-view="month prev"]');
|
|
this.$monthNext = $picker.find('[data-view="month next"]');
|
|
this.$monthCurrent = $picker.find('[data-view="month current"]');
|
|
this.$days = $picker.find('[data-view="days"]');
|
|
|
|
if (this.isInline) {
|
|
$(options.container || $this).append($picker.addClass(CLASS_INLINE));
|
|
} else {
|
|
$(document.body).append($picker.addClass(CLASS_DROPDOWN));
|
|
$picker.addClass(CLASS_HIDE);
|
|
}
|
|
|
|
this.fillWeek();
|
|
},
|
|
|
|
unbuild: function () {
|
|
if (!this.isBuilt) {
|
|
return;
|
|
}
|
|
|
|
this.isBuilt = false;
|
|
this.$picker.remove();
|
|
},
|
|
|
|
bind: function () {
|
|
var options = this.options;
|
|
var $this = this.$element;
|
|
|
|
if ($.isFunction(options.show)) {
|
|
$this.on(EVENT_SHOW, options.show);
|
|
}
|
|
|
|
if ($.isFunction(options.hide)) {
|
|
$this.on(EVENT_HIDE, options.hide);
|
|
}
|
|
|
|
if ($.isFunction(options.pick)) {
|
|
$this.on(EVENT_PICK, options.pick);
|
|
}
|
|
|
|
if (this.isInput) {
|
|
$this.on(EVENT_KEYUP, $.proxy(this.keyup, this));
|
|
|
|
if (!options.trigger) {
|
|
$this.on(EVENT_FOCUS, $.proxy(this.show, this));
|
|
}
|
|
}
|
|
|
|
this.$trigger.on(EVENT_CLICK, $.proxy(this.show, this));
|
|
},
|
|
|
|
unbind: function () {
|
|
var options = this.options;
|
|
var $this = this.$element;
|
|
|
|
if ($.isFunction(options.show)) {
|
|
$this.off(EVENT_SHOW, options.show);
|
|
}
|
|
|
|
if ($.isFunction(options.hide)) {
|
|
$this.off(EVENT_HIDE, options.hide);
|
|
}
|
|
|
|
if ($.isFunction(options.pick)) {
|
|
$this.off(EVENT_PICK, options.pick);
|
|
}
|
|
|
|
if (this.isInput) {
|
|
$this.off(EVENT_KEYUP, this.keyup);
|
|
|
|
if (!options.trigger) {
|
|
$this.off(EVENT_FOCUS, this.show);
|
|
}
|
|
}
|
|
|
|
this.$trigger.off(EVENT_CLICK, this.show);
|
|
},
|
|
|
|
showView: function (view) {
|
|
var $yearsPicker = this.$yearsPicker;
|
|
var $monthsPicker = this.$monthsPicker;
|
|
var $daysPicker = this.$daysPicker;
|
|
var format = this.format;
|
|
|
|
if (format.hasYear || format.hasMonth || format.hasDay) {
|
|
switch (Number(view)) {
|
|
case 2:
|
|
case 'years':
|
|
$monthsPicker.addClass(CLASS_HIDE);
|
|
$daysPicker.addClass(CLASS_HIDE);
|
|
|
|
if (format.hasYear) {
|
|
this.fillYears();
|
|
$yearsPicker.removeClass(CLASS_HIDE);
|
|
} else {
|
|
this.showView(0);
|
|
}
|
|
|
|
break;
|
|
|
|
case 1:
|
|
case 'months':
|
|
$yearsPicker.addClass(CLASS_HIDE);
|
|
$daysPicker.addClass(CLASS_HIDE);
|
|
|
|
if (format.hasMonth) {
|
|
this.fillMonths();
|
|
$monthsPicker.removeClass(CLASS_HIDE);
|
|
} else {
|
|
this.showView(2);
|
|
}
|
|
|
|
break;
|
|
|
|
// case 0:
|
|
// case 'days':
|
|
default:
|
|
$yearsPicker.addClass(CLASS_HIDE);
|
|
$monthsPicker.addClass(CLASS_HIDE);
|
|
|
|
if (format.hasDay) {
|
|
this.fillDays();
|
|
$daysPicker.removeClass(CLASS_HIDE);
|
|
} else {
|
|
this.showView(1);
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
hideView: function () {
|
|
if (this.options.autohide) {
|
|
this.hide();
|
|
}
|
|
},
|
|
|
|
place: function () {
|
|
var options = this.options;
|
|
var $this = this.$element;
|
|
var $picker = this.$picker;
|
|
var containerWidth = $document.outerWidth();
|
|
var containerHeight = $document.outerHeight();
|
|
var elementWidth = $this.outerWidth();
|
|
var elementHeight = $this.outerHeight();
|
|
var width = $picker.width();
|
|
var height = $picker.height();
|
|
var offsets = $this.offset();
|
|
var left = offsets.left;
|
|
var top = offsets.top;
|
|
var offset = parseFloat(options.offset) || 10;
|
|
var placement = CLASS_TOP_LEFT;
|
|
|
|
if (top > height && top + elementHeight + height > containerHeight) {
|
|
top -= height + offset;
|
|
placement = CLASS_BOTTOM_LEFT;
|
|
} else {
|
|
top += elementHeight + offset;
|
|
}
|
|
|
|
if (left + width > containerWidth) {
|
|
left = left + elementWidth - width;
|
|
placement = placement.replace('left', 'right');
|
|
}
|
|
|
|
$picker.removeClass(CLASS_PLACEMENTS).addClass(placement).css({
|
|
top: top,
|
|
left: left,
|
|
zIndex: parseInt(options.zIndex, 10)
|
|
});
|
|
},
|
|
|
|
// A shortcut for triggering custom events
|
|
trigger: function (type, data) {
|
|
var e = $.Event(type, data);
|
|
|
|
this.$element.trigger(e);
|
|
|
|
return e;
|
|
},
|
|
|
|
createItem: function (data) {
|
|
var options = this.options;
|
|
var itemTag = options.itemTag;
|
|
var defaults = {
|
|
text: '',
|
|
view: '',
|
|
muted: false,
|
|
picked: false,
|
|
disabled: false
|
|
};
|
|
|
|
$.extend(defaults, data);
|
|
|
|
return (
|
|
'<' + itemTag + ' ' +
|
|
(defaults.disabled ? 'class="' + options.disabledClass + '"' :
|
|
defaults.picked ? 'class="' + options.pickedClass + '"' :
|
|
defaults.muted ? 'class="' + options.mutedClass + '"' : '') +
|
|
(defaults.view ? ' data-view="' + defaults.view + '"' : '') +
|
|
'>' +
|
|
defaults.text +
|
|
'</' + itemTag + '>'
|
|
);
|
|
},
|
|
|
|
fillAll: function () {
|
|
this.fillYears();
|
|
this.fillMonths();
|
|
this.fillDays();
|
|
},
|
|
|
|
fillWeek: function () {
|
|
var options = this.options;
|
|
var weekStart = parseInt(options.weekStart, 10) % 7;
|
|
var days = options.daysMin;
|
|
var list = '';
|
|
var i;
|
|
|
|
days = $.merge(days.slice(weekStart), days.slice(0, weekStart));
|
|
|
|
for (i = 0; i <= 6; i++) {
|
|
list += this.createItem({
|
|
text: days[i]
|
|
});
|
|
}
|
|
|
|
this.$week.html(list);
|
|
},
|
|
|
|
fillYears: function () {
|
|
var options = this.options;
|
|
var disabledClass = options.disabledClass || '';
|
|
var suffix = options.yearSuffix || '';
|
|
var filter = $.isFunction(options.filter) && options.filter;
|
|
var startDate = this.startDate;
|
|
var endDate = this.endDate;
|
|
var viewDate = this.viewDate;
|
|
var viewYear = viewDate.getFullYear();
|
|
var viewMonth = viewDate.getMonth();
|
|
var viewDay = viewDate.getDate();
|
|
var date = this.date;
|
|
var year = date.getFullYear();
|
|
var isPrevDisabled = false;
|
|
var isNextDisabled = false;
|
|
var isDisabled = false;
|
|
var isPicked = false;
|
|
var isMuted = false;
|
|
var list = '';
|
|
var start = -5;
|
|
var end = 6;
|
|
var i;
|
|
|
|
for (i = start; i <= end; i++) {
|
|
date = new Date(viewYear + i, viewMonth, viewDay);
|
|
isMuted = i === start || i === end;
|
|
isPicked = (viewYear + i) === year;
|
|
isDisabled = false;
|
|
|
|
if (startDate) {
|
|
isDisabled = date.getFullYear() < startDate.getFullYear();
|
|
|
|
if (i === start) {
|
|
isPrevDisabled = isDisabled;
|
|
}
|
|
}
|
|
|
|
if (!isDisabled && endDate) {
|
|
isDisabled = date.getFullYear() > endDate.getFullYear();
|
|
|
|
if (i === end) {
|
|
isNextDisabled = isDisabled;
|
|
}
|
|
}
|
|
|
|
if (!isDisabled && filter) {
|
|
isDisabled = filter.call(this.$element, date) === false;
|
|
}
|
|
|
|
list += this.createItem({
|
|
text: viewYear + i,
|
|
view: isDisabled ? 'year disabled' : isPicked ? 'year picked' : 'year',
|
|
muted: isMuted,
|
|
picked: isPicked,
|
|
disabled: isDisabled
|
|
});
|
|
}
|
|
|
|
this.$yearsPrev.toggleClass(disabledClass, isPrevDisabled);
|
|
this.$yearsNext.toggleClass(disabledClass, isNextDisabled);
|
|
this.$yearsCurrent.
|
|
toggleClass(disabledClass, true).
|
|
html((viewYear + start) + suffix + ' - ' + (viewYear + end) + suffix);
|
|
this.$years.html(list);
|
|
},
|
|
|
|
fillMonths: function () {
|
|
var options = this.options;
|
|
var disabledClass = options.disabledClass || '';
|
|
var months = options.monthsShort;
|
|
var filter = $.isFunction(options.filter) && options.filter;
|
|
var startDate = this.startDate;
|
|
var endDate = this.endDate;
|
|
var viewDate = this.viewDate;
|
|
var viewYear = viewDate.getFullYear();
|
|
var viewDay = viewDate.getDate();
|
|
var date = this.date;
|
|
var year = date.getFullYear();
|
|
var month = date.getMonth();
|
|
var isPrevDisabled = false;
|
|
var isNextDisabled = false;
|
|
var isDisabled = false;
|
|
var isPicked = false;
|
|
var list = '';
|
|
var i;
|
|
|
|
for (i = 0; i <= 11; i++) {
|
|
date = new Date(viewYear, i, viewDay);
|
|
isPicked = viewYear === year && i === month;
|
|
isDisabled = false;
|
|
|
|
if (startDate) {
|
|
isPrevDisabled = date.getFullYear() === startDate.getFullYear();
|
|
isDisabled = isPrevDisabled && date.getMonth() < startDate.getMonth();
|
|
}
|
|
|
|
if (!isDisabled && endDate) {
|
|
isNextDisabled = date.getFullYear() === endDate.getFullYear();
|
|
isDisabled = isNextDisabled && date.getMonth() > endDate.getMonth();
|
|
}
|
|
|
|
if (!isDisabled && filter) {
|
|
isDisabled = filter.call(this.$element, date) === false;
|
|
}
|
|
|
|
list += this.createItem({
|
|
index: i,
|
|
text: months[i],
|
|
view: isDisabled ? 'month disabled' : isPicked ? 'month picked' : 'month',
|
|
picked: isPicked,
|
|
disabled: isDisabled
|
|
});
|
|
}
|
|
|
|
this.$yearPrev.toggleClass(disabledClass, isPrevDisabled);
|
|
this.$yearNext.toggleClass(disabledClass, isNextDisabled);
|
|
this.$yearCurrent.
|
|
toggleClass(disabledClass, isPrevDisabled && isNextDisabled).
|
|
html(viewYear + options.yearSuffix || '');
|
|
this.$months.html(list);
|
|
},
|
|
|
|
fillDays: function () {
|
|
var options = this.options;
|
|
var disabledClass = options.disabledClass || '';
|
|
var suffix = options.yearSuffix || '';
|
|
var months = options.monthsShort;
|
|
var weekStart = parseInt(options.weekStart, 10) % 7;
|
|
var filter = $.isFunction(options.filter) && options.filter;
|
|
var startDate = this.startDate;
|
|
var endDate = this.endDate;
|
|
var viewDate = this.viewDate;
|
|
var viewYear = viewDate.getFullYear();
|
|
var viewMonth = viewDate.getMonth();
|
|
var prevViewYear = viewYear;
|
|
var prevViewMonth = viewMonth;
|
|
var nextViewYear = viewYear;
|
|
var nextViewMonth = viewMonth;
|
|
var date = this.date;
|
|
var year = date.getFullYear();
|
|
var month = date.getMonth();
|
|
var day = date.getDate();
|
|
var isPrevDisabled = false;
|
|
var isNextDisabled = false;
|
|
var isDisabled = false;
|
|
var isPicked = false;
|
|
var prevItems = [];
|
|
var nextItems = [];
|
|
var items = [];
|
|
var total = 42; // 6 rows and 7 columns on the days picker
|
|
var length;
|
|
var i;
|
|
var n;
|
|
|
|
// Days of previous month
|
|
// -----------------------------------------------------------------------
|
|
|
|
if (viewMonth === 0) {
|
|
prevViewYear -= 1;
|
|
prevViewMonth = 11;
|
|
} else {
|
|
prevViewMonth -= 1;
|
|
}
|
|
|
|
// The length of the days of previous month
|
|
length = getDaysInMonth(prevViewYear, prevViewMonth);
|
|
|
|
// The first day of current month
|
|
date = new Date(viewYear, viewMonth, 1);
|
|
|
|
// The visible length of the days of previous month
|
|
// [0,1,2,3,4,5,6] - [0,1,2,3,4,5,6] => [-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6]
|
|
n = date.getDay() - weekStart;
|
|
|
|
// [-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6] => [1,2,3,4,5,6,7]
|
|
if (n <= 0) {
|
|
n += 7;
|
|
}
|
|
|
|
if (startDate) {
|
|
isPrevDisabled = date.getTime() <= startDate.getTime();
|
|
}
|
|
|
|
for (i = length - (n - 1); i <= length; i++) {
|
|
date = new Date(prevViewYear, prevViewMonth, i);
|
|
isDisabled = false;
|
|
|
|
if (startDate) {
|
|
isDisabled = date.getTime() < startDate.getTime();
|
|
}
|
|
|
|
if (!isDisabled && filter) {
|
|
isDisabled = filter.call(this.$element, date) === false;
|
|
}
|
|
|
|
prevItems.push(this.createItem({
|
|
text: i,
|
|
view: 'day prev',
|
|
muted: true,
|
|
disabled: isDisabled
|
|
}));
|
|
}
|
|
|
|
// Days of next month
|
|
// -----------------------------------------------------------------------
|
|
|
|
if (viewMonth === 11) {
|
|
nextViewYear += 1;
|
|
nextViewMonth = 0;
|
|
} else {
|
|
nextViewMonth += 1;
|
|
}
|
|
|
|
// The length of the days of current month
|
|
length = getDaysInMonth(viewYear, viewMonth);
|
|
|
|
// The visible length of next month
|
|
n = total - (prevItems.length + length);
|
|
|
|
// The last day of current month
|
|
date = new Date(viewYear, viewMonth, length);
|
|
|
|
if (endDate) {
|
|
isNextDisabled = date.getTime() >= endDate.getTime();
|
|
}
|
|
|
|
for (i = 1; i <= n; i++) {
|
|
date = new Date(nextViewYear, nextViewMonth, i);
|
|
isDisabled = false;
|
|
|
|
if (endDate) {
|
|
isDisabled = date.getTime() > endDate.getTime();
|
|
}
|
|
|
|
if (!isDisabled && filter) {
|
|
isDisabled = filter.call(this.$element, date) === false;
|
|
}
|
|
|
|
nextItems.push(this.createItem({
|
|
text: i,
|
|
view: 'day next',
|
|
muted: true,
|
|
disabled: isDisabled
|
|
}));
|
|
}
|
|
|
|
// Days of current month
|
|
// -----------------------------------------------------------------------
|
|
|
|
for (i = 1; i <= length; i++) {
|
|
date = new Date(viewYear, viewMonth, i);
|
|
isPicked = viewYear === year && viewMonth === month && i === day;
|
|
isDisabled = false;
|
|
|
|
if (startDate) {
|
|
isDisabled = date.getTime() < startDate.getTime();
|
|
}
|
|
|
|
if (!isDisabled && endDate) {
|
|
isDisabled = date.getTime() > endDate.getTime();
|
|
}
|
|
|
|
if (!isDisabled && filter) {
|
|
isDisabled = filter.call(this.$element, date) === false;
|
|
}
|
|
|
|
items.push(this.createItem({
|
|
text: i,
|
|
view: isDisabled ? 'day disabled' : isPicked ? 'day picked' : 'day',
|
|
picked: isPicked,
|
|
disabled: isDisabled
|
|
}));
|
|
}
|
|
|
|
// Render days picker
|
|
// -----------------------------------------------------------------------
|
|
|
|
this.$monthPrev.toggleClass(disabledClass, isPrevDisabled);
|
|
this.$monthNext.toggleClass(disabledClass, isNextDisabled);
|
|
this.$monthCurrent.
|
|
toggleClass(disabledClass, isPrevDisabled && isNextDisabled).
|
|
html(
|
|
options.yearFirst ?
|
|
viewYear + suffix + ' ' + months[viewMonth] :
|
|
months[viewMonth] + ' ' + viewYear + suffix
|
|
);
|
|
this.$days.html(prevItems.join('') + items.join(' ') + nextItems.join(''));
|
|
},
|
|
|
|
click: function (e) {
|
|
var $target = $(e.target);
|
|
var viewDate = this.viewDate;
|
|
var viewYear;
|
|
var viewMonth;
|
|
var viewDay;
|
|
var isYear;
|
|
var year;
|
|
var view;
|
|
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
if ($target.hasClass('disabled')) {
|
|
return;
|
|
}
|
|
|
|
viewYear = viewDate.getFullYear();
|
|
viewMonth = viewDate.getMonth();
|
|
viewDay = viewDate.getDate();
|
|
view = $target.data('view');
|
|
|
|
switch (view) {
|
|
case 'years prev':
|
|
case 'years next':
|
|
viewYear = view === 'years prev' ? viewYear - 10 : viewYear + 10;
|
|
year = $target.text();
|
|
isYear = REGEXP_YEAR.test(year);
|
|
|
|
if (isYear) {
|
|
viewYear = parseInt(year, 10);
|
|
this.date = new Date(viewYear, viewMonth, min(viewDay, 28));
|
|
}
|
|
|
|
this.viewDate = new Date(viewYear, viewMonth, min(viewDay, 28));
|
|
this.fillYears();
|
|
|
|
if (isYear) {
|
|
this.showView(1);
|
|
this.pick('year');
|
|
}
|
|
|
|
break;
|
|
|
|
case 'year prev':
|
|
case 'year next':
|
|
viewYear = view === 'year prev' ? viewYear - 1 : viewYear + 1;
|
|
this.viewDate = new Date(viewYear, viewMonth, min(viewDay, 28));
|
|
this.fillMonths();
|
|
break;
|
|
|
|
case 'year current':
|
|
|
|
if (this.format.hasYear) {
|
|
this.showView(2);
|
|
}
|
|
|
|
break;
|
|
|
|
case 'year picked':
|
|
|
|
if (this.format.hasMonth) {
|
|
this.showView(1);
|
|
} else {
|
|
this.hideView();
|
|
}
|
|
|
|
break;
|
|
|
|
case 'year':
|
|
viewYear = parseInt($target.text(), 10);
|
|
this.date = new Date(viewYear, viewMonth, min(viewDay, 28));
|
|
this.viewDate = new Date(viewYear, viewMonth, min(viewDay, 28));
|
|
|
|
if (this.format.hasMonth) {
|
|
this.showView(1);
|
|
} else {
|
|
this.hideView();
|
|
}
|
|
|
|
this.pick('year');
|
|
break;
|
|
|
|
case 'month prev':
|
|
case 'month next':
|
|
viewMonth = view === 'month prev' ? viewMonth - 1 : view === 'month next' ? viewMonth + 1 : viewMonth;
|
|
this.viewDate = new Date(viewYear, viewMonth, min(viewDay, 28));
|
|
this.fillDays();
|
|
break;
|
|
|
|
case 'month current':
|
|
|
|
if (this.format.hasMonth) {
|
|
this.showView(1);
|
|
}
|
|
|
|
break;
|
|
|
|
case 'month picked':
|
|
|
|
if (this.format.hasDay) {
|
|
this.showView(0);
|
|
} else {
|
|
this.hideView();
|
|
}
|
|
|
|
break;
|
|
|
|
case 'month':
|
|
viewMonth = $.inArray($target.text(), this.options.monthsShort);
|
|
this.date = new Date(viewYear, viewMonth, min(viewDay, 28));
|
|
this.viewDate = new Date(viewYear, viewMonth, min(viewDay, 28));
|
|
|
|
if (this.format.hasDay) {
|
|
this.showView(0);
|
|
} else {
|
|
this.hideView();
|
|
}
|
|
|
|
this.pick('month');
|
|
break;
|
|
|
|
case 'day prev':
|
|
case 'day next':
|
|
case 'day':
|
|
viewMonth = view === 'day prev' ? viewMonth - 1 : view === 'day next' ? viewMonth + 1 : viewMonth;
|
|
viewDay = parseInt($target.text(), 10);
|
|
this.date = new Date(viewYear, viewMonth, viewDay);
|
|
this.viewDate = new Date(viewYear, viewMonth, viewDay);
|
|
this.fillDays();
|
|
|
|
if (view === 'day') {
|
|
this.hideView();
|
|
}
|
|
|
|
this.pick('day');
|
|
break;
|
|
|
|
case 'day picked':
|
|
this.hideView();
|
|
this.pick('day');
|
|
break;
|
|
|
|
// No default
|
|
}
|
|
},
|
|
|
|
clickDoc: function (e) {
|
|
var target = e.target;
|
|
var trigger = this.$trigger[0];
|
|
var ignored;
|
|
|
|
while (target !== document) {
|
|
if (target === trigger) {
|
|
ignored = true;
|
|
break;
|
|
}
|
|
|
|
target = target.parentNode;
|
|
}
|
|
|
|
if (!ignored) {
|
|
this.hide();
|
|
}
|
|
},
|
|
|
|
keyup: function () {
|
|
this.update();
|
|
},
|
|
|
|
getValue: function () {
|
|
var $this = this.$element;
|
|
var val = '';
|
|
|
|
if (this.isInput) {
|
|
val = $this.val();
|
|
} else if (this.isInline) {
|
|
if (this.options.container) {
|
|
val = $this.text();
|
|
}
|
|
} else {
|
|
val = $this.text();
|
|
}
|
|
|
|
return val;
|
|
},
|
|
|
|
setValue: function (val) {
|
|
var $this = this.$element;
|
|
|
|
val = isString(val) ? val : '';
|
|
|
|
if (this.isInput) {
|
|
$this.val(val);
|
|
} else if (this.isInline) {
|
|
if (this.options.container) {
|
|
$this.text(val);
|
|
}
|
|
} else {
|
|
$this.text(val);
|
|
}
|
|
},
|
|
|
|
|
|
// Methods
|
|
// -------------------------------------------------------------------------
|
|
|
|
// Show the datepicker
|
|
show: function () {
|
|
if (!this.isBuilt) {
|
|
this.build();
|
|
}
|
|
|
|
if (this.isShown) {
|
|
return;
|
|
}
|
|
|
|
if (this.trigger(EVENT_SHOW).isDefaultPrevented()) {
|
|
return;
|
|
}
|
|
|
|
this.isShown = true;
|
|
this.$picker.removeClass(CLASS_HIDE).on(EVENT_CLICK, $.proxy(this.click, this));
|
|
this.showView(this.options.startView);
|
|
|
|
if (!this.isInline) {
|
|
$window.on(EVENT_RESIZE, (this._place = proxy(this.place, this)));
|
|
$document.on(EVENT_CLICK, (this._clickDoc = proxy(this.clickDoc, this)));
|
|
this.place();
|
|
}
|
|
},
|
|
|
|
// Hide the datepicker
|
|
hide: function () {
|
|
if (!this.isShown) {
|
|
return;
|
|
}
|
|
|
|
if (this.trigger(EVENT_HIDE).isDefaultPrevented()) {
|
|
return;
|
|
}
|
|
|
|
this.isShown = false;
|
|
this.$picker.addClass(CLASS_HIDE).off(EVENT_CLICK, this.click);
|
|
|
|
if (!this.isInline) {
|
|
$window.off(EVENT_RESIZE, this._place);
|
|
$document.off(EVENT_CLICK, this._clickDoc);
|
|
}
|
|
},
|
|
|
|
// Update the datepicker with the current input value
|
|
update: function () {
|
|
this.setDate(this.getValue(), true);
|
|
},
|
|
|
|
/**
|
|
* Pick the current date to the element
|
|
*
|
|
* @param {String} _view (private)
|
|
*/
|
|
pick: function (_view) {
|
|
var $this = this.$element;
|
|
var date = this.date;
|
|
|
|
if (this.trigger(EVENT_PICK, {
|
|
view: _view || '',
|
|
date: date
|
|
}).isDefaultPrevented()) {
|
|
return;
|
|
}
|
|
|
|
this.setValue(date = this.formatDate(this.date));
|
|
|
|
if (this.isInput) {
|
|
$this.trigger('change');
|
|
}
|
|
},
|
|
|
|
// Reset the datepicker
|
|
reset: function () {
|
|
this.setDate(this.initialDate, true);
|
|
this.setValue(this.initialValue);
|
|
|
|
if (this.isShown) {
|
|
this.showView(this.options.startView);
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Get the month name with given argument or the current date
|
|
*
|
|
* @param {Number} month (optional)
|
|
* @param {Boolean} short (optional)
|
|
* @return {String} (month name)
|
|
*/
|
|
getMonthName: function (month, short) {
|
|
var options = this.options;
|
|
var months = options.months;
|
|
|
|
if ($.isNumeric(month)) {
|
|
month = Number(month);
|
|
} else if (isUndefined(short)) {
|
|
short = month;
|
|
}
|
|
|
|
if (short === true) {
|
|
months = options.monthsShort;
|
|
}
|
|
|
|
return months[isNumber(month) ? month : this.date.getMonth()];
|
|
},
|
|
|
|
/**
|
|
* Get the day name with given argument or the current date
|
|
*
|
|
* @param {Number} day (optional)
|
|
* @param {Boolean} short (optional)
|
|
* @param {Boolean} min (optional)
|
|
* @return {String} (day name)
|
|
*/
|
|
getDayName: function (day, short, min) {
|
|
var options = this.options;
|
|
var days = options.days;
|
|
|
|
if ($.isNumeric(day)) {
|
|
day = Number(day);
|
|
} else {
|
|
if (isUndefined(min)) {
|
|
min = short;
|
|
}
|
|
|
|
if (isUndefined(short)) {
|
|
short = day;
|
|
}
|
|
}
|
|
|
|
days = min === true ? options.daysMin : short === true ? options.daysShort : days;
|
|
|
|
return days[isNumber(day) ? day : this.date.getDay()];
|
|
},
|
|
|
|
/**
|
|
* Get the current date
|
|
*
|
|
* @param {Boolean} formated (optional)
|
|
* @return {Date|String} (date)
|
|
*/
|
|
getDate: function (formated) {
|
|
var date = this.date;
|
|
|
|
return formated ? this.formatDate(date) : new Date(date);
|
|
},
|
|
|
|
/**
|
|
* Set the current date with a new date
|
|
*
|
|
* @param {Date} date
|
|
* @param {Boolean} _isUpdated (private)
|
|
*/
|
|
setDate: function (date, _isUpdated) {
|
|
var filter = this.options.filter;
|
|
|
|
if (isDate(date) || isString(date)) {
|
|
date = this.parseDate(date);
|
|
|
|
if ($.isFunction(filter) && filter.call(this.$element, date) === false) {
|
|
return;
|
|
}
|
|
|
|
this.date = date;
|
|
this.viewDate = new Date(date);
|
|
|
|
if (!_isUpdated) {
|
|
this.pick();
|
|
}
|
|
|
|
if (this.isBuilt) {
|
|
this.fillAll();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Set the start view date with a new date
|
|
*
|
|
* @param {Date} date
|
|
*/
|
|
setStartDate: function (date) {
|
|
if (isDate(date) || isString(date)) {
|
|
this.startDate = this.parseDate(date);
|
|
|
|
if (this.isBuilt) {
|
|
this.fillAll();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Set the end view date with a new date
|
|
*
|
|
* @param {Date} date
|
|
*/
|
|
setEndDate: function (date) {
|
|
if (isDate(date) || isString(date)) {
|
|
this.endDate = this.parseDate(date);
|
|
|
|
if (this.isBuilt) {
|
|
this.fillAll();
|
|
}
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Parse a date string with the set date format
|
|
*
|
|
* @param {String} date
|
|
* @return {Date} (parsed date)
|
|
*/
|
|
parseDate: function (date) {
|
|
var format = this.format;
|
|
var parts = [];
|
|
var length;
|
|
var year;
|
|
var day;
|
|
var month;
|
|
var val;
|
|
var i;
|
|
|
|
if (isDate(date)) {
|
|
return new Date(date.getFullYear(), date.getMonth(), date.getDate());
|
|
} else if (isString(date)) {
|
|
parts = date.match(REGEXP_DIGITS) || [];
|
|
}
|
|
|
|
date = new Date();
|
|
year = date.getFullYear();
|
|
day = date.getDate();
|
|
month = date.getMonth();
|
|
length = format.parts.length;
|
|
|
|
if (parts.length === length) {
|
|
for (i = 0; i < length; i++) {
|
|
val = parseInt(parts[i], 10) || 1;
|
|
|
|
switch (format.parts[i]) {
|
|
case 'dd':
|
|
case 'd':
|
|
day = val;
|
|
break;
|
|
|
|
case 'mm':
|
|
case 'm':
|
|
month = val - 1;
|
|
break;
|
|
|
|
case 'yy':
|
|
year = 2000 + val;
|
|
break;
|
|
|
|
case 'yyyy':
|
|
year = val;
|
|
break;
|
|
|
|
// No default
|
|
}
|
|
}
|
|
}
|
|
|
|
return new Date(year, month, day);
|
|
},
|
|
|
|
/**
|
|
* Format a date object to a string with the set date format
|
|
*
|
|
* @param {Date} date
|
|
* @return {String} (formated date)
|
|
*/
|
|
formatDate: function (date) {
|
|
var format = this.format;
|
|
var formated = '';
|
|
var length;
|
|
var year;
|
|
var part;
|
|
var val;
|
|
var i;
|
|
|
|
if (isDate(date)) {
|
|
formated = format.source;
|
|
year = date.getFullYear();
|
|
val = {
|
|
d: date.getDate(),
|
|
m: date.getMonth() + 1,
|
|
yy: year.toString().substring(2),
|
|
yyyy: year
|
|
};
|
|
|
|
val.dd = (val.d < 10 ? '0' : '') + val.d;
|
|
val.mm = (val.m < 10 ? '0' : '') + val.m;
|
|
length = format.parts.length;
|
|
|
|
for (i = 0; i < length; i++) {
|
|
part = format.parts[i];
|
|
formated = formated.replace(part, val[part]);
|
|
}
|
|
}
|
|
|
|
return formated;
|
|
},
|
|
|
|
// Destroy the datepicker and remove the instance from the target element
|
|
destroy: function () {
|
|
this.unbind();
|
|
this.unbuild();
|
|
this.$element.removeData(NAMESPACE);
|
|
}
|
|
};
|
|
|
|
Datepicker.LANGUAGES = {};
|
|
|
|
Datepicker.DEFAULTS = {
|
|
// Show the datepicker automatically when initialized
|
|
autoshow: false,
|
|
|
|
// Hide the datepicker automatically when picked
|
|
autohide: false,
|
|
|
|
// Pick the initial date automatically when initialized
|
|
autopick: false,
|
|
|
|
// Enable inline mode
|
|
inline: false,
|
|
|
|
// A element (or selector) for putting the datepicker
|
|
container: null,
|
|
|
|
// A element (or selector) for triggering the datepicker
|
|
trigger: null,
|
|
|
|
// The ISO language code (built-in: en-US)
|
|
language: '',
|
|
|
|
// The date string format
|
|
format: 'yyyy-mm-dd',
|
|
|
|
// The initial date
|
|
date: null,
|
|
|
|
// The start view date
|
|
startDate: null,
|
|
|
|
// The end view date
|
|
endDate: null,
|
|
|
|
// The start view when initialized
|
|
startView: 0, // 0 for days, 1 for months, 2 for years
|
|
|
|
// The start day of the week
|
|
weekStart: 0, // 0 for Sunday, 1 for Monday, 2 for Tuesday, 3 for Wednesday, 4 for Thursday, 5 for Friday, 6 for Saturday
|
|
|
|
// Show year before month on the datepicker header
|
|
yearFirst: false,
|
|
|
|
// A string suffix to the year number.
|
|
yearSuffix: '',
|
|
|
|
// Days' name of the week.
|
|
days: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'],
|
|
|
|
// Shorter days' name
|
|
daysShort: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
|
|
|
|
// Shortest days' name
|
|
daysMin: ['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'],
|
|
|
|
// Months' name
|
|
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
|
|
|
|
// Shorter months' name
|
|
monthsShort: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
|
|
|
|
// A element tag for each item of years, months and days
|
|
itemTag: 'li',
|
|
|
|
// A class (CSS) for muted date item
|
|
mutedClass: 'muted',
|
|
|
|
// A class (CSS) for picked date item
|
|
pickedClass: 'picked',
|
|
|
|
// A class (CSS) for disabled date item
|
|
disabledClass: 'disabled',
|
|
|
|
// The template of the datepicker
|
|
template: (
|
|
'<div class="datepicker-container">' +
|
|
'<div class="datepicker-panel" data-view="years picker">' +
|
|
'<ul>' +
|
|
'<li data-view="years prev">‹</li>' +
|
|
'<li data-view="years current"></li>' +
|
|
'<li data-view="years next">›</li>' +
|
|
'</ul>' +
|
|
'<ul data-view="years"></ul>' +
|
|
'</div>' +
|
|
'<div class="datepicker-panel" data-view="months picker">' +
|
|
'<ul>' +
|
|
'<li data-view="year prev">‹</li>' +
|
|
'<li data-view="year current"></li>' +
|
|
'<li data-view="year next">›</li>' +
|
|
'</ul>' +
|
|
'<ul data-view="months"></ul>' +
|
|
'</div>' +
|
|
'<div class="datepicker-panel" data-view="days picker">' +
|
|
'<ul>' +
|
|
'<li data-view="month prev">‹</li>' +
|
|
'<li data-view="month current"></li>' +
|
|
'<li data-view="month next">›</li>' +
|
|
'</ul>' +
|
|
'<ul data-view="week"></ul>' +
|
|
'<ul data-view="days"></ul>' +
|
|
'</div>' +
|
|
'</div>'
|
|
),
|
|
|
|
// The offset top or bottom of the datepicker from the element
|
|
offset: 10,
|
|
|
|
// The `z-index` of the datepicker
|
|
zIndex: 1000,
|
|
|
|
// Filter each date item (return `false` to disable a date item)
|
|
filter: null,
|
|
|
|
// Event shortcuts
|
|
show: null,
|
|
hide: null,
|
|
pick: null
|
|
};
|
|
|
|
Datepicker.setDefaults = function (options) {
|
|
$.extend(Datepicker.DEFAULTS, $.isPlainObject(options) && options);
|
|
};
|
|
|
|
// Save the other datepicker
|
|
Datepicker.other = $.fn.qorDatepicker;
|
|
|
|
// Register as jQuery plugin
|
|
$.fn.qorDatepicker = function (option) {
|
|
var args = toArray(arguments, 1);
|
|
var result;
|
|
|
|
this.each(function () {
|
|
var $this = $(this);
|
|
var data = $this.data(NAMESPACE);
|
|
var options;
|
|
var fn;
|
|
|
|
if (!data) {
|
|
if (/destroy/.test(option)) {
|
|
return;
|
|
}
|
|
|
|
options = $.extend({}, $this.data(), $.isPlainObject(option) && option);
|
|
$this.data(NAMESPACE, (data = new Datepicker(this, options)));
|
|
}
|
|
|
|
if (isString(option) && $.isFunction(fn = data[option])) {
|
|
result = fn.apply(data, args);
|
|
}
|
|
});
|
|
|
|
return isUndefined(result) ? this : result;
|
|
};
|
|
|
|
$.fn.qorDatepicker.Constructor = Datepicker;
|
|
$.fn.qorDatepicker.languages = Datepicker.LANGUAGES;
|
|
$.fn.qorDatepicker.setDefaults = Datepicker.setDefaults;
|
|
|
|
// No conflict
|
|
$.fn.qorDatepicker.noConflict = function () {
|
|
$.fn.qorDatepicker = Datepicker.other;
|
|
return this;
|
|
};
|
|
|
|
});
|