Source: DateInput2.js

import '../css/dateinput.css';
import ACore from "../ACore";
import {
    daysInMonth,
    beginOfDay,
    compareDate,
    formatDateString,
    parseDateString,
    formatDateTime, parseDateTime, DATE_TIME_TOKEN_RGX, weekIndexOf, prevDate
} from "absol/src/Time/datetime";
import ChromeCalendar from "./ChromeCalendar";
import OOP from "absol/src/HTML5/OOP";
import DateInput from "./DateInput";
import AElement from "absol/src/HTML5/AElement";
import DomSignal from "absol/src/HTML5/DomSignal";
import DateTimeInput from "./DateTimeInput";
import { zeroPadding } from "./utils";
import { hitElement } from "absol/src/HTML5/EventEmitter";

var STATE_NEW = 1;
var STATE_EDITED = 2;
var STATE_NONE = 0;


var _ = ACore._;
var $ = ACore.$;

/**
 * @extends AElement
 * @constructor
 */
function DateInput2() {
    this._lastValue = null;
    this._value = null;
    this._format = 'dd/MM/yyyy';
    this.$input = $('input', this);
    this._editingData = {};
    this.startDayOfWeek = 1;
    this.$text = this.$input;
    this.$text.on('mousedown', this.eventHandler.mouseDownInput)
        .on('mouseup', this.eventHandler.mouseUpInput)
        .on('dblclick', this.eventHandler.dblclickInput)
        .on('keydown', this.eventHandler.keydown)
        .on('blur', this.eventHandler.inputBlur)
        .on('contextmenu', function (event) {
            event.preventDefault();
        });

    this.$domSignal = _('attachhook').addTo(this);
    this.domSignal = new DomSignal(this.$domSignal);
    this.domSignal.on('request_auto_select', this._autoSelect.bind(this));

    this._min = new Date(1890, 0, 1, 0, 0, 0, 0);
    this._max = new Date(2090, 0, 1, 0, 0, 0, 0);
    this.$calendarBtn = $('.as-date-input-icon-ctn', this)
        .on('click', this.eventHandler.clickCalendarBtn);
    this.$clearBtn = $('button.as-date-input-clear-btn', this)
        .on('click', this.clear.bind(this));

    this.value = this._value;
    this.format = this._format;
    this.notNull = false;
    OOP.drillProperty(this, this, 'minLimitDate', 'min');
    OOP.drillProperty(this, this, 'minDateLimit', 'min');
    OOP.drillProperty(this, this, 'maxLimitDate', 'max');
    OOP.drillProperty(this, this, 'maxDateLimit', 'max');
}

DateInput2.tag = 'dateinput';
DateInput2.render = function () {
    //only support dd/mm/yyyy
    return _({
        class: 'as-date-input',
        extendEvent: ['change'],
        child: [{
            tag: 'input',
            class: 'as-date-input-text',
            props: {
                value: '__/__/____'
            }
        }, {
            tag: 'button',
            class: 'as-date-input-clear-btn',
            child: 'span.mdi.mdi-close-circle'
        }, {
            tag: 'button',
            class: 'as-date-input-icon-ctn',
            child: 'span.mdi.mdi-calendar'
        }]
    });
};


/**
 * @param {String} text
 */
DateInput2.prototype._verifyFormat = function (text) {
    var regex = /([,.\-\/])|([a-zA-Z0-9]+)/g;
    var tokens = text.match(regex);
    var count = [['dd', 'd'], ['M', 'MM'], ['yy', 'yyyy']].map(function (list) {
        return list.reduce(function (ac, cr) {
            if (tokens.indexOf(cr) >= 0) return ac + 1;
            return ac;
        }, 0);
    });
    return count[0] <= count[1] && count[1] <= count[2] && count[2] === 1;
};

DateInput2.prototype._notifyIfChange = function (event) {
    if (!this._lastValue !== !this._value || (this._lastValue && compareDate(this._lastValue, this._value)) !== 0) {
        this._lastValue = this._value;
        this.emit('change', { type: 'change', target: this, value: this._value, originEvent: event }, this);
    }
};

DateInput2.prototype.notifyChange = function () {
    this._lastValue = this._value;
    this.emit('change', { type: 'change', target: this, value: this._value }, this);
};

