zul/src/main/resources/web/js/zul/db/Timebox.ts

Summary

Maintainability
A
0 mins
Test Coverage
/* Timebox.ts

    Purpose:
        testing textbox.intbox.spinner,timebox,doublebox,longbox and decimalbox on zk5
    Description:

    History:
        Thu June 11 10:17:24     2009, Created by kindalu

Copyright (C) 2009 Potix Corporation. All Rights Reserved.

This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
*/
var LEGAL_CHARS = 'ahKHksmz',
    /* constant for MINUTE (m) field alignment. */
    MINUTE_FIELD = 1,
    /* constant for SECOND (s) field alignment. */
    SECOND_FIELD = 2,
    /* constant for AM_PM (a) field alignment. */
    AM_PM_FIELD = 3,
    /* constant for HOUR0 (H) field alignment. (Hour in day (0-23)) */
    HOUR0_FIELD = 4,
    /* constant for HOUR1 (k) field alignment. (Hour in day (1-24)) */
    HOUR1_FIELD = 5,
    /* constant for HOUR2 (h) field alignment. (Hour in am/pm (1-12)) */
    HOUR2_FIELD = 6,
    /* constant for HOUR3 (K) field alignment. (Hour in am/pm (0-11)) */
    HOUR3_FIELD = 7,
    globallocalizedSymbols: Record<string, zk.LocalizedSymbols> = {};

@zk.WrapClass('zul.db.Timebox')
export class Timebox extends zul.inp.FormatWidget<DateImpl> {
    /** @internal */
    _buttonVisible = true;
    /** @internal */
    override readonly _format = 'HH:mm';
    /** @internal */
    _timezoneAbbr = '';
    /** @internal */
    _fmthdler!: TimeHandler[];
    /** @internal */
    _timeZone?: string;
    /** @internal */
    _unformater?: string;
    /** @internal */
    static _unformater?: zul.db.Unformater;
    /** @internal */
    _changed?: boolean;
    type?: number;
    timerId?: number;
    lastPos?: number;
    /** @internal */
    _lastPos?: number;
    /** @internal */
    _constraint?: string;
    /** @internal */
    _currentbtn?: HTMLElement;

    constructor() {
        super();
        Timebox._updFormat(this, this._format);
    }

    getTimezoneAbbr(): string {
        return this._timezoneAbbr;
    }

    setTimezoneAbbr(timezoneAbbr: string, opts?: Record<string, boolean>): this {
        const o = this._timezoneAbbr;
        this._timezoneAbbr = timezoneAbbr;

        if (o !== timezoneAbbr || opts?.force) {
            Timebox._updFormat(this, this._format);
        }

        return this;
    }

    /**
     * Sets the time zone ID that this time box belongs to.
     * @param timeZone - the time zone's ID, such as "America/Los_Angeles".
     * @since 9.0.0
     */
    setTimeZone(timeZone: string, opts?: Record<string, boolean>): this {
        const o = this._timeZone;
        this._timeZone = timeZone;

        if (o !== timeZone || opts?.force) {
            this._value?.tz(timeZone);
        }

        return this;
    }

    /**
     * @returns the time zone ID that this time box belongs to, such as "America/Los_Angeles".
     * @since 9.0.0
     */
    override getTimeZone(): string | undefined {
        return this._timeZone;
    }

    /**
     * @returns whether the button (on the right of the textbox) is visible.
     * @defaultValue `true`.
     */
    isButtonVisible(): boolean {
        return this._buttonVisible;
    }

    /**
     * Sets whether the button (on the right of the textbox) is visible.
     */
    setButtonVisible(buttonVisible: boolean, opts?: Record<string, boolean>): this {
        const o = this._buttonVisible;
        this._buttonVisible = buttonVisible;

        if (o !== buttonVisible || opts?.force) {
            zul.inp.RoundUtl.buttonVisible(this, buttonVisible);
        }

        return this;
    }

    /**
     * Sets the unformater function. This method is called from Server side.
     * @param unformater - the unformater function
     */
    setUnformater(unformater: zul.db.Unformater, opts?: Record<string, boolean>): this {
        const o = this._unformater;
        this._unformater = unformater.toString();

        if (o !== this._unformater || opts?.force) {
            Timebox._unformater = unformater;
        }

        return this;
    }

    /**
     * @returns the unformater.
     */
    getUnformater(): string | undefined {
        return this._unformater;
    }

    getLocalizedSymbols(): zk.LocalizedSymbols | undefined {
        return this._localizedSymbols;
    }

    setLocalizedSymbols(localizedSymbols?: [string, zk.LocalizedSymbols]): this {
        if (localizedSymbols) {
            if (!globallocalizedSymbols[localizedSymbols[0]])
                globallocalizedSymbols[localizedSymbols[0]] = localizedSymbols[1];
            this._localizedSymbols = globallocalizedSymbols[localizedSymbols[0]];
        }
        return this;
    }

