datatables.net-datetime
Version:
DateTime picker for DataTables.net
1,752 lines (1,479 loc) • 42.4 kB
JavaScript
/*! DateTime picker for DataTables.net v1.5.6
*
* © SpryMedia Ltd, all rights reserved.
* License: MIT datatables.net/license/mit
*/
import jQuery from 'jquery';
// Allow reassignment of the $ variable
let $ = jQuery;
/**
* @summary DateTime picker for DataTables.net
* @version 1.5.6
* @file dataTables.dateTime.js
* @author SpryMedia Ltd
* @contact www.datatables.net/contact
*/
// Supported formatting and parsing libraries:
// * Moment
// * Luxon
// * DayJS
var dateLib;
/*
* This file provides a DateTime GUI picker (calendar and time input). Only the
* format YYYY-MM-DD is supported without additional software, but the end user
* experience can be greatly enhanced by including the momentjs, dayjs or luxon library
* which provide date / time parsing and formatting options.
*
* This functionality is required because the HTML5 date and datetime input
* types are not widely supported in desktop browsers.
*
* Constructed by using:
*
* new DateTime( input, opts )
*
* where `input` is the HTML input element to use and `opts` is an object of
* options based on the `DateTime.defaults` object.
*/
var DateTime = function (input, opts) {
// Check if called with a window or jQuery object for DOM less applications
// This is for backwards compatibility with CommonJS loader
if (DateTime.factory(input, opts)) {
return DateTime;
}
// Attempt to auto detect the formatting library (if there is one). Having it in
// the constructor allows load order independence.
if (typeof dateLib === 'undefined') {
dateLib = window.moment
? window.moment
: window.dayjs
? window.dayjs
: window.luxon
? window.luxon
: null;
}
this.c = $.extend(true, {}, DateTime.defaults, opts);
var classPrefix = this.c.classPrefix;
// Only IS8601 dates are supported without moment, dayjs or luxon
if (!dateLib && this.c.format !== 'YYYY-MM-DD') {
throw "DateTime: Without momentjs, dayjs or luxon only the format 'YYYY-MM-DD' can be used";
}
if (this._isLuxon() && this.c.format == 'YYYY-MM-DD') {
this.c.format = 'yyyy-MM-dd'
}
// Min and max need to be `Date` objects in the config
if (typeof this.c.minDate === 'string') {
this.c.minDate = new Date(this.c.minDate);
}
if (typeof this.c.maxDate === 'string') {
this.c.maxDate = new Date(this.c.maxDate);
}
// DOM structure
var structure = $(
'<div class="' + classPrefix + '">' +
'<div class="' + classPrefix + '-date">' +
'<div class="' + classPrefix + '-title">' +
'<div class="' + classPrefix + '-iconLeft">' +
'<button type="button"></button>' +
'</div>' +
'<div class="' + classPrefix + '-iconRight">' +
'<button type="button"></button>' +
'</div>' +
'<div class="' + classPrefix + '-label">' +
'<span></span>' +
'<select class="' + classPrefix + '-month"></select>' +
'</div>' +
'<div class="' + classPrefix + '-label">' +
'<span></span>' +
'<select class="' + classPrefix + '-year"></select>' +
'</div>' +
'</div>' +
'<div class="' + classPrefix + '-buttons">' +
'<a class="' + classPrefix + '-clear"></a>' +
'<a class="' + classPrefix + '-today"></a>' +
'</div>' +
'<div class="' + classPrefix + '-calendar"></div>' +
'</div>' +
'<div class="' + classPrefix + '-time">' +
'<div class="' + classPrefix + '-hours"></div>' +
'<div class="' + classPrefix + '-minutes"></div>' +
'<div class="' + classPrefix + '-seconds"></div>' +
'</div>' +
'<div class="' + classPrefix + '-error"></div>' +
'</div>'
);
this.dom = {
container: structure,
date: structure.find('.' + classPrefix + '-date'),
title: structure.find('.' + classPrefix + '-title'),
calendar: structure.find('.' + classPrefix + '-calendar'),
time: structure.find('.' + classPrefix + '-time'),
error: structure.find('.' + classPrefix + '-error'),
buttons: structure.find('.' + classPrefix + '-buttons'),
clear: structure.find('.' + classPrefix + '-clear'),
today: structure.find('.' + classPrefix + '-today'),
previous: structure.find('.' + classPrefix + '-iconLeft'),
next: structure.find('.' + classPrefix + '-iconRight'),
input: $(input)
};
this.s = {
/** @type {Date} Date value that the picker has currently selected */
d: null,
/** @type {Date} Date of the calendar - might not match the value */
display: null,
/** @type {number} Used to select minutes in a range where the range base is itself unavailable */
minutesRange: null,
/** @type {number} Used to select minutes in a range where the range base is itself unavailable */
secondsRange: null,
/** @type {String} Unique namespace string for this instance */
namespace: 'dateime-' + (DateTime._instance++),
/** @type {Object} Parts of the picker that should be shown */
parts: {
date: this.c.format.match(/[yYMDd]|L(?!T)|l/) !== null,
time: this.c.format.match(/[Hhm]|LT|LTS/) !== null,
seconds: this.c.format.indexOf('s') !== -1,
hours12: this.c.format.match(/[haA]/) !== null
},
/** Timeout when showing the control to listen for a blur */
showTo: null
};
this.dom.container
.append(this.dom.date)
.append(this.dom.time)
.append(this.dom.error);
this.dom.date
.append(this.dom.title)
.append(this.dom.buttons)
.append(this.dom.calendar);
this.dom.input.addClass('dt-datetime');
this._constructor();
};
$.extend(DateTime.prototype, {
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Public
*/
/**
* Destroy the control
*/
destroy: function () {
clearTimeout(this.s.showTo);
this._hide(true);
this.dom.container.off().empty();
this.dom.input
.removeClass('dt-datetime')
.removeAttr('autocomplete')
.off('.datetime');
},
display: function (year, month) {
if (year !== undefined) {
this.s.display.setUTCFullYear(year);
}
if (month !== undefined) {
this.s.display.setUTCMonth(month - 1);
}
if (year !== undefined || month !== undefined) {
this._setTitle();
this._setCalander();
return this;
}
return {
month: this.s.display.getUTCMonth() + 1,
year: this.s.display.getUTCFullYear()
};
},
errorMsg: function (msg) {
var error = this.dom.error;
if (msg) {
error.html(msg);
}
else {
error.empty();
}
return this;
},
hide: function () {
this._hide();
return this;
},
max: function (date) {
this.c.maxDate = typeof date === 'string'
? new Date(date)
: date;
this._optionsTitle();
this._setCalander();
return this;
},
min: function (date) {
this.c.minDate = typeof date === 'string'
? new Date(date)
: date;
this._optionsTitle();
this._setCalander();
return this;
},
/**
* Check if an element belongs to this control
*
* @param {node} node Element to check
* @return {boolean} true if owned by this control, false otherwise
*/
owns: function (node) {
return $(node).parents().filter(this.dom.container).length > 0;
},
/**
* Get / set the value
*
* @param {string|Date} set Value to set
* @param {boolean} [write=true] Flag to indicate if the formatted value
* should be written into the input element
*/
val: function (set, write) {
if (set === undefined) {
return this.s.d;
}
var oldVal = this.s.d;
if (set instanceof Date) {
this.s.d = this._dateToUtc(set);
}
else if (set === null || set === '') {
this.s.d = null;
}
else if (set === '--now') {
this.s.d = this._dateToUtc(new Date());
}
else if (typeof set === 'string') {
this.s.d = this._dateToUtc(
this._convert(set, this.c.format, null)
);
}
if (write || write === undefined) {
if (this.s.d) {
this._writeOutput(
false,
(oldVal === null && this.s.d !== null) ||
(oldVal !== null && this.s.d === null) ||
oldVal.toString() !== this.s.d.toString()
);
}
else {
// The input value was not valid...
this.dom.input.val(set);
}
}
// Need something to display
this.s.display = this.s.d
? new Date(this.s.d.toString())
: new Date();
// Set the day of the month to be 1 so changing between months doesn't
// run into issues when going from day 31 to 28 (for example)
this.s.display.setUTCDate(1);
// Update the display elements for the new value
this._setTitle();
this._setCalander();
this._setTime();
return this;
},
/**
* Similar to `val()` but uses a given date / time format
*
* @param format Format to get the data as (getter) or that is input (setter)
* @param val Value to write (if undefined, used as a getter)
* @returns
*/
valFormat: function (format, val) {
if (!val) {
return this._convert(this.val(), null, format);
}
// Convert from the format given here to the instance's configured format
this.val(
this._convert(val, format, null)
);
return this;
},
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Constructor
*/
/**
* Build the control and assign initial event handlers
*
* @private
*/
_constructor: function () {
var that = this;
var classPrefix = this.c.classPrefix;
var last = this.dom.input.val();
var onChange = function () {
var curr = that.dom.input.val();
if (curr !== last) {
that.c.onChange.call(that, curr, that.s.d, that.dom.input);
last = curr;
}
};
if (!this.s.parts.date) {
this.dom.date.css('display', 'none');
}
if (!this.s.parts.time) {
this.dom.time.css('display', 'none');
}
if (!this.s.parts.seconds) {
this.dom.time.children('div.' + classPrefix + '-seconds').remove();
this.dom.time.children('span').eq(1).remove();
}
if (!this.c.buttons.clear) {
this.dom.clear.css('display', 'none');
}
if (!this.c.buttons.today) {
this.dom.today.css('display', 'none');
}
// Render the options
this._optionsTitle();
$(document).on('i18n.dt', function (e, settings) {
if (settings.oLanguage.datetime) {
$.extend(true, that.c.i18n, settings.oLanguage.datetime);
that._optionsTitle();
}
});
// When attached to a hidden input, we always show the input picker, and
// do so inline
if (this.dom.input.attr('type') === 'hidden') {
this.dom.container.addClass('inline');
this.c.attachTo = 'input';
this.val(this.dom.input.val(), false);
this._show();
}
// Set the initial value
if (last) {
this.val(last, false);
}
// Trigger the display of the widget when clicking or focusing on the
// input element
this.dom.input
.attr('autocomplete', 'off')
.on('focus.datetime click.datetime', function () {
// If already visible - don't do anything
if (that.dom.container.is(':visible') || that.dom.input.is(':disabled')) {
return;
}
// In case the value has changed by text
that.val(that.dom.input.val(), false);
that._show();
})
.on('keyup.datetime', function () {
// Update the calendar's displayed value as the user types
if (that.dom.container.is(':visible')) {
that.val(that.dom.input.val(), false);
}
});
// Want to prevent the focus bubbling up the document to account for
// focus capture in modals (e.g. Editor and Bootstrap). They can see
// the focus as outside the modal and thus immediately blur focus on
// the picker. Need to use a native addEL since jQuery changes the
// focusin to focus for some reason! focusin bubbles, focus does not.
this.dom.container[0].addEventListener('focusin', function (e) {
e.stopPropagation();
});
// Main event handlers for input in the widget
this.dom.container
.on('change', 'select', function () {
var select = $(this);
var val = select.val();
if (select.hasClass(classPrefix + '-month')) {
// Month select
that._correctMonth(that.s.display, val);
that._setTitle();
that._setCalander();
}
else if (select.hasClass(classPrefix + '-year')) {
// Year select
that.s.display.setUTCFullYear(val);
that._setTitle();
that._setCalander();
}
else if (select.hasClass(classPrefix + '-hours') || select.hasClass(classPrefix + '-ampm')) {
// Hours - need to take account of AM/PM input if present
if (that.s.parts.hours12) {
var hours = $(that.dom.container).find('.' + classPrefix + '-hours').val() * 1;
var pm = $(that.dom.container).find('.' + classPrefix + '-ampm').val() === 'pm';
that.s.d.setUTCHours(hours === 12 && !pm ?
0 :
pm && hours !== 12 ?
hours + 12 :
hours
);
}
else {
that.s.d.setUTCHours(val);
}
that._setTime();
that._writeOutput(true);
onChange();
}
else if (select.hasClass(classPrefix + '-minutes')) {
// Minutes select
that.s.d.setUTCMinutes(val);
that._setTime();
that._writeOutput(true);
onChange();
}
else if (select.hasClass(classPrefix + '-seconds')) {
// Seconds select
that.s.d.setSeconds(val);
that._setTime();
that._writeOutput(true);
onChange();
}
that.dom.input.focus();
that._position();
})
.on('click', function (e) {
var d = that.s.d;
var nodeName = e.target.nodeName.toLowerCase();
var target = nodeName === 'span' ?
e.target.parentNode :
e.target;
nodeName = target.nodeName.toLowerCase();
if (nodeName === 'select') {
return;
}
e.stopPropagation();
if (nodeName === 'a') {
e.preventDefault();
if ($(target).hasClass(classPrefix + '-clear')) {
// Clear the value and don't change the display
that.s.d = null;
that.dom.input.val('');
that._writeOutput();
that._setCalander();
that._setTime();
onChange();
}
else if ($(target).hasClass(classPrefix + '-today')) {
// Don't change the value, but jump to the month
// containing today
that.s.display = new Date();
that._setTitle();
that._setCalander();
}
}
if (nodeName === 'button') {
var button = $(target);
var parent = button.parent();
if (parent.hasClass('disabled') && !parent.hasClass('range')) {
button.blur();
return;
}
if (parent.hasClass(classPrefix + '-iconLeft')) {
// Previous month
that.s.display.setUTCMonth(that.s.display.getUTCMonth() - 1);
that._setTitle();
that._setCalander();
that.dom.input.focus();
}
else if (parent.hasClass(classPrefix + '-iconRight')) {
// Next month
that._correctMonth(that.s.display, that.s.display.getUTCMonth() + 1);
that._setTitle();
that._setCalander();
that.dom.input.focus();
}
else if (button.parents('.' + classPrefix + '-time').length) {
var val = button.data('value');
var unit = button.data('unit');
d = that._needValue();
if (unit === 'minutes') {
if (parent.hasClass('disabled') && parent.hasClass('range')) {
that.s.minutesRange = val;
that._setTime();
return;
}
else {
that.s.minutesRange = null;
}
}
if (unit === 'seconds') {
if (parent.hasClass('disabled') && parent.hasClass('range')) {
that.s.secondsRange = val;
that._setTime();
return;
}
else {
that.s.secondsRange = null;
}
}
// Specific to hours for 12h clock
if (val === 'am') {
if (d.getUTCHours() >= 12) {
val = d.getUTCHours() - 12;
}
else {
return;
}
}
else if (val === 'pm') {
if (d.getUTCHours() < 12) {
val = d.getUTCHours() + 12;
}
else {
return;
}
}
var set = unit === 'hours' ?
'setUTCHours' :
unit === 'minutes' ?
'setUTCMinutes' :
'setSeconds';
d[set](val);
that._setCalander();
that._setTime();
that._writeOutput(true);
onChange();
}
else {
// Calendar click
d = that._needValue();
// Can't be certain that the current day will exist in
// the new month, and likewise don't know that the
// new day will exist in the old month, But 1 always
// does, so we can change the month without worry of a
// recalculation being done automatically by `Date`
d.setUTCDate(1);
d.setUTCFullYear(button.data('year'));
d.setUTCMonth(button.data('month'));
d.setUTCDate(button.data('day'));
that._writeOutput(true);
// Don't hide if there is a time picker, since we want to
// be able to select a time as well.
if (!that.s.parts.time) {
// This is annoying but IE has some kind of async
// behaviour with focus and the focus from the above
// write would occur after this hide - resulting in the
// calendar opening immediately
setTimeout(function () {
that._hide();
}, 10);
}
else {
that._setCalander();
that._setTime();
}
onChange();
}
}
else {
// Click anywhere else in the widget - return focus to the
// input element
that.dom.input.focus();
}
});
},
/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Private
*/
/**
* Compare the date part only of two dates - this is made super easy by the
* toDateString method!
*
* @param {Date} a Date 1
* @param {Date} b Date 2
* @private
*/
_compareDates: function (a, b) {
// Can't use toDateString as that converts to local time
// luxon uses different method names so need to be able to call them
return this._isLuxon()
? dateLib.DateTime.fromJSDate(a).toUTC().toISODate() === dateLib.DateTime.fromJSDate(b).toUTC().toISODate()
: this._dateToUtcString(a) === this._dateToUtcString(b);
},
/**
* Convert from one format to another
*
* @param {string|Date} val Value
* @param {string|null} from Format to convert from. If null a `Date` must be given
* @param {string|null} to Format to convert to. If null a `Date` will be returned
* @returns {string|Date} Converted value
*/
_convert: function (val, from, to) {
if (!val) {
return val;
}
if (!dateLib) {
// Note that in here from and to can either be null or YYYY-MM-DD
// They cannot be anything else
if ((!from && !to) || (from && to)) {
// No conversion
return val;
}
else if (!from) {
// Date in, string back
return val.getUTCFullYear() + '-' +
this._pad(val.getUTCMonth() + 1) + '-' +
this._pad(val.getUTCDate());
}
else { // (! to)
// String in, date back
var match = val.match(/(\d{4})\-(\d{2})\-(\d{2})/);
return match ?
new Date(match[1], match[2] - 1, match[3]) :
null;
}
}
else if (this._isLuxon()) {
// Luxon
var dtLux = val instanceof Date
? dateLib.DateTime.fromJSDate(val).toUTC()
: dateLib.DateTime.fromFormat(val, from);
if (!dtLux.isValid) {
return null;
}
return to
? dtLux.toFormat(to)
: dtLux.toJSDate();
}
else {
// Moment / DayJS
var dtMo = val instanceof Date
? dateLib.utc(val, undefined, this.c.locale, this.c.strict)
: dateLib(val, from, this.c.locale, this.c.strict);
if (!dtMo.isValid()) {
return null;
}
return to
? dtMo.format(to)
: dtMo.toDate();
}
},
/**
* When changing month, take account of the fact that some months don't have
* the same number of days. For example going from January to February you
* can have the 31st of Jan selected and just add a month since the date
* would still be 31, and thus drop you into March.
*
* @param {Date} date Date - will be modified
* @param {integer} month Month to set
* @private
*/
_correctMonth: function (date, month) {
var days = this._daysInMonth(date.getUTCFullYear(), month);
var correctDays = date.getUTCDate() > days;
date.setUTCMonth(month);
if (correctDays) {
date.setUTCDate(days);
date.setUTCMonth(month);
}
},
/**
* Get the number of days in a method. Based on
* http://stackoverflow.com/a/4881951 by Matti Virkkunen
*
* @param {integer} year Year
* @param {integer} month Month (starting at 0)
* @private
*/
_daysInMonth: function (year, month) {
//
var isLeap = ((year % 4) === 0 && ((year % 100) !== 0 || (year % 400) === 0));
var months = [31, (isLeap ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
return months[month];
},
/**
* Create a new date object which has the UTC values set to the local time.
* This allows the local time to be used directly for the library which
* always bases its calculations and display on UTC.
*
* @param {Date} s Date to "convert"
* @return {Date} Shifted date
*/
_dateToUtc: function (s) {
if (!s) {
return s;
}
return new Date(Date.UTC(
s.getFullYear(), s.getMonth(), s.getDate(),
s.getHours(), s.getMinutes(), s.getSeconds()
));
},
/**
* Create a UTC ISO8601 date part from a date object
*
* @param {Date} d Date to "convert"
* @return {string} ISO formatted date
*/
_dateToUtcString: function (d) {
// luxon uses different method names so need to be able to call them
return this._isLuxon()
? dateLib.DateTime.fromJSDate(d).toUTC().toISODate()
: d.getUTCFullYear() + '-' +
this._pad(d.getUTCMonth() + 1) + '-' +
this._pad(d.getUTCDate());
},
/**
* Hide the control and remove events related to its display
*
* @private
*/
_hide: function (destroy) {
if (!destroy && this.dom.input.attr('type') === 'hidden') {
return;
}
var namespace = this.s.namespace;
this.dom.container.detach();
$(window).off('.' + namespace);
$(document).off('keydown.' + namespace);
$('div.dataTables_scrollBody').off('scroll.' + namespace);
$('div.DTE_Body_Content').off('scroll.' + namespace);
$(document).off('click.' + namespace);
$(this.dom.input[0].offsetParent).off('.' + namespace);
},
/**
* Convert a 24 hour value to a 12 hour value
*
* @param {integer} val 24 hour value
* @return {integer} 12 hour value
* @private
*/
_hours24To12: function (val) {
return val === 0 ?
12 :
val > 12 ?
val - 12 :
val;
},
/**
* Generate the HTML for a single day in the calendar - this is basically
* and HTML cell with a button that has data attributes so we know what was
* clicked on (if it is clicked on) and a bunch of classes for styling.
*
* @param {object} day Day object from the `_htmlMonth` method
* @return {string} HTML cell
*/
_htmlDay: function (day) {
var classPrefix = this.c.classPrefix;
if (day.empty) {
return '<td class="' + classPrefix + '-empty"></td>';
}
var classes = ['selectable'];
if (day.disabled) {
classes.push('disabled');
}
if (day.today) {
classes.push('now');
}
if (day.selected) {
classes.push('selected');
}
return '<td data-day="' + day.day + '" class="' + classes.join(' ') + '">' +
'<button class="' + classPrefix + '-button ' + classPrefix + '-day" type="button" ' + 'data-year="' + day.year + '" data-month="' + day.month + '" data-day="' + day.day + '">' +
'<span>' + day.day + '</span>' +
'</button>' +
'</td>';
},
/**
* Create the HTML for a month to be displayed in the calendar table.
*
* Based upon the logic used in Pikaday - MIT licensed
* Copyright (c) 2014 David Bushell
* https://github.com/dbushell/Pikaday
*
* @param {integer} year Year
* @param {integer} month Month (starting at 0)
* @return {string} Calendar month HTML
* @private
*/
_htmlMonth: function (year, month) {
var now = this._dateToUtc(new Date()),
days = this._daysInMonth(year, month),
before = new Date(Date.UTC(year, month, 1)).getUTCDay(),
data = [],
row = [];
if (this.c.firstDay > 0) {
before -= this.c.firstDay;
if (before < 0) {
before += 7;
}
}
var cells = days + before,
after = cells;
while (after > 7) {
after -= 7;
}
cells += 7 - after;
var minDate = this.c.minDate;
var maxDate = this.c.maxDate;
if (minDate) {
minDate.setUTCHours(0);
minDate.setUTCMinutes(0);
minDate.setSeconds(0);
}
if (maxDate) {
maxDate.setUTCHours(23);
maxDate.setUTCMinutes(59);
maxDate.setSeconds(59);
}
for (var i = 0, r = 0; i < cells; i++) {
var day = new Date(Date.UTC(year, month, 1 + (i - before))),
selected = this.s.d ? this._compareDates(day, this.s.d) : false,
today = this._compareDates(day, now),
empty = i < before || i >= (days + before),
disabled = (minDate && day < minDate) ||
(maxDate && day > maxDate);
var disableDays = this.c.disableDays;
if (Array.isArray(disableDays) && $.inArray(day.getUTCDay(), disableDays) !== -1) {
disabled = true;
}
else if (typeof disableDays === 'function' && disableDays(day) === true) {
disabled = true;
}
var dayConfig = {
day: 1 + (i - before),
month: month,
year: year,
selected: selected,
today: today,
disabled: disabled,
empty: empty
};
row.push(this._htmlDay(dayConfig));
if (++r === 7) {
if (this.c.showWeekNumber) {
row.unshift(this._htmlWeekOfYear(i - before, month, year));
}
data.push('<tr>' + row.join('') + '</tr>');
row = [];
r = 0;
}
}
var classPrefix = this.c.classPrefix;
var className = classPrefix + '-table';
if (this.c.showWeekNumber) {
className += ' weekNumber';
}
// Show / hide month icons based on min/max
if (minDate) {
var underMin = minDate >= new Date(Date.UTC(year, month, 1, 0, 0, 0));
this.dom.title.find('div.' + classPrefix + '-iconLeft')
.css('display', underMin ? 'none' : 'block');
}
if (maxDate) {
var overMax = maxDate < new Date(Date.UTC(year, month + 1, 1, 0, 0, 0));
this.dom.title.find('div.' + classPrefix + '-iconRight')
.css('display', overMax ? 'none' : 'block');
}
return '<table class="' + className + '">' +
'<thead>' +
this._htmlMonthHead() +
'</thead>' +
'<tbody>' +
data.join('') +
'</tbody>' +
'</table>';
},
/**
* Create the calendar table's header (week days)
*
* @return {string} HTML cells for the row
* @private
*/
_htmlMonthHead: function () {
var a = [];
var firstDay = this.c.firstDay;
var i18n = this.c.i18n;
// Take account of the first day shift
var dayName = function (day) {
day += firstDay;
while (day >= 7) {
day -= 7;
}
return i18n.weekdays[day];
};
// Empty cell in the header
if (this.c.showWeekNumber) {
a.push('<th></th>');
}
for (var i = 0; i < 7; i++) {
a.push('<th>' + dayName(i) + '</th>');
}
return a.join('');
},
/**
* Create a cell that contains week of the year - ISO8601
*
* Based on https://stackoverflow.com/questions/6117814/ and
* http://techblog.procurios.nl/k/n618/news/view/33796/14863/
*
* @param {integer} d Day of month
* @param {integer} m Month of year (zero index)
* @param {integer} y Year
* @return {string}
* @private
*/
_htmlWeekOfYear: function (d, m, y) {
var date = new Date(y, m, d, 0, 0, 0, 0);
// First week of the year always has 4th January in it
date.setDate(date.getDate() + 4 - (date.getDay() || 7));
var oneJan = new Date(y, 0, 1);
var weekNum = Math.ceil((((date - oneJan) / 86400000) + 1) / 7);
return '<td class="' + this.c.classPrefix + '-week">' + weekNum + '</td>';
},
/**
* Determine if Luxon is being used
*
* @returns Flag for Luxon
*/
_isLuxon: function () {
return dateLib && dateLib.DateTime && dateLib.Duration && dateLib.Settings
? true
: false;
},
/**
* Check if the instance has a date object value - it might be null.
* If is doesn't set one to now.
* @returns A Date object
* @private
*/
_needValue: function () {
if (!this.s.d) {
this.s.d = this._dateToUtc(new Date());
if (!this.s.parts.time) {
this.s.d.setUTCHours(0);
this.s.d.setUTCMinutes(0);
this.s.d.setSeconds(0);
this.s.d.setMilliseconds(0);
}
}
return this.s.d;
},
/**
* Create option elements from a range in an array
*
* @param {string} selector Class name unique to the select element to use
* @param {array} values Array of values
* @param {array} [labels] Array of labels. If given must be the same
* length as the values parameter.
* @private
*/
_options: function (selector, values, labels) {
if (!labels) {
labels = values;
}
var select = this.dom.container.find('select.' + this.c.classPrefix + '-' + selector);
select.empty();
for (var i = 0, ien = values.length; i < ien; i++) {
select.append('<option value="' + values[i] + '">' + labels[i] + '</option>');
}
},
/**
* Set an option and update the option's span pair (since the select element
* has opacity 0 for styling)
*
* @param {string} selector Class name unique to the select element to use
* @param {*} val Value to set
* @private
*/
_optionSet: function (selector, val) {
var select = this.dom.container.find('select.' + this.c.classPrefix + '-' + selector);
var span = select.parent().children('span');
select.val(val);
var selected = select.find('option:selected');
span.html(selected.length !== 0 ?
selected.text() :
this.c.i18n.unknown
);
},
/**
* Create time options list.
*
* @param {string} unit Time unit - hours, minutes or seconds
* @param {integer} count Count range - 12, 24 or 60
* @param {integer} val Existing value for this unit
* @param {integer[]} allowed Values allow for selection
* @param {integer} range Override range
* @private
*/
_optionsTime: function (unit, count, val, allowed, range) {
var classPrefix = this.c.classPrefix;
var container = this.dom.container.find('div.' + classPrefix + '-' + unit);
var i, j;
var render = count === 12 ?
function (i) { return i; } :
this._pad;
var className = classPrefix + '-table';
var i18n = this.c.i18n;
if (!container.length) {
return;
}
var a = '';
var span = 10;
var button = function (value, label, className) {
// Shift the value for PM
if (count === 12 && typeof value === 'number') {
if (val >= 12) {
value += 12;
}
if (value == 12) {
value = 0;
}
else if (value == 24) {
value = 12;
}
}
var selected = val === value || (value === 'am' && val < 12) || (value === 'pm' && val >= 12) ?
'selected' :
'';
if (typeof value === 'number' && allowed && $.inArray(value, allowed) === -1) {
selected += ' disabled';
}
if (className) {
selected += ' ' + className;
}
return '<td class="selectable ' + selected + '">' +
'<button class="' + classPrefix + '-button ' + classPrefix + '-day" type="button" data-unit="' + unit + '" data-value="' + value + '">' +
'<span>' + label + '</span>' +
'</button>' +
'</td>';
}
if (count === 12) {
// Hours with AM/PM
a += '<tr>';
for (i = 1; i <= 6; i++) {
a += button(i, render(i));
}
a += button('am', i18n.amPm[0]);
a += '</tr>';
a += '<tr>';
for (i = 7; i <= 12; i++) {
a += button(i, render(i));
}
a += button('pm', i18n.amPm[1]);
a += '</tr>';
span = 7;
}
else if (count === 24) {
// Hours - 24
var c = 0;
for (j = 0; j < 4; j++) {
a += '<tr>';
for (i = 0; i < 6; i++) {
a += button(c, render(c));
c++;
}
a += '</tr>';
}
span = 6;
}
else {
// Minutes and seconds
a += '<tr>';
for (j = 0; j < 60; j += 10) {
a += button(j, render(j), 'range');
}
a += '</tr>';
// Slight hack to allow for the different number of columns
a += '</tbody></thead><table class="' + className + ' ' + className + '-nospace"><tbody>';
var start = range !== null
? range
: val === -1
? 0
: Math.floor(val / 10) * 10;
a += '<tr>';
for (j = start + 1; j < start + 10; j++) {
a += button(j, render(j));
}
a += '</tr>';
span = 6;
}
container
.empty()
.append(
'<table class="' + className + '">' +
'<thead><tr><th colspan="' + span + '">' +
i18n[unit] +
'</th></tr></thead>' +
'<tbody>' +
a +
'</tbody>' +
'</table>'
);
},
/**
* Create the options for the month and year
*
* @param {integer} year Year
* @param {integer} month Month (starting at 0)
* @private
*/
_optionsTitle: function () {
var i18n = this.c.i18n;
var min = this.c.minDate;
var max = this.c.maxDate;
var minYear = min ? min.getFullYear() : null;
var maxYear = max ? max.getFullYear() : null;
var i = minYear !== null ? minYear : new Date().getFullYear() - this.c.yearRange;
var j = maxYear !== null ? maxYear : new Date().getFullYear() + this.c.yearRange;
this._options('month', this._range(0, 11), i18n.months);
this._options('year', this._range(i, j));
// Set the language strings in case any have changed
this.dom.today.text(i18n.today).text(i18n.today);
this.dom.clear.text(i18n.clear).text(i18n.clear);
this.dom.previous
.attr('title', i18n.previous)
.children('button')
.text(i18n.previous);
this.dom.next
.attr('title', i18n.next)
.children('button')
.text(i18n.next);
},
/**
* Simple two digit pad
*
* @param {integer} i Value that might need padding
* @return {string|integer} Padded value
* @private
*/
_pad: function (i) {
return i < 10 ? '0' + i : i;
},
/**
* Position the calendar to look attached to the input element
* @private
*/
_position: function () {
var offset = this.c.attachTo === 'input' ? this.dom.input.position() : this.dom.input.offset();
var container = this.dom.container;
var inputHeight = this.dom.input.outerHeight();
if (container.hasClass('inline')) {
container.insertAfter(this.dom.input);
return;
}
if (this.s.parts.date && this.s.parts.time && $(window).width() > 550) {
container.addClass('horizontal');
}
else {
container.removeClass('horizontal');
}
if (this.c.attachTo === 'input') {
container
.css({
top: offset.top + inputHeight,
left: offset.left
})
.insertAfter(this.dom.input);
}
else {
container
.css({
top: offset.top + inputHeight,
left: offset.left
})
.appendTo('body');
}
var calHeight = container.outerHeight();
var calWidth = container.outerWidth();
var scrollTop = $(window).scrollTop();
// Correct to the bottom
if (offset.top + inputHeight + calHeight - scrollTop > $(window).height()) {
var newTop = offset.top - calHeight;
container.css('top', newTop < 0 ? 0 : newTop);
}
// Correct to the right
if (calWidth + offset.left > $(window).width()) {
var newLeft = $(window).width() - calWidth - 5;
// Account for elements which are inside a position absolute element
if (this.c.attachTo === 'input') {
newLeft -= $(container).offsetParent().offset().left;
}
container.css('left', newLeft < 0 ? 0 : newLeft);
}
},
/**
* Create a simple array with a range of values
*
* @param {integer} start Start value (inclusive)
* @param {integer} end End value (inclusive)
* @param {integer} [inc=1] Increment value
* @return {array} Created array
* @private
*/
_range: function (start, end, inc) {
var a = [];
if (!inc) {
inc = 1;
}
for (var i = start; i <= end; i += inc) {
a.push(i);
}
return a;
},
/**
* Redraw the calendar based on the display date - this is a destructive
* operation
*
* @private
*/
_setCalander: function () {
if (this.s.display) {
this.dom.calendar
.empty()
.append(this._htmlMonth(
this.s.display.getUTCFullYear(),
this.s.display.getUTCMonth()
));
}
},
/**
* Set the month and year for the calendar based on the current display date
*
* @private
*/
_setTitle: function () {
this._optionSet('month', this.s.display.getUTCMonth());
this._optionSet('year', this.s.display.getUTCFullYear());
},
/**
* Set the time based on the current value of the widget
*
* @private
*/
_setTime: function () {
var that = this;
var d = this.s.d;
// luxon uses different method names so need to be able to call them. This happens a few time later in this method too
var luxDT = null
if (this._isLuxon()) {
luxDT = dateLib.DateTime.fromJSDate(d).toUTC();
}
var hours = luxDT != null
? luxDT.hour
: d
? d.getUTCHours()
: -1;
var allowed = function (prop) { // Backwards compt with `Increment` option
return that.c[prop + 'Available'] ?
that.c[prop + 'Available'] :
that._range(0, 59, that.c[prop + 'Increment']);
}
this._optionsTime('hours', this.s.parts.hours12 ? 12 : 24, hours, this.c.hoursAvailable)
this._optionsTime(
'minutes',
60,
luxDT != null
? luxDT.minute
: d
? d.getUTCMinutes()
: -1,
allowed('minutes'),
this.s.minutesRange
);
this._optionsTime(
'seconds',
60,
luxDT != null
? luxDT.second
: d
? d.getSeconds()
: -1,
allowed('seconds'),
this.s.secondsRange
);
},
/**
* Show the widget and add events to the document required only while it
* is displayed
*
* @private
*/
_show: function () {
var that = this;
var namespace = this.s.namespace;
this._position();
// Need to reposition on scroll
$(window).on('scroll.' + namespace + ' resize.' + namespace, function () {
that._position();
});
$('div.DTE_Body_Content').on('scroll.' + namespace, function () {
that._position();
});
$('div.dataTables_scrollBody').on('scroll.' + namespace, function () {
that._position();
});
var offsetParent = this.dom.input[0].offsetParent;
if (offsetParent !== document.body) {
$(offsetParent).on('scroll.' + namespace, function () {
that._position();
});
}
// On tab focus will move to a different field (no keyboard navigation
// in the date picker - this might need to be changed).
$(document).on('keydown.' + namespace, function (e) {
if (
e.keyCode === 9 || // tab
e.keyCode === 27 || // esc
e.keyCode === 13 // return
) {
that._hide();
}
});
clearTimeout(this.s.showTo);
// We can't use blur to hide, as we want to keep the picker open while
// to let the user select from it. But if focus is moved outside of of
// the picker, then we auto hide.
this.dom.input.on('blur', function (e) {
that.s.showTo = setTimeout(function () {
let name = document.activeElement.tagName.toLowerCase();
if (document.activeElement === that.dom.input[0]) {
return;
}
if (that.dom.container.find(document.activeElement).length) {
return;
}
if (['input', 'select', 'button'].includes(name)) {
that.hide();
}
}, 10);
});
// Hide if clicking outside of the widget - but in a different click
// event from the one that was used to trigger the show (bubble and
// inline)
setTimeout(function () {
$(document).on('click.' + namespace, function (e) {
var parents = $(e.target).parents();
if (!parents.filter(that.dom.container).length && e.target !== that.dom.input[0]) {
that._hide();
}
});
}, 10);
},
/**
* Write the formatted string to the input element this control is attached
* to
*
* @private
*/
_writeOutput: function (focus, change) {
var date = this.s.d;
var out = '';
var input = this.dom.input;
if (date) {
out = this._convert(date, null, this.c.format);
}
input.val(out);
if (change === undefined || change) {
// Create a DOM synthetic event. Can't use $().trigger() as
// that doesn't actually trigger non-jQuery event listeners
var event = new Event('change', { bubbles: true });
input[0].dispatchEvent(event);
}
if (input.attr('type') === 'hidden') {
this.val(out, false);
}
if (focus) {
input.focus();
}
}
});
/**
* Use a specificmoment compatible date library
*/
DateTime.use = function (lib) {
dateLib = lib;
};
/**
* For generating unique namespaces
*
* @type {Number}
* @private
*/
DateTime._instance = 0;
/**
* To indicate to DataTables what type of library this is
*/
DateTime.type = 'DateTime';
/**
* Defaults for the date time picker
*
* @type {Object}
*/
DateTime.defaults = {
attachTo: 'body',
buttons: {
clear: false,
today: false
},
// Not documented - could be an internal property
classPrefix: 'dt-datetime',
// function or array of ints
disableDays: null,
// first day of the week (0: Sunday, 1: Monday, etc)
firstDay: 1,
format: 'YYYY-MM-DD',
hoursAvailable: null,
i18n: {
clear: 'Clear',
previous: 'Previous',
next: 'Next',
months: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
weekdays: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
amPm: ['am', 'pm'],
hours: 'Hour',
minutes: 'Minute',
seconds: 'Second',
unknown: '-',
today: 'Today'
},
maxDate: null,
minDate: null,
minutesAvailable: null,
minutesIncrement: 1, // deprecated
strict: true,
locale: 'en',
onChange: function () { },
secondsAvailable: null,
secondsIncrement: 1, // deprecated
// show the ISO week number at the head of the row
showWeekNumber: false,
// overruled by max / min date
yearRange: 25
};
DateTime.version = '1.5.6';
/**
* CommonJS factory function pass through. Matches DataTables.
* @param {*} root Window
* @param {*} jq jQUery
* @returns {boolean} Indicator
*/
DateTime.factory = function (root, jq) {
var is = false;
// Test if the first parameter is a window object
if (root && root.document) {
window = root;
document = root.document;
}
// Test if the second parameter is a jQuery object
if (jq && jq.fn && jq.fn.jquery) {
$ = jq;
is = true;
}
return is;
}
// Global export - if no conflicts
if (!window.DateTime) {
window.DateTime = DateTime;
}
// Global DataTable
if (window.DataTable) {
window.DataTable.DateTime = DateTime;
}
// Make available via jQuery
$.fn.dtDateTime = function (options) {
return this.each(function () {
new DateTime(this, options);
});
}
// Attach to DataTables if present
if ($.fn.dataTable) {
$.fn.dataTable.DateTime = DateTime;
$.fn.DataTable.DateTime = DateTime;
if ($.fn.dataTable.Editor) {
$.fn.dataTable.Editor.DateTime = DateTime;
}
}
export default DateTime;