DateInput2.prototype.focus = function () {
    this.$input.focus();
};

DateInput2.prototype.blur = function () {
    this.$input.blur();
};

DateInput2.prototype.clear = function () {
    this._applyValue(null);
    this._notifyIfChange();
};

/***
 *
 * @param {Date|null} value
 */
DateInput2.prototype._applyValue = function (value) {
    this._value = value;
    if (!value) {
        this.$input.value = this.format;
    }
    else {
        this.$input.value = formatDateTime(this._value, this._format);
    }
    this._updateNullClass();
};

DateInput2.prototype._updateNullClass = function () {
    var value = this._value;
    if (!value) {
        this.addClass('as-value-null');
    }
    else {
        this.removeClass('as-value-null');
    }
};


DateInput2.prototype.tokenRegex = DateTimeInput.prototype.tokenRegex;

DateInput2.prototype._autoSelect = DateTimeInput.prototype._autoSelect;
DateInput2.prototype._tokenAt = DateTimeInput.prototype._tokenAt;
DateInput2.prototype._editNextToken = DateTimeInput.prototype._editNextToken;
DateInput2.prototype._editPrevToken = DateTimeInput.prototype._editPrevToken;
DateInput2.prototype._makeTokenDict = DateTimeInput.prototype._makeTokenDict;

DateInput2.prototype._correctingInput = function () {
    var tkDict = this._makeTokenDict(this.$text.value);
    var min = this._min;
    var max = this._max;
    var equalMin;
    var equalMax;
    if (!isNaN(tkDict.y.value)) {
        tkDict.y.value = Math.max(min.getFullYear(), Math.min(max.getFullYear(), tkDict.y.value));
        equalMin = tkDict.y.value === min.getFullYear();
        equalMax = tkDict.y.value === max.getFullYear();
    }
    else {
        equalMin = false;
        equalMax = false;
    }

    if (tkDict.M && !isNaN(tkDict.M.value)) {
        tkDict.M.value = Math.max(1, Math.min(12, tkDict.M.value));
        if (equalMin) {
            tkDict.M.value = Math.max(min.getMonth() + 1, tkDict.M.value);
            equalMin = tkDict.M.value === min.getMonth() + 1;
        }

        if (equalMax) {
            tkDict.M.value = Math.min(max.getMonth() + 1, tkDict.M.value);
            equalMax = max.getMonth() + 1;
        }
    }
    else {
        equalMin = false;
        equalMax = false;
    }

    if (tkDict.d && !isNaN(tkDict.d.value)) {
        tkDict.d.value = Math.max(1, Math.min(31, tkDict.d.value));
        if (!isNaN(tkDict.M.value)) {
            tkDict.d.value = Math.min(tkDict.d.value,
                daysInMonth(isNaN(tkDict.y.value) ? 2020 : tkDict.y.value, tkDict.M.value - 1));
        }

        if (equalMin) {
            tkDict.d.value = Math.max(min.getDate(), tkDict.d.value);
        }

        if (equalMax) {
            tkDict.d.value = Math.min(max.getDate(), tkDict.d.value);
        }
    }

    if (tkDict.w && !isNaN(tkDict.w.value)) {
        if (!isNaN(tkDict.y.value)) {
            tkDict.w.value = Math.max(1, Math.min(tkDict.w.value, 1
                + weekIndexOf(prevDate(new Date(tkDict.y.value + 1, 0, 1)), false, this._startDayOfWeek)));
        }
    }

    this.$text.value = this._applyTokenDict(this._format, tkDict);
}