    /**
     * Sets the constraint.
     * @defaultValue `null` (means no constraint all all).
     */
    override setConstraint(constraint: string, opts?: Record<string, boolean>): this {
        const o = this._constraint;
        this._constraint = constraint;

        if (o !== constraint || opts?.force) {
            if (typeof constraint == 'string' && !constraint.startsWith('[')/*by server*/)
                this._cst = new zul.inp.SimpleLocalTimeConstraint(constraint, this);
            else
                this._cst = constraint;
            if (this._cst)
                this._reVald = true; //revalidate required
            // FIXME: never assigned
            // if (this._pop) {
            //     this._pop.setConstraint(this._constraint);
            //     this._pop.rerender();
            // }
        }

        return this;
    }

    /**
     * @returns the constraint, or null if no constraint at all.
     */
    override getConstraint(): string | undefined {
        return this._constraint;
    }

    /**
     * A method for component getter symmetry, it will call getValue
     * @since 10.0.0
     */
    getValueInZonedDateTime(): DateImpl | undefined {
        return this.getValue();
    }

    /**
     * A method for component setter symmetry, it will call setValue
     * @since 10.0.0
     */
    setValueInZonedDateTime(valueInZonedDateTime: DateImpl, fromServer?: boolean): this {
        return this.setValue(valueInZonedDateTime, fromServer);
    }

    /**
     * A method for component getter symmetry, it will call getValue
     * @since 10.0.0
     */
    getValueInLocalDateTime(): DateImpl | undefined {
        return this.getValue();
    }

    /**
     * A method for component setter symmetry, it will call setValue
     * @since 10.0.0
     */
    setValueInLocalDateTime(valueInLocalDateTime: DateImpl, fromServer?: boolean): this {
        return this.setValue(valueInLocalDateTime, fromServer);
    }

    /**
     * A method for component getter symmetry, it will call getValue
     * @since 10.0.0
     */
    getValueInLocalDate(): DateImpl | undefined {
        return this.getValue();
    }

    /**
     * A method for component setter symmetry, it will call setValue
     * @since 10.0.0
     */
    setValueInLocalDate(valueInLocalDate: DateImpl, fromServer?: boolean): this {
        return this.setValue(valueInLocalDate, fromServer);
    }

    /**
     * A method for component getter symmetry, it will call getValue
     * @since 10.0.0
     */
    getValueInLocalTime(): DateImpl | undefined {
        return this.getValue();
    }

    /**
     * A method for component setter symmetry, it will call setValue
     * @since 10.0.0
     */
    setValueInLocalTime(valueInLocalTime: DateImpl, fromServer?: boolean): this {
        return this.setValue(valueInLocalTime, fromServer);
    }

    /**
     * Sets the time zone ID that this time box belongs to.
     * @param timezone - the time zone's ID, such as "America/Los_Angeles".
     * @deprecated Use {@link setTimeZone} instead.
     */
    setTimezone(timezone: string): this {
        return this.setTimeZone(timezone);
    }

    /**
     * @returns the time zone ID that this time box belongs to, such as "America/Los_Angeles"
     * @since 8.5.1
     * @deprecated Use {@link getTimeZone} instead.
     */
    getTimezone(): string | undefined {
        return this._timeZone;
    }

    override inRoundedMold(): boolean {
        return true;
    }

