fuelux
Version:
Base Fuel UX styles and controls
820 lines (690 loc) • 23.5 kB
JavaScript
/* global jQuery:true */
/*
* Fuel UX Datepicker
* https://github.com/ExactTarget/fuelux
*
* Copyright (c) 2014 ExactTarget
* Licensed under the BSD New license.
*/
// -- BEGIN UMD WRAPPER PREFACE --
// For more information on UMD visit:
// https://github.com/umdjs/umd/blob/master/jqueryPlugin.js
(function umdFactory (factory) {
if (typeof define === 'function' && define.amd) {
// if AMD loader is available, register as an anonymous module.
define(['jquery'], factory);
} else if (typeof exports === 'object') {
// Node/CommonJS
module.exports = factory(require('jquery'), require('moment'));
} else {
// OR use browser globals if AMD is not present
factory(jQuery);
}
}(function DatepickerWrapper ($) {
// -- END UMD WRAPPER PREFACE --
// -- BEGIN MODULE CODE HERE --
var INVALID_DATE = 'Invalid Date';
var MOMENT_NOT_AVAILABLE = 'moment.js is not available so you cannot use this function';
var datepickerStack = [];
var moment = false;
var old = $.fn.datepicker;
var requestedMoment = false;
var runStack = function () {
var i, l;
requestedMoment = true;
for (i = 0, l = datepickerStack.length; i < l; i++) {
datepickerStack[i].init.call(datepickerStack[i].scope);
}
datepickerStack = [];
};
//only load moment if it's there. otherwise we'll look for it in window.moment
if (typeof define === 'function' && define.amd) {//check if AMD is available
require(['moment'], function (amdMoment) {
moment = amdMoment;
runStack();
}, function (err) {
var failedId = err.requireModules && err.requireModules[0];
if (failedId === 'moment') {
runStack();
}
});
} else {
runStack();
}
// DATEPICKER CONSTRUCTOR AND PROTOTYPE
var Datepicker = function (element, options) {
this.$element = $(element);
this.options = $.extend(true, {}, $.fn.datepicker.defaults, options);
this.$calendar = this.$element.find('.datepicker-calendar');
this.$days = this.$calendar.find('.datepicker-calendar-days');
this.$header = this.$calendar.find('.datepicker-calendar-header');
this.$headerTitle = this.$header.find('.title');
this.$input = this.$element.find('input');
this.$inputGroupBtn = this.$element.find('.input-group-btn');
this.$wheels = this.$element.find('.datepicker-wheels');
this.$wheelsMonth = this.$element.find('.datepicker-wheels-month');
this.$wheelsYear = this.$element.find('.datepicker-wheels-year');
this.artificialScrolling = false;
this.formatDate = this.options.formatDate || this.formatDate;
this.inputValue = null;
this.moment = false;
this.momentFormat = null;
this.parseDate = this.options.parseDate || this.parseDate;
this.preventBlurHide = false;
this.restricted = this.options.restricted || [];
this.restrictedParsed = [];
this.restrictedText = this.options.restrictedText;
this.sameYearOnly = this.options.sameYearOnly;
this.selectedDate = null;
this.yearRestriction = null;
this.$calendar.find('.datepicker-today').on('click.fu.datepicker', $.proxy(this.todayClicked, this));
this.$days.on('click.fu.datepicker', 'tr td button', $.proxy(this.dateClicked, this));
this.$header.find('.next').on('click.fu.datepicker', $.proxy(this.next, this));
this.$header.find('.prev').on('click.fu.datepicker', $.proxy(this.prev, this));
this.$headerTitle.on('click.fu.datepicker', $.proxy(this.titleClicked, this));
this.$input.on('change.fu.datepicker', $.proxy(this.inputChanged, this));
this.$input.on('mousedown.fu.datepicker', $.proxy(this.showDropdown, this));
this.$inputGroupBtn.on('hidden.bs.dropdown', $.proxy(this.hide, this));
this.$inputGroupBtn.on('shown.bs.dropdown', $.proxy(this.show, this));
this.$wheels.find('.datepicker-wheels-back').on('click.fu.datepicker', $.proxy(this.backClicked, this));
this.$wheels.find('.datepicker-wheels-select').on('click.fu.datepicker', $.proxy(this.selectClicked, this));
this.$wheelsMonth.on('click.fu.datepicker', 'ul button', $.proxy(this.monthClicked, this));
this.$wheelsYear.on('click.fu.datepicker', 'ul button', $.proxy(this.yearClicked, this));
this.$wheelsYear.find('ul').on('scroll.fu.datepicker', $.proxy(this.onYearScroll, this));
var init = function () {
if (this.checkForMomentJS()) {
moment = moment || window.moment;// need to pull in the global moment if they didn't do it via require
this.moment = true;
this.momentFormat = this.options.momentConfig.format;
this.setCulture(this.options.momentConfig.culture);
// support moment with lang (< v2.8) or locale
moment.locale = moment.locale || moment.lang;
}
this.setRestrictedDates(this.restricted);
if (!this.setDate(this.options.date)) {
this.$input.val('');
this.inputValue = this.$input.val();
}
if (this.sameYearOnly) {
this.yearRestriction = (this.selectedDate) ? this.selectedDate.getFullYear() : new Date().getFullYear();
}
};
if (requestedMoment) {
init.call(this);
} else {
datepickerStack.push({
init: init,
scope: this
});
}
};
Datepicker.prototype = {
constructor: Datepicker,
backClicked: function () {
this.changeView('calendar');
},
changeView: function (view, date) {
if (view === 'wheels') {
this.$calendar.hide().attr('aria-hidden', 'true');
this.$wheels.show().removeAttr('aria-hidden', '');
if (date) {
this.renderWheel(date);
}
} else {
this.$wheels.hide().attr('aria-hidden', 'true');
this.$calendar.show().removeAttr('aria-hidden', '');
if (date) {
this.renderMonth(date);
}
}
},
checkForMomentJS: function () {
if (
($.isFunction(window.moment) || (typeof moment !== 'undefined' && $.isFunction(moment))) &&
$.isPlainObject(this.options.momentConfig) &&
(typeof this.options.momentConfig.culture === 'string' && typeof this.options.momentConfig.format === 'string')
) {
return true;
} else {
return false;
}
},
dateClicked: function (e) {
var $td = $(e.currentTarget).parents('td:first');
var date;
if ($td.hasClass('restricted')) {
return;
}
this.$days.find('td.selected').removeClass('selected');
$td.addClass('selected');
date = new Date($td.attr('data-year'), $td.attr('data-month'), $td.attr('data-date'));
this.selectedDate = date;
this.$input.val(this.formatDate(date));
this.inputValue = this.$input.val();
this.hide();
this.$input.focus();
this.$element.trigger('dateClicked.fu.datepicker', date);
},
destroy: function () {
this.$element.remove();
// any external bindings
// [none]
// empty elements to return to original markup
this.$days.find('tbody').empty();
this.$wheelsYear.find('ul').empty();
return this.$element[0].outerHTML;
},
disable: function () {
this.$element.addClass('disabled');
this.$element.find('input, button').attr('disabled', 'disabled');
this.$inputGroupBtn.removeClass('open');
},
enable: function () {
this.$element.removeClass('disabled');
this.$element.find('input, button').removeAttr('disabled');
},
formatDate: function (date) {
var padTwo = function (value) {
var s = '0' + value;
return s.substr(s.length - 2);
};
if (this.moment) {
return moment(date).format(this.momentFormat);
} else {
return padTwo(date.getMonth() + 1) + '/' + padTwo(date.getDate()) + '/' + date.getFullYear();
}
},
getCulture: function () {
if (this.moment) {
return moment.locale();
} else {
throw MOMENT_NOT_AVAILABLE;
}
},
getDate: function () {
return (!this.selectedDate) ? new Date(NaN) : this.selectedDate;
},
getFormat: function () {
if (this.moment) {
return this.momentFormat;
} else {
throw MOMENT_NOT_AVAILABLE;
}
},
getFormattedDate: function () {
return (!this.selectedDate) ? INVALID_DATE : this.formatDate(this.selectedDate);
},
getRestrictedDates: function () {
return this.restricted;
},
inputChanged: function () {
var inputVal = this.$input.val();
var date;
if (inputVal !== this.inputValue) {
date = this.setDate(inputVal);
if (date === null) {
this.$element.trigger('inputParsingFailed.fu.datepicker', inputVal);
} else if (date === false) {
this.$element.trigger('inputRestrictedDate.fu.datepicker', date);
} else {
this.$element.trigger('changed.fu.datepicker', date);
}
}
},
show: function () {
var date = (this.selectedDate) ? this.selectedDate : new Date();
this.changeView('calendar', date);
this.$inputGroupBtn.addClass('open');
this.$element.trigger('shown.fu.datepicker');
},
showDropdown: function (e) { //input mousedown handler, name retained for legacy support of showDropdown
if (!this.$input.is(':focus') && !this.$inputGroupBtn.hasClass('open')) {
this.show();
}
},
hide: function () {
this.$inputGroupBtn.removeClass('open');
this.$element.trigger('hidden.fu.datepicker');
},
hideDropdown: function () { //for legacy support of hideDropdown
this.hide();
},
isInvalidDate: function (date) {
var dateString = date.toString();
if (dateString === INVALID_DATE || dateString === 'NaN') {
return true;
}
return false;
},
isRestricted: function (date, month, year) {
var restricted = this.restrictedParsed;
var i, from, l, to;
if (this.sameYearOnly && this.yearRestriction !== null && year !== this.yearRestriction) {
return true;
}
for (i = 0, l = restricted.length; i < l; i++) {
from = restricted[i].from;
to = restricted[i].to;
if (
(year > from.year || (year === from.year && month > from.month) || (year === from.year && month === from.month && date >= from.date)) &&
(year < to.year || (year === to.year && month < to.month) || (year === to.year && month === to.month && date <= to.date))
) {
return true;
}
}
return false;
},
monthClicked: function (e) {
this.$wheelsMonth.find('.selected').removeClass('selected');
$(e.currentTarget).parent().addClass('selected');
},
next: function () {
var month = this.$headerTitle.attr('data-month');
var year = this.$headerTitle.attr('data-year');
month++;
if (month > 11) {
if (this.sameYearOnly) {
return;
}
month = 0;
year++;
}
this.renderMonth(new Date(year, month, 1));
},
onYearScroll: function (e) {
if (this.artificialScrolling) {
return;
}
var $yearUl = $(e.currentTarget);
var height = ($yearUl.css('box-sizing') === 'border-box') ? $yearUl.outerHeight() : $yearUl.height();
var scrollHeight = $yearUl.get(0).scrollHeight;
var scrollTop = $yearUl.scrollTop();
var bottomPercentage = (height / (scrollHeight - scrollTop)) * 100;
var topPercentage = (scrollTop / scrollHeight) * 100;
var i, start;
if (topPercentage < 5) {
start = parseInt($yearUl.find('li:first').attr('data-year'), 10);
for (i = (start - 1); i > (start - 11); i--) {
$yearUl.prepend('<li data-year="' + i + '"><button type="button">' + i + '</button></li>');
}
this.artificialScrolling = true;
$yearUl.scrollTop(($yearUl.get(0).scrollHeight - scrollHeight) + scrollTop);
this.artificialScrolling = false;
} else if (bottomPercentage > 90) {
start = parseInt($yearUl.find('li:last').attr('data-year'), 10);
for (i = (start + 1); i < (start + 11); i++) {
$yearUl.append('<li data-year="' + i + '"><button type="button">' + i + '</button></li>');
}
}
},
//some code ripped from http://stackoverflow.com/questions/2182246/javascript-dates-in-ie-nan-firefox-chrome-ok
parseDate: function (date) {
var self = this;
var BAD_DATE = new Date(NaN);
var dt, isoExp, momentParse, momentParseWithFormat, tryMomentParseAll, month, parts, use;
if (date) {
if (this.moment) {//if we have moment, use that to parse the dates
momentParseWithFormat = function (d) {
var md = moment(d, self.momentFormat);
return (true === md.isValid()) ? md.toDate() : BAD_DATE;
};
momentParse = function (d) {
var md = moment(new Date(d));
return (true === md.isValid()) ? md.toDate() : BAD_DATE;
};
tryMomentParseAll = function (rawDateString, parseFunc1, parseFunc2) {
var pd = parseFunc1(rawDateString);
if (!self.isInvalidDate(pd)) {
return pd;
}
pd = parseFunc2(rawDateString);
if (!self.isInvalidDate(pd)) {
return pd;
}
return BAD_DATE;
};
if ('string' === typeof (date)) {
// Attempts to parse date strings using this.momentFormat, falling back on newing a date
return tryMomentParseAll(date, momentParseWithFormat, momentParse);
} else {
// Attempts to parse date by newing a date object directly, falling back on parsing using this.momentFormat
return tryMomentParseAll(date, momentParse, momentParseWithFormat);
}
} else {//if moment isn't present, use previous date parsing strategy
if (typeof (date) === 'string') {
dt = new Date(Date.parse(date));
if (!this.isInvalidDate(dt)) {
return dt;
} else {
date = date.split('T')[0];
isoExp = /^\s*(\d{4})-(\d\d)-(\d\d)\s*$/;
parts = isoExp.exec(date);
if (parts) {
month = parseInt(parts[2], 10);
dt = new Date(parts[1], month - 1, parts[3]);
if (month === (dt.getMonth() + 1)) {
return dt;
}
}
}
} else {
dt = new Date(date);
if (!this.isInvalidDate(dt)) {
return dt;
}
}
}
}
return new Date(NaN);
},
prev: function () {
var month = this.$headerTitle.attr('data-month');
var year = this.$headerTitle.attr('data-year');
month--;
if (month < 0) {
if (this.sameYearOnly) {
return;
}
month = 11;
year--;
}
this.renderMonth(new Date(year, month, 1));
},
renderMonth: function (date) {
date = date || new Date();
var firstDay = new Date(date.getFullYear(), date.getMonth(), 1).getDay();
var lastDate = new Date(date.getFullYear(), date.getMonth() + 1, 0).getDate();
var lastMonthDate = new Date(date.getFullYear(), date.getMonth(), 0).getDate();
var $month = this.$headerTitle.find('.month');
var month = date.getMonth();
var now = new Date();
var nowDate = now.getDate();
var nowMonth = now.getMonth();
var nowYear = now.getFullYear();
var selected = this.selectedDate;
var $tbody = this.$days.find('tbody');
var year = date.getFullYear();
var curDate, curMonth, curYear, i, j, rows, stage, previousStage, lastStage, $td, $tr;
if (selected) {
selected = {
date: selected.getDate(),
month: selected.getMonth(),
year: selected.getFullYear()
};
}
$month.find('.current').removeClass('current');
$month.find('span[data-month="' + month + '"]').addClass('current');
this.$headerTitle.find('.year').text(year);
this.$headerTitle.attr({
'data-month': month,
'data-year': year
});
$tbody.empty();
if (firstDay !== 0) {
curDate = lastMonthDate - firstDay + 1;
stage = -1;
} else {
curDate = 1;
stage = 0;
}
rows = (lastDate <= (35 - firstDay)) ? 5 : 6;
for (i = 0; i < rows; i++) {
$tr = $('<tr></tr>');
for (j = 0; j < 7; j++) {
$td = $('<td></td>');
if (stage === -1) {
$td.addClass('last-month');
if (previousStage !== stage) {
$td.addClass('first');
}
} else if (stage === 1) {
$td.addClass('next-month');
if (previousStage !== stage) {
$td.addClass('first');
}
}
curMonth = month + stage;
curYear = year;
if (curMonth < 0) {
curMonth = 11;
curYear--;
} else if (curMonth > 11) {
curMonth = 0;
curYear++;
}
$td.attr({
'data-date': curDate,
'data-month': curMonth,
'data-year': curYear
});
if (curYear === nowYear && curMonth === nowMonth && curDate === nowDate) {
$td.addClass('current-day');
} else if (curYear < nowYear || (curYear === nowYear && curMonth < nowMonth) ||
(curYear === nowYear && curMonth === nowMonth && curDate < nowDate)) {
$td.addClass('past');
if (!this.options.allowPastDates) {
$td.addClass('restricted').attr('title', this.restrictedText);
}
}
if (this.isRestricted(curDate, curMonth, curYear)) {
$td.addClass('restricted').attr('title', this.restrictedText);
}
if (selected && curYear === selected.year && curMonth === selected.month && curDate === selected.date) {
$td.addClass('selected');
}
if ($td.hasClass('restricted')) {
$td.html('<span><b class="datepicker-date">' + curDate + '</b></span>');
} else {
$td.html('<span><button type="button" class="datepicker-date">' + curDate + '</button></span>');
}
curDate++;
lastStage = previousStage;
previousStage = stage;
if (stage === -1 && curDate > lastMonthDate) {
curDate = 1;
stage = 0;
if (lastStage !== stage) {
$td.addClass('last');
}
} else if (stage === 0 && curDate > lastDate) {
curDate = 1;
stage = 1;
if (lastStage !== stage) {
$td.addClass('last');
}
}
if (i === (rows - 1) && j === 6) {
$td.addClass('last');
}
$tr.append($td);
}
$tbody.append($tr);
}
},
renderWheel: function (date) {
var month = date.getMonth();
var $monthUl = this.$wheelsMonth.find('ul');
var year = date.getFullYear();
var $yearUl = this.$wheelsYear.find('ul');
var i, $monthSelected, $yearSelected;
if (this.sameYearOnly) {
this.$wheelsMonth.addClass('full');
this.$wheelsYear.addClass('hidden');
} else {
this.$wheelsMonth.removeClass('full');
this.$wheelsYear.removeClass('hide hidden'); // .hide is deprecated
}
$monthUl.find('.selected').removeClass('selected');
$monthSelected = $monthUl.find('li[data-month="' + month + '"]');
$monthSelected.addClass('selected');
$monthUl.scrollTop($monthUl.scrollTop() + ($monthSelected.position().top - $monthUl.outerHeight() / 2 - $monthSelected.outerHeight(true) / 2));
$yearUl.empty();
for (i = (year - 10); i < (year + 11); i++) {
$yearUl.append('<li data-year="' + i + '"><button type="button">' + i + '</button></li>');
}
$yearSelected = $yearUl.find('li[data-year="' + year + '"]');
$yearSelected.addClass('selected');
this.artificialScrolling = true;
$yearUl.scrollTop($yearUl.scrollTop() + ($yearSelected.position().top - $yearUl.outerHeight() / 2 - $yearSelected.outerHeight(true) / 2));
this.artificialScrolling = false;
$monthSelected.find('button').focus();
},
selectClicked: function () {
var month = this.$wheelsMonth.find('.selected').attr('data-month');
var year = this.$wheelsYear.find('.selected').attr('data-year');
this.changeView('calendar', new Date(year, month, 1));
},
setCulture: function (cultureCode) {
if (!cultureCode) {
return false;
}
if (this.moment) {
moment.locale(cultureCode);
} else {
throw MOMENT_NOT_AVAILABLE;
}
},
setDate: function (date) {
var parsed = this.parseDate(date);
if (!this.isInvalidDate(parsed)) {
if (!this.isRestricted(parsed.getDate(), parsed.getMonth(), parsed.getFullYear())) {
this.selectedDate = parsed;
this.renderMonth(parsed);
this.$input.val(this.formatDate(parsed));
} else {
this.selectedDate = false;
this.renderMonth();
}
} else {
this.selectedDate = null;
this.renderMonth();
}
this.inputValue = this.$input.val();
return this.selectedDate;
},
setFormat: function (format) {
if (!format) {
return false;
}
if (this.moment) {
this.momentFormat = format;
} else {
throw MOMENT_NOT_AVAILABLE;
}
},
setRestrictedDates: function (restricted) {
var parsed = [];
var self = this;
var i, l;
var parseItem = function (val) {
if (val === -Infinity) {
return {
date: -Infinity,
month: -Infinity,
year: -Infinity
};
} else if (val === Infinity) {
return {
date: Infinity,
month: Infinity,
year: Infinity
};
} else {
val = self.parseDate(val);
return {
date: val.getDate(),
month: val.getMonth(),
year: val.getFullYear()
};
}
};
this.restricted = restricted;
for (i = 0, l = restricted.length; i < l; i++) {
parsed.push({
from: parseItem(restricted[i].from),
to: parseItem(restricted[i].to)
});
}
this.restrictedParsed = parsed;
},
titleClicked: function (e) {
this.changeView('wheels', new Date(this.$headerTitle.attr('data-year'), this.$headerTitle.attr('data-month'), 1));
},
todayClicked: function (e) {
var date = new Date();
if ((date.getMonth() + '') !== this.$headerTitle.attr('data-month') || (date.getFullYear() + '') !== this.$headerTitle.attr('data-year')) {
this.renderMonth(date);
}
},
yearClicked: function (e) {
this.$wheelsYear.find('.selected').removeClass('selected');
$(e.currentTarget).parent().addClass('selected');
}
};
//for control library consistency
Datepicker.prototype.getValue = Datepicker.prototype.getDate;
// DATEPICKER PLUGIN DEFINITION
$.fn.datepicker = function (option) {
var args = Array.prototype.slice.call(arguments, 1);
var methodReturn;
var $set = this.each(function () {
var $this = $(this);
var data = $this.data('fu.datepicker');
var options = typeof option === 'object' && option;
if (!data) {
$this.data('fu.datepicker', (data = new Datepicker(this, options)));
}
if (typeof option === 'string') {
methodReturn = data[option].apply(data, args);
}
});
return (methodReturn === undefined) ? $set : methodReturn;
};
$.fn.datepicker.defaults = {
allowPastDates: false,
date: new Date(),
formatDate: null,
momentConfig: {
culture: 'en',
format: 'L'// more formats can be found here http://momentjs.com/docs/#/customization/long-date-formats/.
},
parseDate: null,
restricted: [],//accepts an array of objects formatted as so: { from: {{date}}, to: {{date}} } (ex: [ { from: new Date('12/11/2014'), to: new Date('03/31/2015') } ])
restrictedText: 'Restricted',
sameYearOnly: false
};
$.fn.datepicker.Constructor = Datepicker;
$.fn.datepicker.noConflict = function () {
$.fn.datepicker = old;
return this;
};
// DATA-API
$(document).on('mousedown.fu.datepicker.data-api', '[data-initialize=datepicker]', function (e) {
var $control = $(e.target).closest('.datepicker');
if (!$control.data('datepicker')) {
$control.datepicker($control.data());
}
});
//used to prevent the dropdown from closing when clicking within it's bounds
$(document).on('click.fu.datepicker.data-api', '.datepicker .dropdown-menu', function (e) {
var $target = $(e.target);
if (!$target.is('.datepicker-date') || $target.closest('.restricted').length) {
e.stopPropagation();
}
});
//used to prevent the dropdown from closing when clicking on the input
$(document).on('click.fu.datepicker.data-api', '.datepicker input', function (e) {
e.stopPropagation();
});
$(function () {
$('[data-initialize=datepicker]').each(function () {
var $this = $(this);
if ($this.data('datepicker')) {
return;
}
$this.datepicker($this.data());
});
});
// -- BEGIN UMD WRAPPER AFTERWORD --
}));
// -- END UMD WRAPPER AFTERWORD --