DateInput2.prototype._correctingCurrentToken = function () {
    var token = this._tokenAt(this.$text.selectionStart);
    if (!token) return;
    var value;

    value = parseInt(token.text);
    var rqMin = {
        d: 1, dd: 1,
        M: 1, MM: 1,
        y: 1890, yyyy: 1890,
        w: 1, ww: 1
    }[token.ident];
    var rqMax = {
        d: 31, dd: 31,
        M: 12, MM: 12,
        y: 2089, yyyy: 2089,
        w: 54, ww: 54
    }[token.ident];
    if (rqMin !== undefined) {
        if (!isNaN(value)) {
            if ((value < rqMin || value > rqMin)) {
                value = Math.max(rqMin, Math.min(rqMax, value));
                token.replace(zeroPadding(value, token.ident.length), false);
            }
        }
        else if (this.notNull) {
            if (token.ident.startsWith('y')) {
                value = new Date().getFullYear();
            }
            else {
                value = rqMin;
            }
            token.replace(zeroPadding(value, token.ident.length), false);
        }
        else if (token.text !== token.ident) {
            token.replace(token.ident, false);
        }
    }
};

/***
 *
 * @param {Date|string|null}date
 * @return {Date|null}
 */
DateInput2.prototype._normalizeValue = function (date) {
    var temp;
    if (date === null || date === undefined || date === false) {
        return null;
    }

    if (typeof date === 'string') {
        temp = new Date(date);
        if (isNaN(temp.getTime())) {
            temp = parseDateTime(date, this._format);
        }
        date = temp;
    }
    else if (typeof date === 'number') {
        date = new Date(date);
    }
    if (date.getTime && date.getHours) {
        if (isNaN(date.getTime())) {
            return null;
        }
        else {
            return beginOfDay(date);
        }
    }
    else {
        return null;
    }
};

DateInput2.prototype._loadValueFromInput = function () {
    var tkDict = this._makeTokenDict(this.$text.value);
    var y = tkDict.y ? tkDict.y.value : new Date().getFullYear();
    var m = tkDict.M ? tkDict.M.value - 1 : 0;
    var d = tkDict.d ? tkDict.d.value : 1;
    var date = new Date(y, m, d);
    if (isNaN(date.getTime())) {
        this._value = null;
    }
    else {
        this._value = date;
    }
    this._updateNullClass();
};

DateInput2.prototype._applyTokenDict = function (format, dict, debug) {
    var rgx = new RegExp(this.tokenRegex.source, 'g');
    var tokenMap = this.tokenMap;
    var res = format.replace(rgx, function (full, g1, g2, sourceText) {
        if (g1 && tokenMap[g1]) {
            var ident = tokenMap[g1];
            if (dict[ident] && !isNaN(dict[ident].value)) {
                return zeroPadding(dict[ident].value, g1.length);
            }
            else {
                return full;
            }
        }
        else
            return full;
    });
    return res;
};

DateInput2.prototype.focus = function () {
    this.$text.focus();
    this.$text.select();
};


DateInput2.prototype.tokenMap = {
    d: 'd',
    dd: 'd',
    M: 'M',
    MM: 'M',
    y: 'y',
    yyyy: 'y',
    ww: 'w'
}

/**
 * @type {DateInput2}
 */
DateInput2.eventHandler = {};