    override setFormat(format: string, opts?: Record<string, boolean> | undefined): this {
        format = format ? format.replace(/'/g, '') : format;
        Timebox._updFormat(this, format);
        return super.setFormat(format, opts);
    }

    override setValue(value: DateImpl, fromServer?: boolean): this {
        var tz = this.getTimeZone();
        if (tz && value) value.tz(tz);
        if (fromServer && value == null) //Bug ZK-1322: if from server side, return empty string
            this._changed = false;
        return super.setValue(value, fromServer);
    }

    /** @internal */
    override coerceToString_(date?: DateImpl): string {
        if (!this._changed && !date && arguments.length) return '';
        var out = '', th: TimeHandler, text: string, offset: number | boolean;
        for (var i = 0, f = this._fmthdler, l = f.length; i < l; i++) {
            th = f[i];
            text = th.format(date);
            out += text;
            //sync handler index
            if (th.type && (offset = th.isSingleLength()) !== false
                && ((offset as number) += text.length - 1))
                th._doShift(this, offset as number);
        }
        return out;
    }

    /** @internal */
    override coerceFromString_(val: string | undefined): DateImpl | undefined {
        var unf = Timebox._unformater,
            tz = this.getTimeZone();
        if (unf && jq.isFunction(unf)) {
            var cusv = unf(val);
            if (cusv) {
                this._shortcut = val;
                return cusv;
            }
        }
        if (!val) return undefined;

        // F65-ZK-1825: use this._value instead of "today"
        // We cannot use this._value in this case, which won't trigger onChange
        // event. Using clone date instead.
        var date = this._value ? Dates.newInstance(this._value) : zUtl.today(this._format, tz),
            hasAM, isAM,
            fmt: TimeHandler[] = [],
            fmtObeyCount: boolean[] = [],
            emptyCount = 0;
        date.setSeconds(0);
        date.setMilliseconds(0);

        for (var i = 0, f = this._fmthdler, l = f.length; i < l; i++) {
            var th = f[i];
            if (th.type == AM_PM_FIELD) {
                hasAM = true;
                isAM = th.unformat(date, val, {});
                if (!th.getText(val).trim().length)
                    emptyCount++;
            } else if (th.type) {
                var shouldObeyCount = i + 1 < l && Timebox._shouldObeyCount(f[i + 1].type);
                fmt.push(th);
                fmtObeyCount.push(shouldObeyCount);
                th.parse(val, {obeyCount: shouldObeyCount}); // in order to shift if necessary
                if (!th.getText(val).trim().length) // ZK-4342: no need to pass obeyCount to determine if part is empty
                    emptyCount++;
            }
        }

        if (fmt.length == (hasAM ? --emptyCount : emptyCount)) {
            this._changed = false;//for return empty string
            return;
        }

        for (var i = 0, l = fmt.length; i < l; i++) {
            if (!hasAM && (fmt[i].type == HOUR2_FIELD || fmt[i].type == HOUR3_FIELD))
                isAM = true;
            date = fmt[i].unformat(date, val, {am: isAM, obeyCount: fmtObeyCount[i]}) as DateImpl; // FIXME: inconsistent use of unformat
        }
        return date;
    }

    override onSize(): void {
        var inp = this.getInputNode();
        if (inp && this._value && !inp.value)
            inp.value = this.coerceToString_(this._value);
    }

    onHide = undefined;
    validate = undefined;

    /** @internal */
    override doClick_(evt: zk.Event, popupOnly?: boolean): void {
        if (evt.domTarget == this.getInputNode())
            this._doCheckPos();
        super.doClick_(evt, popupOnly);
    }

    /** @internal */
    override doKeyPress_(evt: zk.Event): void {
        if (zk.opera && evt.keyCode != 9) {
            evt.stop();
            return;
        }
        super.doKeyPress_(evt);
    }

    /** @internal */
    _doBeforeInput(evt: zk.Event): void {
        var inp = this.getInputNode()!;
        if (inp.disabled || inp.readOnly)
            return;

        // control input keys only when no custom unformater is given
        if (!Timebox._unformater) {
            var char = (evt.domEvent!.originalEvent as InputEvent).data!;
            if (/\d/.test(char)) {
                this._doType(parseInt(char));
                evt.stop();
            }
        }
    }

    /** @internal */
    override doKeyDown_(evt: zk.Event): void {
        var inp = this.getInputNode()!;
        if (inp.disabled || inp.readOnly)
            return;

        // control input keys only when no custom unformater is given
        if (!Timebox._unformater) {
            var code = evt.keyCode;
            switch (code) {
            case 48: case 96://0
            case 49: case 97://1
            case 50: case 98://2
            case 51: case 99://3
            case 52: case 100://4
            case 53: case 101://5
            case 54: case 102://6
            case 55: case 103://7
            case 56: case 104://8
            case 57: case 105://9
                code = code - (code >= 96 ? 96 : 48);
                this._doType(code);
                evt.stop();
                return;
            case 35://end
                this.lastPos = inp.value.length;
                return;
            case 36://home
                this.lastPos = 0;
                return;
            case 37://left
                if (this.lastPos! > 0)
                    this.lastPos!--;
                return;
            case 39://right
                if (this.lastPos! < inp.value.length)
                    this.lastPos!++;
                return;
            case 38://up
                this._doUp();
                evt.stop();
                return;
            case 40://down
                this._doDown();
                evt.stop();
                return;
            case 46://del
                this._doDel();
                evt.stop();
                return;
            case 8://backspace
                this._doBack();
                evt.stop();
                return;
            case 9:
                // do nothing
                break;
            case 13: case 27://enter,esc,tab
                break;
            default:
                if (!(code >= 112 && code <= 123) //F1-F12
                        && !evt.ctrlKey && !evt.metaKey && !evt.altKey) {
                    if (evt.shiftKey && code == 45) break; // Allow macOS paste (shift + insert)
                    evt.stop();
                }
            }
        }
        super.doKeyDown_(evt);
    }

    /** @internal */
    override doPaste_(evt: zk.Event): void {
        this._updateChanged();
        super.doPaste_(evt);
    }

    /** @internal */
    _updateChanged(): void {
        var inp = this.getInputNode()!,
            self = this,
            curVal = inp.value;
        setTimeout(function () {
            var inpNode = self.getInputNode();
            if (inpNode) {
                if (curVal != inpNode.value) {
                    self._changed = true;
                }
            }
        }, 0);
    }

    /** @internal */
    _ondropbtnup(evt: zk.Event): void {
        this.domUnlisten_(document.body, 'onZMouseup', '_ondropbtnup');
        this._stopAutoIncProc();
        this._currentbtn = undefined;
    }

    /** @internal */
    _btnDown(evt: zk.Event): void { // TODO: format the value first
        if (!this._buttonVisible || this._disabled) return;

        // cache it for IE and others to keep the same position at the first time being clicked.
        this._lastPos = this._getPos();

        var btn = this.$n('btn'),
            inp = this.getInputNode()!;

        if (!zk.dragging) {
            if (this._currentbtn) // just in case
                this._ondropbtnup(evt);

            this.domListen_(document.body, 'onZMouseup', '_ondropbtnup');
            this._currentbtn = btn;
        }

        // if btn down before blur, needs to convert to real time string first
        if (inp.value && Timebox._unformater)
            inp.value = this.coerceToString_(this.coerceFromString_(inp.value));
        if (!inp.value)
            inp.value = this.coerceToString_();

        var ofs = zk(btn).revisedOffset(),
            isOverUpBtn = (evt.pageY - ofs[1]) < btn!.offsetHeight / 2;
        if (zk.webkit) {
            zk(inp).focus(); //Bug ZK-1527: chrome and safari will trigger focus if executing setSelectionRange, focus it early here
        }

        var newLastPos = this._getPos();

        // Chrome and Firefox get wrong position at initial case
        if (this._lastPos != newLastPos)
            zk(inp).setSelectionRange(this._lastPos);

        if (isOverUpBtn) { //up
            this._doUp();
            this._startAutoIncProc(true);
        } else {
            this._doDown();
            this._startAutoIncProc(false);
        }

        this._changed = true;
        delete this._shortcut;

        zk.Widget.mimicMouseDown_(this); //set zk.currentFocus
        zk(inp).focus(); //we have to set it here; otherwise, if it is in popup of
            //datebox, datebox's onblur will find zk.currentFocus is null

        // disable browser's text selection
        evt.stop();
    }

    /** @internal */
    _btnUp(evt: zk.Event): void {
        if (!this._buttonVisible || this._disabled || zk.dragging) return;

        if (zk.opera) zk(this.getInputNode()).focus();
            //unfortunately, in opera, it won't gain focus if we set in _btnDown

        this._onChanging();
        this._stopAutoIncProc();

        if (zk.webkit && this._lastPos)
            zk(this.getInputNode()).setSelectionRange(this._lastPos, this._lastPos);
    }

    /** @internal */
    _getPos(): number {
        return zk(this.getInputNode()).getSelectionRange()[0];
    }

    /** @internal */
    _doCheckPos(): void {
        this.lastPos = this._getPos();
    }

    /** @internal */
    _doUp(): void {
        this._changed = true;
        this.getTimeHandler().increase(this, 1);
        this._onChanging();
    }

    /** @internal */
    _doDown(): void {
        this._changed = true;
        this.getTimeHandler().increase(this, -1);
        this._onChanging();
    }

    /** @internal */
    _doBack(): void {
        this._changed = true;
        this.getTimeHandler().deleteTime(this, true);
    }

    /** @internal */
    _doDel(): void {
        this._changed = true;
        this.getTimeHandler().deleteTime(this, false);
    }

    /** @internal */
    _doType(val: number): void {
        this._changed = true;
        this.getTimeHandler().addTime(this, val);
    }

    getTimeHandler(): TimeHandler {
        var sr = zk(this.getInputNode()).getSelectionRange(),
            start = sr[0],
            end = sr[1],
            //don't use [0] as the end variable, it may have a bug when the format is aHH:mm:ss
            //when use UP/Down key to change the time
            hdler: TimeHandler | undefined;

        // Bug ZK-434
        for (var i = 0, f = this._fmthdler, l = f.length; i < l; i++) {
            if (!f[i].type) continue;
            if (f[i].index[0] <= start) {
                hdler = f[i];
                if (f[i].index[1] + 1 >= end)
                    return f[i];
            }
        }
        return hdler || this._fmthdler[0];
    }

    getNextTimeHandler(th: TimeHandler): TimeHandler {
        var f = this._fmthdler,
            index = f.$indexOf(th),
            lastHandler: TimeHandler;

        while ((lastHandler = f[++index])
                && (!lastHandler.type || lastHandler.type == AM_PM_FIELD));

        return lastHandler;
    }

    /** @internal */
    _startAutoIncProc(up: boolean): void {
        if (this.timerId)
            clearInterval(this.timerId);
        var self = this,
            fn: '_doUp' | '_doDown' = up ? '_doUp' : '_doDown';
        this.timerId = setInterval(function () {
            if (zk.webkit && self._lastPos)
                zk(self.getInputNode()).setSelectionRange(self._lastPos, self._lastPos);
            self[fn]();
        }, 300);
        jq(this.$n('btn-' + (up ? 'up' : 'down'))).addClass(this.$s('active'));
    }

    /** @internal */
    _stopAutoIncProc(): void {
        if (this.timerId)
            clearTimeout(this.timerId);
        // this.currentStep = this.defaultStep; // FIXME: both properties are not initialized
        this.timerId = undefined;
        jq('.' + this.$s('icon'), this.$n('btn')).removeClass(this.$s('active'));
    }

    /** @internal */
    override doFocus_(evt: zk.Event): void {
        super.doFocus_(evt);
        var inp = this.getInputNode()!,
            selrng = zk(inp).getSelectionRange();

        if (!inp.value)
            inp.value = Timebox._unformater ? '' : this.coerceToString_();

        this._doCheckPos();

        // Bug 2688620
        if (selrng[0] !== selrng[1]) {
            zk(inp).setSelectionRange(selrng[0], selrng[1]);
            this.lastPos = selrng[1];
        }

        zul.inp.RoundUtl.doFocus_(this);
    }

    /** @internal */
    override doBlur_(evt: zk.Event): void {
        // skip onchange, Bug 2936568
        if (!this._value && !this._changed && !Timebox._unformater)
            this.getInputNode()!.value = this._defRawVal = '';

        super.doBlur_(evt);

        zul.inp.RoundUtl.doBlur_(this);
    }

    /** @internal */
    override afterKeyDown_(evt: zk.Event, simulated?: boolean): boolean {
        if (!simulated && this._inplace)
            jq(this.$n_()).toggleClass(this.getInplaceCSS(), evt.keyCode == 13);

        return super.afterKeyDown_(evt, simulated);
    }

    /** @internal */
    override bind_(desktop?: zk.Desktop, skipper?: zk.Skipper, after?: CallableFunction[]): void {
        super.bind_(desktop, skipper, after);
        var btn: HTMLElement | undefined;

        if (zk.android && zk.chrome)
            this.domListen_(this.getInputNode()!, 'onBeforeInput', '_doBeforeInput');
        if (btn = this.$n('btn'))
            this.domListen_(btn, 'onZMouseDown', '_btnDown')
                .domListen_(btn, 'onZMouseUp', '_btnUp');
        zWatch.listen({onSize: this});
    }

    /** @internal */
    override unbind_(skipper?: zk.Skipper, after?: CallableFunction[], keepRod?: boolean): void {
        if (this.timerId) {
            clearTimeout(this.timerId);
            this.timerId = undefined;
        }
        zWatch.unlisten({onSize: this});
        var btn = this.$n('btn');
        if (btn) {
            this.domUnlisten_(btn, 'onZMouseDown', '_btnDown')
                .domUnlisten_(btn, 'onZMouseUp', '_btnUp');
        }
        if (zk.android && zk.chrome)
            this.domUnlisten_(this.getInputNode()!, 'onBeforeInput', '_doBeforeInput');
        this._changed = false;
        super.unbind_(skipper, after, keepRod);
    }

    /** @internal */
    getBtnUpIconClass_(): string {
        return 'z-icon-angle-up';
    }

    /** @internal */
    getBtnDownIconClass_(): string {
        return 'z-icon-angle-down';
    }

    /** @internal */
    static _updFormat(wgt: Timebox, fmt: string): void {
        var index: TimeHandler[] = [],
            APM = wgt._localizedSymbols ? wgt._localizedSymbols.APM! : zk.APM;
        for (var i = 0, l = fmt.length; i < l; i++) {
            var c = fmt.charAt(i);
            switch (c) {
            case 'a':
                var len = APM[0].length;
                index.push(new zul.inp.AMPMHandler([i, i + len - 1], AM_PM_FIELD, wgt));
                break;
            case 'K':
                var start = i,
                    end = fmt.charAt(i + 1) == 'K' ? ++i : i;
                index.push(new zul.inp.HourHandler2([start, end], HOUR3_FIELD, wgt));
                break;
            case 'h':
                var start = i,
                    end = fmt.charAt(i + 1) == 'h' ? ++i : i;
                index.push(new zul.inp.HourHandler([start, end], HOUR2_FIELD, wgt));
                break;
            case 'H':
                var start = i,
                    end = fmt.charAt(i + 1) == 'H' ? ++i : i;
                index.push(new zul.inp.HourInDayHandler([start, end], HOUR0_FIELD, wgt));
                break;
            case 'k':
                var start = i,
                    end = fmt.charAt(i + 1) == 'k' ? ++i : i;
                index.push(new zul.inp.HourInDayHandler2([start, end], HOUR1_FIELD, wgt));
                break;
            case 'm':
                var start = i,
                    end = fmt.charAt(i + 1) == 'm' ? ++i : i;
                index.push(new zul.inp.MinuteHandler([start, end], MINUTE_FIELD, wgt));
                break;
            case 's':
                var start = i,
                    end = fmt.charAt(i + 1) == 's' ? ++i : i;
                index.push(new zul.inp.SecondHandler([start, end], SECOND_FIELD, wgt));
                break;
            case 'z':
                index.push({index: [i, i], format: (function (text) {
                    return function () {
                        return text;
                    };
                })(wgt._timezoneAbbr)} as TimeHandler);
                break;
            default:
                var ary = '',
                    start = i,
                    end = i;

                while ((ary += c) && ++end < l) {
                    c = fmt.charAt(end);
                    if (LEGAL_CHARS.includes(c)) {
                        end--;
                        break;
                    }
                }
                index.push({index: [start, end], format: (function (text) {
                    return function () {
                        return text;
                    };
                })(ary)} as TimeHandler);
                i = end;
            }
        }
        for (var shift, i = 0, l = index.length; i < l; i++) {
            if (index[i].type == AM_PM_FIELD) {
                shift = index[i].index[1] - index[i].index[0];
                if (!shift) break; // no need to shift.
            } else if (shift) {
                index[i].index[0] += shift;
                index[i].index[1] += shift;
            }
        }
        wgt._fmthdler = index;
    }

    /** @internal */
    static _shouldObeyCount(nextType: number): boolean {
        switch (nextType) {
            case MINUTE_FIELD:
            case SECOND_FIELD:
            case HOUR0_FIELD:
            case HOUR1_FIELD:
            case HOUR2_FIELD:
            case HOUR3_FIELD:
                return true;
            default:
                return false;
        }
    }
}

@zk.WrapClass('zul.inp.TimeHandler')
export class TimeHandler extends zk.Object {
    maxsize = 59;
    minsize = 0;
    digits = 2;
    index: number[];
    type: number;
    wgt: Timebox;

