UNPKG

datatables.net-datetime

Version:

DateTime picker for DataTables.net

1,596 lines 54.8 kB
/*! DateTime 2.0.0 for DataTables.net
 * Copyright (c) SpryMedia Ltd - datatables.net/license
 */

(function(factory){
	if (typeof define === 'function' && define.amd) {
		// AMD
		define(['datatables.net'], function (dt) {
			return factory(window, document, dt);
		});
	}
	else if (typeof exports === 'object') {
		// CommonJS
		var cjsRequires = function (root) {
			if (! root.DataTable) {
				require('datatables.net')(root);
			}
		};

		if (typeof window === 'undefined') {
			module.exports = function (root) {
				if (! root) {
					// CommonJS environments without a window global must pass a
					// root. This will give an error otherwise
					root = window;
				}

				cjsRequires(root);
				return factory(root, root.document, root.DataTable);
			};
		}
		else {
			cjsRequires(window);
			module.exports = factory(window, window.document, window.DataTable);
		}
	}
	else {
		// Browser
		factory(window, document, window.DataTable);
	}
}(function(window, document, DataTable) {
'use strict';

var Dom = DataTable.Dom;
var util = DataTable.util;


// Supported formatting and parsing libraries:
// * Moment
// * Luxon
// * DayJS
var dateLib;
class DateTime {
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * Statics
     */
    /**
     * Use a specific compatible date library
     */
    static use(lib) {
        dateLib = lib;
    }
    /* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
     * Public methods
     */
    /**
     * Destroy the control
     */
    destroy() {
        var namespace = this.s.namespace;
        clearTimeout(this.s.showTo);
        this._hide(true);
        this.dom.container.off().empty();
        this.dom.input
            .classRemove('dt-datetime')
            .attrRemove('autocomplete')
            .off('.datetime');
        Dom.s(document).off('.' + namespace);
        Dom.w.off('.' + namespace);
    }
    display(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 this.s.display
            ? {
                month: this.s.display.getUTCMonth() + 1,
                year: this.s.display.getUTCFullYear()
            }
            : {
                month: null,
                year: null
            };
    }
    errorMsg(msg) {
        var error = this.dom.error;
        if (msg) {
            error.html(msg);
        }
        else {
            error.empty();
        }
        return this;
    }
    hide() {
        this._hide();
        return this;
    }
    max(date) {
        this.c.maxDate = typeof date === 'string' ? new Date(date) : date;
        this._optionsTitle();
        this._setCalander();
        return this;
    }
    min(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(node) {
        return Dom.s(node).closest(this.dom.container.get(0)).count() > 0;
    }
    val(set, write = true) {
        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
        if (this.s.d) {
            this.s.display = new Date(this.s.d.toString());
        }
        else if (this.c.display) {
            this.s.display = new Date();
            this.s.display.setUTCDate(1);
            this.display(this.c.display.year, this.c.display.month);
        }
        else {
            this.s.display = 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(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
     */
    constructor(input, opts) {
        // Attempt to auto detect the formatting library (if there is one).
        // Having it in the constructor allows load order independence.
        let win = window;
        if (typeof dateLib === 'undefined') {
            dateLib = win.moment
                ? win.moment
                : win.dayjs
                    ? win.dayjs
                    : win.luxon
                        ? win.luxon
                        : null;
        }
        this.c = util.object.assignDeep({}, 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 = Dom.c('div')
            .classAdd(classPrefix)
            .html('<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>' +
            '<a class="' +
            classPrefix +
            '-selected"></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>');
        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'),
            selected: structure.find('.' + classPrefix + '-selected'),
            previous: structure.find('.' + classPrefix + '-iconLeft'),
            next: structure.find('.' + classPrefix + '-iconRight'),
            input: Dom.s(input)
        };
        this.s = {
            d: null,
            display: null,
            minutesRange: null,
            secondsRange: null,
            namespace: 'datetime-' + DateTime._instance++,
            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
            },
            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.classAdd('dt-datetime');
        this._init();
    }
    /**
     * Build the control and assign initial event handlers
     */
    _init() {
        var that = this;
        var classPrefix = this.c.classPrefix;
        var last = this.dom.input.val();
        var namespace = this.s.namespace;
        var onChange = function () {
            var curr = that.dom.input.val();
            if (curr !== last) {
                that.c.onChange.call(that, curr, that.s.d, that.dom.input.get(0));
                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');
        }
        if (!this.c.buttons.selected) {
            this.dom.selected.css('display', 'none');
        }
        // Render the options
        this._optionsTitle();
        Dom.s(document).on('i18n.dt.' + namespace, function (e, settings) {
            if (settings.language.datetime) {
                util.object.assignDeep(that.c.i18n, settings.language.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.c.alwaysVisible) {
            this.dom.container.classAdd('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.isVisible() ||
                that.dom.input.is(':disabled')) {
                return;
            }
            // In case the value has changed by text
            last = that.dom.input.val();
            that.val(last, false);
            that._show();
        })
            .on('keyup.datetime', function () {
            // Update the calendar's displayed value as the user types
            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.get(0).addEventListener('focusin', function (e) {
            e.stopPropagation();
        });
        // Main event handlers for input in the widget
        this.dom.container
            .on('change', 'select', function () {
            var select = Dom.s(this);
            var val = parseInt(select.val());
            if (select.classHas(classPrefix + '-month')) {
                // Month select
                that._correctMonth(that.s.display, val);
                that._setTitle();
                that._setCalander();
            }
            else if (select.classHas(classPrefix + '-year')) {
                // Year select
                that.s.display.setUTCFullYear(val);
                that._setTitle();
                that._setCalander();
            }
            else if (select.classHas(classPrefix + '-hours') ||
                select.classHas(classPrefix + '-ampm')) {
                // Hours - need to take account of AM/PM input if present
                if (that.s.parts.hours12) {
                    var hours = parseInt(that.dom.container
                        .find('.' + classPrefix + '-hours')
                        .val(), 10);
                    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.classHas(classPrefix + '-minutes')) {
                // Minutes select
                that.s.d.setUTCMinutes(val);
                that._setTime();
                that._writeOutput(true);
                onChange();
            }
            else if (select.classHas(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 (Dom.s(target).classHas(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 (Dom.s(target).classHas(classPrefix + '-today')) {
                    // Don't change the value, but jump to the month
                    // containing today
                    that.s.display = new Date();
                    that._setTitle();
                    that._setCalander();
                }
                else if (Dom.s(target).classHas(classPrefix + '-selected')) {
                    // Don't change the value, but jump to where the
                    // selected value is
                    that.s.display = new Date(that.s.d.getTime());
                    that._setTitle();
                    that._setCalander();
                }
            }
            if (nodeName === 'button') {
                var button = Dom.s(target);
                var parent = button.parent();
                if (parent.classHas('disabled') &&
                    !parent.classHas('range')) {
                    button.blur();
                    return;
                }
                if (parent.classHas(classPrefix + '-iconLeft')) {
                    // Previous month
                    that.s.display.setUTCMonth(that.s.display.getUTCMonth() - 1);
                    that._setTitle();
                    that._setCalander();
                    that.dom.input.focus();
                }
                else if (parent.classHas(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.closest('.' + classPrefix + '-time').count()) {
                    var val = button.data('value');
                    var unit = button.data('unit');
                    d = that._needValue();
                    if (unit === 'minutes') {
                        if (parent.classHas('disabled') &&
                            parent.classHas('range')) {
                            that.s.minutesRange = val;
                            that._setTime();
                            return;
                        }
                        else {
                            that.s.minutesRange = null;
                        }
                    }
                    if (unit === 'seconds') {
                        if (parent.classHas('disabled') &&
                            parent.classHas('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 a Date 1
     * @param b Date 2
     */
    _compareDates(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 val Value
     * @param from Format to convert from. If null a `Date` must be given
     * @param to Format to convert to. If null a `Date` will be returned
     * @returns Converted value
     */
    _convert(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 && val instanceof Date) {
                // Date in, string back
                return (val.getUTCFullYear() +
                    '-' +
                    this._pad(val.getUTCMonth() + 1) +
                    '-' +
                    this._pad(val.getUTCDate()));
            }
            else if (typeof val === 'string') {
                // (! to)
                // String in, date back
                var match = val.match(/(\d{4})\-(\d{2})\-(\d{2})/);
                return match
                    ? new Date(parseInt(match[1]), parseInt(match[2]) - 1, parseInt(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 - will be modified
     * @param month Month to set
     */
    _correctMonth(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  year  Year
     * @param  month Month (starting at 0)
     */
    _daysInMonth(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  s Date to "convert"
     * @return Shifted date
     */
    _dateToUtc(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  d Date to "convert"
     * @return ISO formatted date
     */
    _dateToUtcString(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
     *
     * @param destroy Flag to indicate that the instance is being destroyed
     */
    _hide(destroy = false) {
        if (!destroy &&
            (this.dom.input.attr('type') === 'hidden' || this.c.alwaysVisible)) {
            // Normally we wouldn't need to redraw the calander if it changes
            // and then hides, but if it is hidden, then we do need to make sure
            // that it is correctly up to date.
            this._setCalander();
            this._setTime();
            return;
        }
        var namespace = this.s.namespace;
        this.dom.container.detach();
        Dom.w.off('.' + namespace);
        Dom.s(document)
            .off('keydown.' + namespace)
            .off('keyup.' + namespace)
            .off('click.' + namespace);
        Dom.s('div.dt-scroll').off('scroll.' + namespace);
        Dom.s('div.DTE_Body_Content').off('scroll.' + namespace);
        Dom.s(this.dom.input.get(0).offsetParent).off('.' + namespace);
    }
    /**
     * Convert a 24 hour value to a 12 hour value
     *
     * @param val 24 hour value
     * @return 12 hour value
     */
    _hours24To12(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(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  year  Year
     * @param  month Month (starting at 0)
     * @return Calendar month HTML
     */
    _htmlMonth(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 === null) {
            this.c.firstDay = this._localeFirstDay();
        }
        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) &&
                disableDays.includes(day.getUTCDay())) {
                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
     */
    _htmlMonthHead() {
        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  d Day of month
     * @param  m Month of year (zero index)
     * @param  y Year
     * @return HTML string for a day
     */
    _htmlWeekOfYear(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() {
        return dateLib &&
            dateLib.DateTime &&
            dateLib.Duration &&
            dateLib.Settings
            ? true
            : false;
    }
    /**
     * Determine if Moment is being used
     *
     * @returns Flag for Moment
     */
    _isMoment() {
        return dateLib &&
            typeof dateLib.isDate === 'function' &&
            typeof dateLib.weekdays === 'function'
            ? true
            : false;
    }
    /**
     * Determine the first day of the week based on the current locale
     */
    _localeFirstDay() {
        let locale = this.c.locale;
        if (locale === 'en') {
            // Somewhat bonkers way to get the current locale, but there doesn't
            // seem to be a better way
            let browserLocale = new Intl.NumberFormat().resolvedOptions()
                .locale;
            // Not yet in Firefox
            if (Intl.Locale) {
                let loc = new Intl.Locale(browserLocale);
                if (loc.getWeekInfo) {
                    let weekInfo = loc.getWeekInfo();
                    // Safari and new Chrome have it as a function
                    if (typeof weekInfo.firstDay === 'function') {
                        return weekInfo.firstDay();
                    }
                    else if (typeof weekInfo.firstDay === 'number') {
                        // Old Chrome as an accessor property
                        return weekInfo.firstDay;
                    }
                }
            }
        }
        if (this._isMoment()) {
            return dateLib.localeData(this.c.locale).firstDayOfWeek();
        }
        // There doesn't appear to be a way to do it in luxon
        // Default to Monday
        return 1;
    }
    /**
     * 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
     */
    _needValue() {
        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 selector Class name unique to the select element to use
     * @param values   Array of values
     * @param labels Array of labels. If given must be the same length as the
     *   values parameter.
     */
    _options(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(Dom.c('option').attr('value', values[i]).text(labels[i]));
        }
    }
    /**
     * Set an option and update the option's span pair (since the select element
     * has opacity 0 for styling)
     *
     * @param selector Class name unique to the select element to use
     * @param val      Value to set
     */
    _optionSet(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')
            .filter(e => e.selected);
        span.html(selected.count() !== 0 ? selected.text() : this.c.i18n.unknown);
    }
    /**
     * Create time options list.
     *
     * @param unit Time unit - hours, minutes or seconds
     * @param count Count range - 12, 24 or 60
     * @param val Existing value for this unit
     * @param allowed Values allow for selection
     * @param range Override range
     */
    _optionsTime(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.count()) {
            return;
        }
        var a = '';
        var span = 10;
        var button = function (value, label, className = null) {
            // 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 &&
                !allowed.includes(value)) {
                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()
            .html('<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
     */
    _optionsTitle() {
        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.selected.text(i18n.selected).text(i18n.selected);
        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
     */
    _pad(i) {
        return i < 10 ? '0' + i : i;
    }
    /**
     * Position the calendar to look attached to the input element
     */
    _position() {
        var offset = this.c.attachTo === 'input'
            ? this.dom.input.position()
            : this.dom.input.offset();
        var container = this.dom.container;
        var inputHeight = this.dom.input.height('outer');
        if (container.classHas('inline')) {
            container.insertAfter(this.dom.input);
            return;
        }
        if (this.s.parts.date && this.s.parts.time && Dom.w.width() > 550) {
            container.classAdd('horizontal');
        }
        else {
            container.classRemove('horizontal');
        }
        if (this.c.attachTo === 'input') {
            container
                .css({
                top: offset.top + inputHeight + 'px',
                left: offset.left + 'px'
            })
                .insertAfter(this.dom.input);
        }
        else {
            container
                .css({
                top: offset.top + inputHeight + 'px',
                left: offset.left + 'px'
            })
                .appendTo('body');
        }
        var calHeight = container.height('outer');
        var calWidth = container.width('outer');
        var scrollTop = Dom.w.scrollTop();
        // Correct to the bottom
        if (offset.top + inputHeight + calHeight - scrollTop > Dom.w.height()) {
            var newTop = offset.top - calHeight;
            container.css('top', newTop < 0 ? '0' : newTop + 'px');
        }
        // Correct to the right
        if (calWidth + offset.left > Dom.w.width()) {
            var newLeft = Dom.w.width() - calWidth - 5;
            // Account for elements which are inside a position absolute element
            if (this.c.attachTo === 'input') {
                newLeft -= container
                    .map(e => e.offsetParent)
                    .offset().left;
            }
            container.css('left', newLeft < 0 ? '0' : newLeft + 'px');
        }
    }
    /**
     * Create a simple array with a range of values
     *
     * @param  start   Start value (inclusive)
     * @param  end     End value (inclusive)
     * @param  inc Increment value
     * @return Created array
     */
    _range(start, end, inc = 1) {
        var a = [];
        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
     */
    _setCalander() {
        if (this.s.display) {
            this.dom.calendar
                .empty()
                .html(this._htmlMonth(this.s.display.getUTCFullYear(), this.s.display.getUTCMonth()));
        }
    }
    /**
     * Set the month and year for the calendar based on the current display date
     */
    _setTitle() {
        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
     */
    _setTime() {
        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
     *
     */
    _show() {
        var that = this;
        var namespace = this.s.namespace;
        this._position();
        // Need to reposition on scroll
        Dom.w.on('scroll.' + namespace + ' resize.' + namespace, function () {
            that._position();
        });
        Dom.s('div.DTE_Body_Content').on('scroll.' + namespace, function () {
            that._position();
        });
        Dom.s('div.dt-scroll').on('scroll.' + namespace, function () {
            that._position();
        });
        var offsetParent = this.dom.input.get(0).offsetParent;
        if (offsetParent !== document.body) {
            Dom.s(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).
        Dom.s(document).on('keydown.' + namespace, function (e) {
            if (that.dom.container.isVisible() &&
                (e.keyCode === 9 || // tab
                    e.keyCode === 13) // return
            ) {
                that._hide();
            }
        });
        // Esc is on keyup to allow Editor to know that the container was hidden
        // and thus not act on the esc itself.
        Dom.s(document).on('keyup.' + namespace, function (e) {
            if (that.dom.container.isVisible() && e.keyCode === 27) {
                // esc
                e.preventDefault();
                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.get(0)) {
                    return;
                }
                if (that.dom.container.find(document.activeElement).count()) {
                    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 () {
            Dom.s(document).on('click.' + namespace, function (e) {
                if (!Dom.s(e.target)
                    .closest(that.dom.container.get(0))
                    .count() &&
                    e.target !== that.dom.input.get(0)) {
                    that._hide();
                }
            });
        }, 10);
    }
    /**
     * Write the formatted string to the input element this control is attached
     * to
     */
    _writeOutput(focus = false, change = true) {
        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) {
            // 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.get(0).dispatchEvent(event);
        }
        if (input.attr('type') === 'hidden') {
            this.val(out, false);
        }
        if (focus) {
            input.focus();
        }
    }
}
/**
 * For generating unique namespaces
 */
DateTime._instance = 0;
/**
 * To indicate to DataTables what type of library this is
 */
DateTime.type = 'DateTime';
/**
 * Defaults for the date time picker
 */
DateTime.defaults = {
    alwaysVisible: false,
    attachTo: 'body',
    buttons: {
        clear: false,
        selected: false,
        today: false
    },
    classPrefix: 'dt-datetime',
    disableDays: null,
    display: null,
    firstDay: null,
    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',
        selected: 'Selected'
    },
    maxDate: null,
    minDate: null,
    minutesAvailable: null,
    minutesIncrement: 1, // deprecated
    strict: true,
    locale: 'en',
    onChange: () => { },
    secondsAvailable: null,
    secondsIncrement: 1, // deprecated
    showWeekNumber: false,
    yearRange: 25
};
DateTime.version = '2.0.0';
// Global export - if no conflicts
// TODO is this right in the UDM?
if (!window.DateTime) {
    window.DateTime = DateTime;
}
// Make available via jQuery
if (window.jQuery) {
    window.jQuery.fn.dtDateTime = function (options) {
        return this.each(function () {
            new DateTime(this, options);
        });
    };
}
// Attach to DataTables
DataTable.DateTime = DateTime;
DataTable.use('datetime', DateTime);
// And to Editor (legacy)
if (DataTable.Editor) {
    DataTable.Editor.DateTime = DateTime;
}




return DateTime;
}));