DateInput2.eventHandler.keydown = function (event) {
    if (this.readOnly) {
        if (!event.ctrlKey || event.key !== 'c') {
            event.preventDefault();
        }
        return;
    }
    var token = this._tokenAt(this.$text.selectionStart);
    var endToken = this._tokenAt(this.$text.selectionEnd);
    if (!token) {
        if (event.key === 'Enter') {
            this._correctingInput();
            this._loadValueFromInput();
            this._notifyIfChange(event);
        }
        return;
    }
    var newTokenText;
    var value;
    if (event.key.startsWith('Arrow') || event.key.match(/^[\-/,\s]$/)) {
        event.preventDefault();
        switch (event.key) {
            case 'ArrowLeft':
                this._editPrevToken();
                break;
            case 'ArrowRight':
            case '-':
            case ',':
            case '/':
            case ' ':
                this._editNextToken();
                break;
            case 'ArrowUp':
            case 'ArrowDown':
                switch (token.ident) {
                    case 'dd':
                    case 'd':
                        value = parseInt(token.text);
                        if (isNaN(value)) {
                            this._editingData.d = event.key === 'ArrowUp' ? 1 : 31;
                        }
                        else {
                            this._editingData.d = 1 + (value + (event.key === 'ArrowUp' ? 0 : 29)) % 31;
                        }
                        newTokenText = '' + this._editingData.d;
                        while (newTokenText.length < token.ident.length) newTokenText = '0' + newTokenText;
                        token.replace(newTokenText, true);
                        break;
                    case 'w':
                    case 'ww':
                        value = parseInt(token.text);
                        if (isNaN(value)) {
                            this._editingData.w = event.key === 'ArrowUp' ? 1 : 54;
                        }
                        else {
                            this._editingData.w = 1 + (value + (event.key === 'ArrowUp' ? 0 : 52)) % 54;
                        }
                        newTokenText = zeroPadding(this._editingData.w, token.ident.length);
                        token.replace(newTokenText, true);
                        break;
                    case 'MM':
                    case 'M':
                        value = parseInt(token.text) - 1;
                        if (isNaN(value)) {
                            this._editingData.M = event.key === 'ArrowUp' ? 0 : 11;
                        }
                        else {
                            this._editingData.M = (value + (event.key === 'ArrowUp' ? 1 : 11)) % 12;
                        }
                        newTokenText = '' + (this._editingData.M + 1);
                        while (newTokenText.length < token.ident.length) newTokenText = '0' + newTokenText;
                        token.replace(newTokenText, true);
                        break;
                    case 'yyyy':
                        value = parseInt(token.text);
                        if (isNaN(value)) {
                            this._editingData.y = new Date().getFullYear();
                        }
                        else {
                            this._editingData.y = Math.max(1890, Math.min(2089, value + (event.key === 'ArrowUp' ? 1 : -1)));
                        }

                        newTokenText = this._editingData.y + '';
                        while (newTokenText.length < token.ident.length) newTokenText = '0' + newTokenText;
                        token.replace(newTokenText, true);
                        break;

                }
        }
    }
    else if (event.key === "Delete" || event.key === 'Backspace') {
        event.preventDefault();
        if (endToken.idx !== token.idx) {
            if (this.notNull) {
                this.$text.value = formatDateTime(new Date(Math.min(this.max.getTime(), Math.max(this.min.getTime(), new Date().getTime()))), this._format);
            }
            else {
                this.$text.value = this._format;
            }
            this.$text.select();
        }
        else {
            if (this.notNull) {
                switch (token.ident) {
                    case 'y':
                    case 'yyyy':
                        token.replace(zeroPadding(new Date().getFullYear(), token.ident.length), true);
                        break;
                    case 'w':
                    case 'ww':
                        token.replace(zeroPadding(1, token.ident.length), true);
                        break;
                    case 'M':
                    case 'MM':
                    case 'd':
                    case 'dd':
                        token.replace(zeroPadding(1, token.ident.length), true);
                        break;
                    default:
                        token.replace(token.ident, true);
                }
            }
            else {
                token.replace(token.ident, true);
            }
            if (event.key === "Delete") this._editNextToken();
            else this._editPrevToken();
        }
    }
    else if (event.key === "Enter" || event.key === 'Tab') {
        this._correctingInput();
        this._loadValueFromInput();
        this._notifyIfChange(event);
    }
    else if (event.ctrlKey) {
        switch (event.key) {
            case 'a':
            case 'A':
                break;
            case 'c':
            case 'C':
                break;
            case 'x':
            case 'X':
                this.domSignal.once('clear_value', function () {
                    this.$text.value = this._format;
                    this.$text.select();
                }.bind(this));
                this.domSignal.emit('clear_value');
                break;
            default:
                event.preventDefault();
        }
    }
    else if (event.key.match(/^[0-9]$/g)) {
        event.preventDefault();
        var dVal = parseInt(event.key);
        if (this._editingData.state === STATE_NEW) {
            switch (token.ident) {
                case 'dd':
                case 'd':
                    token.replace(zeroPadding(dVal, token.ident.length), true);
                    this._editingData.state = STATE_EDITED;
                    this._editingData.d = dVal;
                    if (dVal > 3) {
                        this._editNextToken();
                    }
                    break;
                case 'w':
                case 'ww':
                    token.replace(zeroPadding(dVal, token.ident.length), true);
                    this._editingData.state = STATE_EDITED;
                    this._editingData.d = dVal;
                    if (dVal > 6) {
                        this._editNextToken();
                    }
                    break;
                case 'MM':
                case 'M':
                    token.replace(zeroPadding(dVal, token.ident.length), true);
                    this._editingData.state = STATE_EDITED;
                    this._editingData.M = dVal;
                    if (dVal > 1) {
                        this._editNextToken();
                    }
                    break;
                case 'yyyy':
                case 'y':
                    token.replace(zeroPadding(dVal, token.ident.length), true);
                    this._editingData.state = STATE_EDITED;
                    this._editingData.state_num = 1;
                    break;
            }
        }
        else {
            switch (token.ident) {
                case 'dd':
                case 'd':
                    dVal = (parseInt(token.text.split('').pop()) || 0) * 10 + dVal;
                    dVal = Math.max(1, Math.min(31, dVal));
                    this._editingData.d = dVal;
                    token.replace(zeroPadding(dVal, token.ident.length), true);
                    this._editNextToken();
                    break;
                case 'ww':
                case 'w':
                    dVal = (parseInt(token.text.split('').pop()) || 0) * 10 + dVal;
                    dVal = Math.max(1, Math.min(54, dVal));
                    this._editingData.d = dVal;
                    token.replace(zeroPadding(dVal, token.ident.length), true);
                    this._editNextToken();
                    break;
                case 'MM':
                case 'M':
                    dVal = (parseInt(token.text.split('').pop()) || 0) * 10 + dVal;
                    dVal = Math.max(1, Math.min(12, dVal));
                    this._editingData.M = dVal - 1;
                    token.replace(zeroPadding(dVal, token.ident.length), true);
                    this._editNextToken();
                    break;
                case 'yyyy':
                case 'y':
                    dVal = (parseInt(token.text.replace(/^./, '')) || 0) * 10 + dVal;
                    this._editingData.state_num++;
                    if (this._editingData.state_num >= 4) {
                        // dVal = Math.max(1890, Math.min(2089, dVal));
                        token.replace(zeroPadding(dVal, token.ident.length), true);
                        this._editNextToken();
                    }
                    else {
                        token.replace(zeroPadding(dVal, token.ident.length), true);
                    }
                    break;
            }
        }
    }
    else {
        event.preventDefault();
    }
};