    constructor(index: number[], type: number, wgt: Timebox) {
        super();
        this.index = index;
        this.type = type;
        if (index[0] == index[1])
            this.digits = 1;
        this.wgt = wgt;
    }

    format(date?: DateImpl): string {
        return '00';
    }

    unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl | boolean {
        return date;
    }

    increase(wgt: Timebox, up: number): void {
        var inp = wgt.getInputNode()!,
            start = this.index[0],
            end = this.index[1] + 1,
            val = inp.value,
            text: string | number = this.getText(val),
            singleLen = this.isSingleLength() !== false,
            ofs: number | undefined;

        text = zk.parseInt(singleLen ? text :
                text.replace(/ /g, '0')) + up;

        var max = this.maxsize + 1;
        if (text < this.minsize) {
            text = this.maxsize;
            ofs = 1;
        } else if (text >= max) {
            text = this.minsize;
            ofs = -1;
        } else if (singleLen)
            ofs = (up > 0) ?
                    (text == 10) ? 1 : 0 :
                    (text == 9) ? -1 : 0;

        if (text < 10 && !singleLen)
                text = '0' + text;

        inp.value = val.substring(0, start) + text + val.substring(end);

        if (singleLen && ofs) {
            this._doShift(wgt, ofs);
            end += ofs;
        }

        zk(inp).setSelectionRange(start, end);
    }