DateInput2.eventHandler.mouseUpInput = DateTimeInput.eventHandler.mouseUpInput;

DateInput2.eventHandler.mouseDownInput = DateTimeInput.eventHandler.mouseDownInput;

DateInput2.eventHandler.dblclickInput = DateTimeInput.eventHandler.dblclickInput;
DateInput2.eventHandler.inputBlur = DateTimeInput.eventHandler.inputBlur;

DateInput2.eventHandler.calendarSelect = function (value) {
    this.value = value;
    this.notifyChange();
};

DateInput2.eventHandler.clickCalendarBtn = function () {
    if (this.readOnly) return;
    this._attachCalendar();
};

DateInput2.eventHandler.clickOut = function (event) {
    if (hitElement(this.share.$calendar, event)) return;
    this._releaseCalendar();
};

DateInput2.eventHandler.calendarPick = function (event) {
    this._applyValue(event.value);
    this._notifyIfChange(event.originEvent || event);
    this._releaseCalendar();
};


DateInput2.property = {};

DateInput2.property.value = {
    set: function (value) {
        value = this._normalizeValue(value);
        if (!value && this.notNull) value = beginOfDay(new Date());
        this._lastValue = value;
        this._applyValue(value);
    },
    get: function () {
        return this._value;
    }
};

/**
 * not support MMM, MMMM, support number only
 * @type {DateInput2}
 */
DateInput2.property.format = {
    set: function (value) {
        value = value || 'dd/MM/yyyy';
        value = value.replace(/m/g, 'M');
        value = value.replace(/MM([M]+)/, 'MM');
        if (!this._verifyFormat(value)) {
            value = 'dd/MM/yyyy';
            console.error("Invalid date format: " + value);
        }
        this._format = value;
        this._formatTokens = this._format.match(new RegExp(DATE_TIME_TOKEN_RGX.source, 'g')) || [];
        this.value = this.value;//update
    },
    get: function () {
        return this._format;
    }
};

DateInput2.property.disabled = {
    set: function (value) {
        value = !!value;
        this.$input.disabled = value;
        if (value) this.addClass('as-disabled');
        else this.removeClass('as-disabled');
        this.$text.disabled = value;
    },
    get: function () {
        return this.$input.disabled;
    }
};

DateInput2.property.readOnly = {
    set: function (value) {
        if (value) {
            this.addClass('as-read-only');
            this.$input.readOnly = true;
        }
        else {
            this.removeClass('as-read-only');
            this.$input.readOnly = false;

        }
    },
    get: function () {
        return this.hasClass('as-read-only');
    }
};

DateInput2.property.text = {
    get: function () {
        return this.$input.value;
    }
};

/***
 * @memberOf DateInput2
 * @name calendarLevel
 * @type {number}
 */
DateInput2.property.calendarLevel = {
    get: function () {
        if (this._formatTokens.indexOf('d') >= 0 || this._formatTokens.indexOf('dd') >= 0) return 'day';
        if (this._formatTokens.indexOf('w') >= 0 || this._formatTokens.indexOf('ww') >= 0) return 'week';
        if (this._formatTokens.indexOf('M') >= 0 || this._formatTokens.indexOf('MM') >= 0) return 'month';
        return 'year';
    }
};


DateInput2.property.min = {
    set: function (value) {
        this._min = this._normalizeValue(value) || new Date(1890, 0, 1);
    },
    get: function () {
        return this._min;
    }
};

DateInput2.property.max = {
    set: function (value) {
        this._max = this._normalizeValue(value) || new Date(2090, 0, 1);
    },
    get: function () {
        return this._max;
    }
};


DateInput2.property.notNull = {
    set: function (value) {
        if (value) {
            this.addClass('as-must-not-null');
            if (!this.value) this.value = new Date();
        }
        else {
            this.removeClass('as-must-not-null');
        }
    },
    get: function () {
        return this.hasClass('as-must-not-null');
    }
};


DateInput2.prototype.share = {
    /***
     * @type {ChromeCalendar}
     */
    $calendar: null,
    /***
     * @type {Follower}
     */
    $follower: null,
    /***
     * @type {DateInput2}
     */
    $holdingInput: null
};

DateInput2.prototype._prepareCalendar = function () {
    if (this.share.$calendar) return;

    this.share.$calendar = _({
        tag: 'chromecalendar',
        class: ['as-date-input-calendar', 'as-dropdown-box-common-style']
    });
    this.share.$follower = _({
        tag: 'follower',
        class: 'as-date-input-follower',
        child: this.share.$calendar
    });
};

DateInput2.prototype._attachCalendar = function () {
    this._prepareCalendar();
    if (this.share.$holdingInput) this.share.$holdingInput._releaseCalendar();
    this.share.$follower.addTo(document.body);
    this.share.$follower.addStyle('visibility', 'hidden');
    this.share.$holdingInput = this;
    this.share.$follower.followTarget = this;
    this.share.$calendar.level = this.calendarLevel;
    this.share.$calendar.startDayOfWeek = this.startDayOfWeek || 0;
    this.share.$calendar.min = this._min;
    this.share.$calendar.max = this._max;
    this.share.$calendar.on('pick', this.eventHandler.calendarPick);
    this.share.$calendar.selectedDates = this.value ? [this.value] : [];
    this.share.$calendar.viewDate = this.value ? this.value
        : new Date(Math.max(this._min.getTime(), Math.min(this._max.getTime(), new Date().getTime())));
    setTimeout(function () {
        document.body.addEventListener('click', this.eventHandler.clickOut);
        this.share.$follower.removeStyle('visibility');
    }.bind(this), 5);
    this.$calendarBtn.off('click', this.eventHandler.clickCalendarBtn);
};

DateInput2.prototype._releaseCalendar = function () {
    if (this.share.$holdingInput !== this) return;
    this.share.$calendar.off('pick', this.eventHandler.calendarPick);
    this.share.$follower.remove();
    document.body.removeEventListener('click', this.eventHandler.clickOut);
    setTimeout(function () {
        this.$calendarBtn.on('click', this.eventHandler.clickCalendarBtn);
    }.bind(this), 5)
    this.share.$holdingInput = null;
};


ACore.install(DateInput2);

export default DateInput2;