    deleteTime(wgt: Timebox, backspace: boolean): void {
        var inp = wgt.getInputNode()!,
            sel = zk(inp).getSelectionRange(),
            pos = sel[0],
            val = inp.value,
            maxLength = TimeHandler._getMaxLen(wgt);

        // clean over text
        if (val.length > maxLength) {
            val = inp.value = val.substr(0, maxLength);
            sel = [Math.min(sel[0], maxLength), Math.min(sel[1], maxLength)];
            pos = sel[0];
        }

        if (pos != sel[1]) {
            //select delete
            inp.value = val.substring(0, pos) + TimeHandler._cleanSelectionText(wgt, this)
                            + val.substring(sel[1]);
        } else {
            var fmthdler = wgt._fmthdler,
                index = fmthdler.$indexOf(this),
                ofs = backspace ? -1 : 1,
                ofs2 = backspace ? 0 : 1,
                ofs3 = backspace ? 1 : 0,
                hdler: TimeHandler,
                posOfs: number | boolean | undefined;
            if (pos == this.index[ofs2] + ofs2) {// on start or end
                //delete by sibling handler
                if (hdler = fmthdler[index + ofs * 2])
                    pos = hdler.index[ofs3] + ofs3 + ofs;
            } else {// delete self
                pos += ofs;
                hdler = this;
            }
            if (hdler) {
                posOfs = hdler.isSingleLength();
                inp.value = val.substring(0, (ofs3 += pos) - 1)
                    + (posOfs ? '' : ' ') + val.substring(ofs3);
                if (posOfs)
                    hdler._doShift(wgt, posOfs as number);
            }
            if (posOfs && !backspace) pos--;
        }
        zk(inp).setSelectionRange(pos, pos);
    }

    /** @internal */
    _addNextTime(wgt: Timebox, num: number): void {
        var inp = wgt.getInputNode(),
            index: number,
            NTH: TimeHandler;
        if (NTH = wgt.getNextTimeHandler(this)) {
            index = NTH.index[0];
            zk(inp).setSelectionRange(index,
                Math.max(index,
                    zk(inp).getSelectionRange()[1]));
            NTH.addTime(wgt, num);
        }
    }

    addTime(wgt: Timebox, num: number): void {
        var inp = wgt.getInputNode()!,
            sel = zk(inp).getSelectionRange(),
            val = inp.value,
            pos = sel[0],
            maxLength = TimeHandler._getMaxLen(wgt),
            posOfs = this.isSingleLength();

        // clean over text
        if (val.length > maxLength) {
            val = inp.value = val.substr(0, maxLength);
            sel = [Math.min(sel[0], maxLength), Math.min(sel[1], maxLength)];
            pos = sel[0];
        }

        if (pos == maxLength)
            return;

        // first number (hendle max bound)
        if (pos == this.index[0]) {
            var text = this.getText(val)
                        .substring((posOfs === 0) ? 0 : 2).trim(),
                i;
            if (!text.length) text = '0';

            if ((i = zk.parseInt(num + text)) > this.maxsize) {
                if (posOfs !== 0) {
                    val = inp.value = val.substring(0, pos) + (posOfs ? '0' : '00')
                        + val.substring(pos + 2);
                    if (!posOfs) pos++;
                    zk(inp).setSelectionRange(pos, Math.max(sel[1], pos));
                    sel = zk(inp).getSelectionRange();
                }
                if (posOfs)
                    this._doShift(wgt, posOfs as number);
            }
        } else if (pos == (this.index[1] + 1)) {//end of handler
            var i;
            if (posOfs !== false) {
                var text = this.getText(val);
                if ((i = zk.parseInt(text + num)) <= this.maxsize) {//allow add number
                    if (i && i < 10) // 1-9
                        pos--;
                    else if (i || posOfs) { // 0 or larger then 10, except zero and non-posOfs
                        val = inp.value = val.substring(0, (pos + (posOfs as number)))
                                + (posOfs ? '' : '0') + val.substring(pos);
                        if (i) // larger then 10
                            this._doShift(wgt, 1);
                        else { // 0
                            zk(inp).setSelectionRange(pos, Math.max(sel[1], pos));
                            if (posOfs)//2 digits zero
                                this._doShift(wgt, posOfs as number);
                        }
                    }
                }
            }

            if (!i || i > this.maxsize) {
                this._addNextTime(wgt, num);
                return;
            }
        }

        if (pos != sel[1]) {
            //select edit
            var s = TimeHandler._cleanSelectionText(wgt, this),
                ofs: number | undefined;
            //in middle position
            if (posOfs !== false && (ofs = pos - this.index[1]))
                this._doShift(wgt, ofs);

            inp.value = val.substring(0, pos++) + num
                + s.substring(ofs ? 0 : 1)
                + val.substring(sel[1]);
        } else {
            inp.value = val.substring(0, pos)
                + num + val.substring(++pos);
        }
        wgt.lastPos = pos;
        zk(inp).setSelectionRange(pos, pos);
    }

    getText(val: string, obeyCount?: boolean): string {
        var start = this.index[0],
            end = this.index[1] + 1;
        return obeyCount !== false ? val.substring(start, end) : val.substring(start);
    }

    /** @internal */
    _doShift(wgt: Timebox, shift: number): void {
        var f = wgt._fmthdler,
            index = f.$indexOf(this),
            NTH: TimeHandler;
        this.index[1] += shift;
        while (NTH = f[++index]) {
            NTH.index[0] += shift;
            NTH.index[1] += shift;
        }
    }

    isSingleLength(): boolean | number {
        return this.digits == 1 && (this.index[0] - this.index[1]);
    }

    parse(val: string, opt: {obeyCount?: boolean}): number {
        var text = this.getText(val, opt.obeyCount),
            parsed = /^\s*\d*/.exec(text)!,
            offset = parsed.length ? parsed[0].length - (this.index[1] - this.index[0] + 1) : 0;
        if (offset) {
            this._doShift(this.wgt, offset);
        }
        return zk.parseInt(text);
    }

    /** @internal */
    static _getMaxLen(wgt: Timebox): number {
        var val = wgt.getInputNode()!.value,
            len = 0,
            th: TimeHandler,
            lastTh: TimeHandler | undefined;
        for (var i = 0, f = wgt._fmthdler, l = f.length; i < l; i++) {
            th = f[i];
            if (i == l - 1) {
                len += th.format().length;
            } else
                len += (th.type ? th.getText(val) : th.format()).length;
            if (th.type) lastTh = th;
        }
        return (lastTh!.digits == 1) ? ++len : len;
    }

    /** @internal */
    static _cleanSelectionText(wgt: Timebox, startHandler: TimeHandler): string {
        var inp = wgt.getInputNode(),
            sel = zk(inp).getSelectionRange(),
            pos = sel[0],
            selEnd = sel[1],
            fmthdler = wgt._fmthdler,
            index = fmthdler.$indexOf(startHandler),
            text = '',
            hdler = startHandler,
            isFirst = true,
            prevStart: number | undefined,
            ofs,
            hStart: number,
            hEnd,
            posOfs: number | boolean;

        //restore separator
        do {
            hStart = hdler.index[0];
            hEnd = hdler.index[1] + 1;

            if (hdler.type && (posOfs = hdler.isSingleLength())) {
                //sync handler index
                hdler._doShift(wgt, posOfs as number);
                selEnd--;
            }

            //latest one
            if (hEnd >= selEnd && hdler.type) {
                ofs = selEnd - hStart;
                while (ofs-- > 0) //replace by space (after)
                    text += ' ';
                break;
            }

            if (hdler.type) {
                prevStart = isFirst ? pos : hStart;
                isFirst = false;
                continue;
            }
            ofs = hStart - prevStart!;
            while (ofs-- > 0) //replace by space (before)
                text += ' ';

            text += hdler.format();

        } while (hdler = fmthdler[++index]);
        return text;
    }
}

@zk.WrapClass('zul.inp.HourInDayHandler')
export class HourInDayHandler extends zul.inp.TimeHandler {
    override maxsize = 23;
    override minsize = 0;

    override format(date?: DateImpl): string {
        var singleLen = this.digits == 1;
        if (!date) return singleLen ? '0' : '00';
        else {
            var h: number | string = date.getHours();
            if (!singleLen && h < 10)
                h = '0' + h;
            return h.toString();
        }
    }

    override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl {
        date.setHours(this.parse(val, opt));
        return date;
    }
}

@zk.WrapClass('zul.inp.HourInDayHandler2')
export class HourInDayHandler2 extends zul.inp.TimeHandler {
    override maxsize = 24;
    override minsize = 1;

    override format(date?: DateImpl): string {
        if (!date) return '24';
        else {
            var h: number | string = date.getHours();
            if (h == 0)
                h = '24';
            else if (this.digits == 2 && h < 10)
                h = '0' + h;
            return h.toString();
        }
    }

    override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl {
        var hours = this.parse(val, opt);
        if (hours >= this.maxsize)
            hours = 0;
        date.setHours(hours);
        return date;
    }
}

@zk.WrapClass('zul.inp.HourHandler')
export class HourHandler extends zul.inp.TimeHandler {
    override maxsize = 12;
    override minsize = 1;

    override format(date?: DateImpl): string {
        if (!date) return '12';
        else {
            var h: number | string = date.getHours();
            h = (h % 12);
            if (h == 0)
                h = '12';
            else if (this.digits == 2 && h < 10)
                h = '0' + h;
            return h.toString();
        }
    }

    override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl {
        var hours = this.parse(val, opt);
        if (hours >= this.maxsize)
            hours = 0;
        date.setHours(opt.am ? hours : hours + 12);
        return date;
    }
}

@zk.WrapClass('zul.inp.HourHandler2')
export class HourHandler2 extends zul.inp.TimeHandler {
    override maxsize = 11;
    override minsize = 0;

    override format(date?: DateImpl): string {
        var singleLen = this.digits == 1;
        if (!date) return singleLen ? '0' : '00';
        else {
            var h: number | string = date.getHours();
            h = (h % 12);
            if (!singleLen && h < 10)
                h = '0' + h;
            return h.toString();
        }
    }

    override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl {
        var hours = this.parse(val, opt);
        date.setHours(opt.am ? hours : hours + 12);
        return date;
    }
}

@zk.WrapClass('zul.inp.MinuteHandler')
export class MinuteHandler extends zul.inp.TimeHandler {
    override format(date?: DateImpl): string {
        var singleLen = this.digits == 1;
        if (!date) return singleLen ? '0' : '00';
        else {
            var m: number | string = date.getMinutes();
            if (!singleLen && m < 10)
                m = '0' + m;
            return m.toString();
        }
    }

    override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl {
        date.setMinutes(this.parse(val, opt));
        return date;
    }
}

@zk.WrapClass('zul.inp.SecondHandler')
export class SecondHandler extends zul.inp.TimeHandler {
    override format(date?: DateImpl): string {
        var singleLen = this.digits == 1;
        if (!date) return singleLen ? '0' : '00';
        else {
            var s: number | string = date.getSeconds();
            if (!singleLen && s < 10)
                s = '0' + s;
            return s.toString();
        }
    }

    override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): DateImpl {
        date.setSeconds(this.parse(val, opt));
        return date;
    }
}

@zk.WrapClass('zul.inp.AMPMHandler')
export class AMPMHandler extends zul.inp.TimeHandler {
    override format(date?: DateImpl): string {
        var APM = this.wgt._localizedSymbols ? this.wgt._localizedSymbols.APM! : zk.APM;
        if (!date)
            return APM[0];
        var h = date.getHours();
        return APM[h < 12 ? 0 : 1];
    }

    override unformat(date: DateImpl, val: string, opt: Record<string, unknown>): boolean {
        var text = this.getText(val).trim(),
            APM = this.wgt._localizedSymbols ? this.wgt._localizedSymbols.APM! : zk.APM;
        return (text.length == APM[0].length) ?
            APM[0] == text : true;
    }

    override addTime(wgt: Timebox, num: number): void {
        var inp = wgt.getInputNode()!,
            start = this.index[0],
            end = this.index[1] + 1,
            val = inp.value,
            text = val.substring(start, end),
            APM = wgt._localizedSymbols ? wgt._localizedSymbols.APM! : zk.APM;
        //restore A/PM text
        if (text != APM[0] && text != APM[1]) {
            text = APM[0];
            inp.value = val.substring(0, start) + text + val.substring(end);
        }
        this._addNextTime(wgt, num);
    }

    // Bug ZK-434, we have to delete a sets of "AM/PM", rather than a single word "A/P/M"
    override deleteTime(wgt: Timebox, backspace: boolean): void {
        var inp = wgt.getInputNode()!,
            sel = zk(inp).getSelectionRange(),
            pos = sel[0],
            pos1 = sel[1],
            start = this.index[0],
            end = this.index[1] + 1,
            val = inp.value;
        if (pos1 - pos > end - start)
            return super.deleteTime(wgt, backspace);

        var t = [''];
        for (var i = end - start; i > 0; i--)
            t.push(' ');

        inp.value = val.substring(0, start) + t.join('') + val.substring(end);
        zk(inp).setSelectionRange(start, start);
    }

    override increase(wgt: Timebox, up: number): void {
        var inp = wgt.getInputNode()!,
            start = this.index[0],
            end = this.index[1] + 1,
            val = inp.value,
            text = val.substring(start, end),
            APM = wgt._localizedSymbols ? wgt._localizedSymbols.APM! : zk.APM;

        text = APM[0] == text ? APM[1] : APM[0];
        inp.value = val.substring(0, start) + text + val.substring(end);
        zk(inp).setSelectionRange(start, end);
    }

    override parse(val: string, opt: {obeyCount?: boolean}): number {
        return zk.parseInt(this.getText(val).trim());
    }
}