widget/time/timepicker.js

import $ from 'jquery';
import event from '../../js/event';
/*!
 * Timepicker
 *
 * Forked from https://github.com/jdewit/bootstrap-timepicker:
 *
 * Copyright 2013 Joris de Wit and timepicker contributors
 *
 * Contributors https://github.com/jdewit/bootstrap-timepicker/graphs/contributors
 * Contributors https://github.com/enketo/timepicker-basic/graphs/contributors
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
( ( ( $, window, document ) => {
    // TIMEPICKER PUBLIC CLASS DEFINITION
    const Timepicker = function( element, options ) {
        this.widget = '';
        this.$element = $( element );
        this.defaultTime = options.defaultTime;
        this.disableFocus = options.disableFocus;
        this.disableMousewheel = options.disableMousewheel;
        this.isOpen = options.isOpen;
        this.minuteStep = options.minuteStep;
        this.orientation = options.orientation;
        this.secondStep = options.secondStep;
        this.snapToStep = options.snapToStep;
        this.showInputs = options.showInputs;
        this.showMeridian = options.showMeridian;
        this.meridianNotation = options.meridianNotation;
        this.showSeconds = options.showSeconds;
        this.template = options.template;
        this.appendWidgetTo = options.appendWidgetTo;
        this.showWidgetOnAddonClick = options.showWidgetOnAddonClick;
        this.icons = options.icons;
        this.maxHours = options.maxHours;
        this.explicitMode = options.explicitMode; // If true 123 = 1:23, 12345 = 1:23:45, else invalid.

        this.handleDocumentClick = e => {
            const self = e.data.scope;
            // This condition was inspired by datepicker.
            // The element the timepicker is invoked on is the input but it has a sibling for addon/button.
            if ( !( self.$element.parent().find( e.target ).length ||
                    self.$widget.is( e.target ) ||
                    self.$widget.find( e.target ).length ) ) {
                self.hideWidget();
            }
        };

        this._init();
    };

    Timepicker.prototype = {

        constructor: Timepicker,
        _init() {
            const self = this;

            if ( this.showWidgetOnAddonClick && ( this.$element.parent().hasClass( 'input-group' ) && this.$element.parent().hasClass( 'timepicker' ) ) ) {
                this.$element.parent( '.input-group.timepicker' ).find( '.input-group-addon' ).on( {
                    'click.timepicker': $.proxy( this.showWidget, this )
                } );
                this.$element.on( {
                    'focus.timepicker': $.proxy( this.highlightUnit, this ),
                    'click.timepicker': $.proxy( this.highlightUnit, this ),
                    'keydown.timepicker': $.proxy( this.elementKeydown, this ),
                    'blur.timepicker': $.proxy( this.blurElement, this ),
                    'mousewheel.timepicker DOMMouseScroll.timepicker': $.proxy( this.mousewheel, this )
                } );
            } else {
                if ( this.template ) {
                    this.$element.on( {
                        'focus.timepicker': $.proxy( this.showWidget, this ),
                        'click.timepicker': $.proxy( this.showWidget, this ),
                        'blur.timepicker': $.proxy( this.blurElement, this ),
                        'mousewheel.timepicker DOMMouseScroll.timepicker': $.proxy( this.mousewheel, this )
                    } );
                } else {
                    this.$element.on( {
                        'focus.timepicker': $.proxy( this.highlightUnit, this ),
                        'click.timepicker': $.proxy( this.highlightUnit, this ),
                        'keydown.timepicker': $.proxy( this.elementKeydown, this ),
                        'blur.timepicker': $.proxy( this.blurElement, this ),
                        'mousewheel.timepicker DOMMouseScroll.timepicker': $.proxy( this.mousewheel, this )
                    } );
                }
            }

            if ( this.template !== false ) {
                this.$widget = $( this.getTemplate() ).on( 'click', $.proxy( this.widgetClick, this ) );
            } else {
                this.$widget = false;
            }

            if ( this.showInputs && this.$widget !== false ) {
                this.$widget.find( 'input' ).each( function() {
                    $( this ).on( {
                        'click.timepicker': function() {
                            $( this ).select();
                        },
                        'keydown.timepicker': $.proxy( self.widgetKeydown, self ),
                        'keyup.timepicker': $.proxy( self.widgetKeyup, self )
                    } );
                } );
            }

            this.setDefaultTime( this.defaultTime );
        },

        blurElement() {
            this.highlightedUnit = null;
            this.updateFromElementVal();
        },

        clear() {
            this.hour = '';
            this.minute = '';
            this.second = '';
            this.meridian = '';

            this.$element.val( '' );
        },

        decrementHour() {
            // If value is empty, make sure that first shown value is current hour.
            if ( this.hour === '' ) {
                this.hour = this.getCurrentHour();
                this.incrementHour();
            }

            if ( this.showMeridian ) {
                if ( this.hour === 1 ) {
                    this.hour = 12;
                } else if ( this.hour === 12 ) {
                    this.hour--;

                    return this.toggleMeridian();
                } else if ( this.hour === 0 ) {
                    this.hour = 11;

                    return this.toggleMeridian();
                } else {
                    this.hour--;
                }
            } else {
                if ( this.hour <= 0 ) {
                    this.hour = this.maxHours - 1;
                } else {
                    this.hour--;
                }
            }
        },

        decrementMinute( step ) {
            let newVal;

            // If value is empty, make sure that first shown value is current minutes.
            if ( this.minute === '' ) {
                this.minute = this.getCurrentMinute();
                this.incrementMinute();
            }

            if ( step ) {
                newVal = this.minute - step;
            } else {
                newVal = this.minute - this.minuteStep;
            }

            if ( newVal < 0 ) {
                this.decrementHour();
                this.minute = newVal + 60;
            } else {
                this.minute = newVal;
            }
        },

        decrementSecond() {
            const newVal = this.second - this.secondStep;

            if ( newVal < 0 ) {
                this.decrementMinute( true );
                this.second = newVal + 60;
            } else {
                this.second = newVal;
            }
        },

        elementKeydown( e ) {
            switch ( e.which ) {
                case 9: //tab
                    if ( e.shiftKey ) {
                        if ( this.highlightedUnit === 'hour' ) {
                            this.hideWidget();
                            break;
                        }
                        this.highlightPrevUnit();
                    } else if ( ( this.showMeridian && this.highlightedUnit === 'meridian' ) || ( this.showSeconds && this.highlightedUnit === 'second' ) || ( !this.showMeridian && !this.showSeconds && this.highlightedUnit === 'minute' ) ) {
                        this.hideWidget();
                        break;
                    } else {
                        this.highlightNextUnit();
                    }
                    e.preventDefault();
                    this.updateFromElementVal();
                    break;
                case 27: // escape
                    this.updateFromElementVal();
                    break;
                case 37: // left arrow
                    e.preventDefault();
                    this.highlightPrevUnit();
                    this.updateFromElementVal();
                    break;
                case 38: // up arrow
                    e.preventDefault();
                    switch ( this.highlightedUnit ) {
                        case 'hour':
                            this.incrementHour();
                            this.highlightHour();
                            break;
                        case 'minute':
                            this.incrementMinute();
                            this.highlightMinute();
                            break;
                        case 'second':
                            this.incrementSecond();
                            this.highlightSecond();
                            break;
                        case 'meridian':
                            this.toggleMeridian();
                            this.highlightMeridian();
                            break;
                    }
                    this.update();
                    break;
                case 39: // right arrow
                    e.preventDefault();
                    this.highlightNextUnit();
                    this.updateFromElementVal();
                    break;
                case 40: // down arrow
                    e.preventDefault();
                    switch ( this.highlightedUnit ) {
                        case 'hour':
                            this.decrementHour();
                            this.highlightHour();
                            break;
                        case 'minute':
                            this.decrementMinute();
                            this.highlightMinute();
                            break;
                        case 'second':
                            this.decrementSecond();
                            this.highlightSecond();
                            break;
                        case 'meridian':
                            this.toggleMeridian();
                            this.highlightMeridian();
                            break;
                    }

                    this.update();
                    break;
            }
        },

        getCursorPosition() {
            const input = this.$element.get( 0 );

            if ( 'selectionStart' in input ) { // Standard-compliant browsers

                return input.selectionStart;
            } else if ( document.selection ) { // IE fix
                input.focus();
                const sel = document.selection.createRange(),
                    selLen = document.selection.createRange().text.length;

                sel.moveStart( 'character', -input.value.length );

                return sel.text.length - selLen;
            }
        },

        getMeridianLength() {
            const el = document.createElement( 'span' );
            el.textContent = this.meridianNotation.am;
            el.style.position = 'absolute';
            document.querySelector( 'body' ).appendChild( el );
            const amLength = el.scrollWidth;
            el.textContent = this.meridianNotation.pm;
            const pmLength = el.scrollWidth;
            el.remove();

            return amLength > pmLength ? amLength : pmLength;
        },

        getTemplate() {
            let template, hourTemplate, minuteTemplate, secondTemplate, meridianTemplate, templateContent;

            if ( this.showInputs ) {
                const width = this.getMeridianLength() > 26 ? `style="width: ${this.getMeridianLength() + 24}px"` : '';
                hourTemplate = `<input type="text" class="timepicker-hour" ${width}/>`;
                minuteTemplate = `<input type="text" class="timepicker-minute" ${width}/>`;
                secondTemplate = `<input type="text" class="timepicker-second" ${width}/>`;
                meridianTemplate = `<input type="text" class="timepicker-meridian"${width}/>`;
            } else {
                hourTemplate = '<span class="timepicker-hour"></span>';
                minuteTemplate = '<span class="timepicker-minute"></span>';
                secondTemplate = '<span class="timepicker-second"></span>';
                meridianTemplate = '<span class="timepicker-meridian"></span>';
            }

            templateContent = `<table><tr><td><a href="#" data-action="incrementHour"><span class="${this.icons.up}"></span></a></td><td class="separator">&nbsp;</td><td><a href="#" data-action="incrementMinute"><span class="${this.icons.up}"></span></a></td>${this.showSeconds ?
                `<td class="separator">&nbsp;</td><td><a href="#" data-action="incrementSecond"><span class="${this.icons.up}"></span></a></td>` : ''}${this.showMeridian ?
                `<td class="separator">&nbsp;</td><td class="meridian-column"><a href="#" data-action="toggleMeridian"><span class="${this.icons.up}"></span></a></td>` : ''}</tr><tr><td>${hourTemplate}</td> <td class="separator">:</td><td>${minuteTemplate}</td> ${this.showSeconds ?
                `<td class="separator">:</td><td>${secondTemplate}</td>` : ''}${this.showMeridian ?
                `<td class="separator">&nbsp;</td><td>${meridianTemplate}</td>` : ''}</tr><tr><td><a href="#" data-action="decrementHour"><span class="${this.icons.down}"></span></a></td><td class="separator"></td><td><a href="#" data-action="decrementMinute"><span class="${this.icons.down}"></span></a></td>${this.showSeconds ?
                `<td class="separator">&nbsp;</td><td><a href="#" data-action="decrementSecond"><span class="${this.icons.down}"></span></a></td>` : ''}${this.showMeridian ?
                `<td class="separator">&nbsp;</td><td><a href="#" data-action="toggleMeridian"><span class="${this.icons.down}"></span></a></td>` : ''}</tr></table>`;

            switch ( this.template ) {
                case 'dropdown':
                    template = `<div class="timepicker-widget dropdown-menu">${templateContent}</div>`;
                    break;
            }

            return template;
        },

        getTime() {
            if ( this.hour === '' ) {
                return '';
            }

            //return this.hour +                                                           ':' + ( this.minute.toString().length === 1 ? '0' + this.minute : this.minute ) + ( this.showSeconds ? ':' + ( this.second.toString().length === 1 ? '0' + this.second : this.second ) : '' ) + ( this.showMeridian ? ' ' + this.meridian : '' );
            return `${this.hour.toString().length === 1 ? `0${this.hour}` : this.hour}:${this.minute.toString().length === 1 ? `0${this.minute}` : this.minute}${this.showSeconds ? `:${this.second.toString().length === 1 ? `0${this.second}` : this.second}` : ''}${this.showMeridian ? ` ${this.meridian}` : ''}`;
        },

        hideWidget() {
            if ( this.isOpen === false ) {
                return;
            }

            this.$element.trigger( {
                'type': 'hide.timepicker',
                'time': {
                    'value': this.getTime(),
                    'hours': this.hour,
                    'minutes': this.minute,
                    'seconds': this.second,
                    'meridian': this.meridian
                }
            } );

            this.$widget.removeClass( 'open' );

            $( document ).off( 'mousedown.timepicker, touchend.timepicker', this.handleDocumentClick );

            this.isOpen = false;
            // show/hide approach taken by datepicker
            this.$widget.detach();
        },

        highlightUnit() {
            this.position = this.getCursorPosition();
            if ( this.position >= 0 && this.position <= 2 ) {
                this.highlightHour();
            } else if ( this.position >= 3 && this.position <= 5 ) {
                this.highlightMinute();
            } else if ( this.position >= 6 && this.position <= 8 ) {
                if ( this.showSeconds ) {
                    this.highlightSecond();
                } else {
                    this.highlightMeridian();
                }
            } else if ( this.position >= 9 && this.position <= 11 ) {
                this.highlightMeridian();
            }
        },

        highlightNextUnit() {
            switch ( this.highlightedUnit ) {
                case 'hour':
                    this.highlightMinute();
                    break;
                case 'minute':
                    if ( this.showSeconds ) {
                        this.highlightSecond();
                    } else if ( this.showMeridian ) {
                        this.highlightMeridian();
                    } else {
                        this.highlightHour();
                    }
                    break;
                case 'second':
                    if ( this.showMeridian ) {
                        this.highlightMeridian();
                    } else {
                        this.highlightHour();
                    }
                    break;
                case 'meridian':
                    this.highlightHour();
                    break;
            }
        },

        highlightPrevUnit() {
            switch ( this.highlightedUnit ) {
                case 'hour':
                    if ( this.showMeridian ) {
                        this.highlightMeridian();
                    } else if ( this.showSeconds ) {
                        this.highlightSecond();
                    } else {
                        this.highlightMinute();
                    }
                    break;
                case 'minute':
                    this.highlightHour();
                    break;
                case 'second':
                    this.highlightMinute();
                    break;
                case 'meridian':
                    if ( this.showSeconds ) {
                        this.highlightSecond();
                    } else {
                        this.highlightMinute();
                    }
                    break;
            }
        },

        highlightHour() {
            const $element = this.$element.get( 0 ),
                self = this;

            this.highlightedUnit = 'hour';

            if ( $element.setSelectionRange ) {
                setTimeout( () => {
                    if ( self.hour < 10 ) {
                        $element.setSelectionRange( 0, 1 );
                    } else {
                        $element.setSelectionRange( 0, 2 );
                    }
                }, 0 );
            }
        },

        highlightMinute() {
            const $element = this.$element.get( 0 ),
                self = this;

            this.highlightedUnit = 'minute';

            if ( $element.setSelectionRange ) {
                setTimeout( () => {
                    if ( self.hour < 10 ) {
                        $element.setSelectionRange( 2, 4 );
                    } else {
                        $element.setSelectionRange( 3, 5 );
                    }
                }, 0 );
            }
        },

        highlightSecond() {
            const $element = this.$element.get( 0 ),
                self = this;

            this.highlightedUnit = 'second';

            if ( $element.setSelectionRange ) {
                setTimeout( () => {
                    if ( self.hour < 10 ) {
                        $element.setSelectionRange( 5, 7 );
                    } else {
                        $element.setSelectionRange( 6, 8 );
                    }
                }, 0 );
            }
        },

        highlightMeridian() {
            const $element = this.$element.get( 0 ),
                self = this;

            this.highlightedUnit = 'meridian';

            if ( $element.setSelectionRange ) {
                if ( this.showSeconds ) {
                    setTimeout( () => {
                        if ( self.hour < 10 ) {
                            $element.setSelectionRange( 8, 10 );
                        } else {
                            $element.setSelectionRange( 9, 11 );
                        }
                    }, 0 );
                } else {
                    setTimeout( () => {
                        if ( self.hour < 10 ) {
                            $element.setSelectionRange( 5, 7 );
                        } else {
                            $element.setSelectionRange( 6, 8 );
                        }
                    }, 0 );
                }
            }
        },

        getCurrentHour() {
            const h24 = new Date().getHours();

            return ( this.showMeridian ) ? h24 % 12 : h24;
        },

        getCurrentMinute() {
            return new Date().getMinutes();
        },

        incrementHour() {
            // If value is empty, make sure that first shown value is current hour.
            if ( this.hour === '' ) {
                this.hour = this.getCurrentHour();
                this.decrementHour();
            }

            // if this.hour is empty string
            if ( this.showMeridian ) {
                if ( this.hour === 11 ) {
                    this.hour++;

                    return this.toggleMeridian();
                } else if ( this.hour === 12 ) {
                    this.hour = 0;
                }
            }
            if ( this.hour === this.maxHours - 1 ) {
                this.hour = 0;

                return;
            }
            this.hour++;
        },

        incrementMinute( step ) {
            let newVal;

            // If value is empty, make sure that first shown value is current minutes.
            if ( this.minute === '' ) {
                this.minute = this.getCurrentMinute();
                this.decrementMinute();
            }

            if ( step ) {
                newVal = this.minute + step;
            } else {
                newVal = this.minute + this.minuteStep - ( this.minute % this.minuteStep );
            }

            if ( newVal > 59 ) {
                this.incrementHour();
                this.minute = newVal - 60;
            } else {
                this.minute = newVal;
            }
        },

        incrementSecond() {
            const newVal = this.second + this.secondStep - ( this.second % this.secondStep );

            if ( newVal > 59 ) {
                this.incrementMinute( true );
                this.second = newVal - 60;
            } else {
                this.second = newVal;
            }
        },

        mousewheel( e ) {
            if ( this.disableMousewheel ) {
                return;
            }

            e.preventDefault();
            e.stopPropagation();

            const delta = e.originalEvent.wheelDelta || -e.originalEvent.detail;
            let scrollTo = null;

            if ( e.type === 'mousewheel' ) {
                scrollTo = ( e.originalEvent.wheelDelta * -1 );
            } else if ( e.type === 'DOMMouseScroll' ) {
                scrollTo = 40 * e.originalEvent.detail;
            }

            if ( scrollTo ) {
                e.preventDefault();
                $( this ).scrollTop( scrollTo + $( this ).scrollTop() );
            }

            switch ( this.highlightedUnit ) {
                case 'minute':
                    if ( delta > 0 ) {
                        this.incrementMinute();
                    } else {
                        this.decrementMinute();
                    }
                    this.highlightMinute();
                    break;
                case 'second':
                    if ( delta > 0 ) {
                        this.incrementSecond();
                    } else {
                        this.decrementSecond();
                    }
                    this.highlightSecond();
                    break;
                case 'meridian':
                    this.toggleMeridian();
                    this.highlightMeridian();
                    break;
                default:
                    if ( delta > 0 ) {
                        this.incrementHour();
                    } else {
                        this.decrementHour();
                    }
                    this.highlightHour();
                    break;
            }

            return false;
        },

        /**
         * Given a segment value like 43, will round and snap the segment
         * to the nearest "step", like 45 if step is 15. Segment will
         * "overflow" to 0 if it's larger than 59 or would otherwise
         * round up to 60.
         *
         * @param {number} segment - The segment value
         * @param {number} step - The step
         */
        changeToNearestStep( segment, step ) {
            if ( segment % step === 0 ) {
                return segment;
            }
            if ( Math.round( ( segment % step ) / step ) ) {
                return ( segment + ( step - segment % step ) ) % 60;
            } else {
                return segment - segment % step;
            }
        },

        // This method was adapted from datepicker.
        place() {
            if ( this.isInline ) {
                return;
            }
            const widgetWidth = this.$widget.outerWidth(),
                widgetHeight = this.$widget.outerHeight(),
                visualPadding = 10,
                windowWidth =
                $( window ).width(),
                windowHeight = $( window ).height(),
                scrollTop = $( window ).scrollTop();

            const zIndex = parseInt( this.$element.parents().filter( function() {
                return $( this ).css( 'z-index' ) !== 'auto';
            } ).first().css( 'z-index' ), 10 ) + 10;
            const offset = this.component ? this.component.parent().offset() : this.$element.offset();
            const height = this.component ? this.component.outerHeight( true ) : this.$element.outerHeight( false );
            const width = this.component ? this.component.outerWidth( true ) : this.$element.outerWidth( false );
            let left = offset.left,
                top = offset.top;

            this.$widget.removeClass( 'timepicker-orient-top timepicker-orient-bottom timepicker-orient-right timepicker-orient-left' );

            if ( this.orientation.x !== 'auto' ) {
                this.$widget.addClass( `timepicker-orient-${this.orientation.x}` );
                if ( this.orientation.x === 'right' ) {
                    left -= widgetWidth - width;
                }
            } else {
                // auto x orientation is best-placement: if it crosses a window edge, fudge it sideways
                // Default to left
                this.$widget.addClass( 'timepicker-orient-left' );
                if ( offset.left < 0 ) {
                    left -= offset.left - visualPadding;
                } else if ( offset.left + widgetWidth > windowWidth ) {
                    left = windowWidth - widgetWidth - visualPadding;
                }
            }
            // auto y orientation is best-situation: top or bottom, no fudging, decision based on which shows more of the widget
            let yorient = this.orientation.y,
                topOverflow, bottomOverflow;
            if ( yorient === 'auto' ) {
                topOverflow = -scrollTop + offset.top - widgetHeight;
                bottomOverflow = scrollTop + windowHeight - ( offset.top + height + widgetHeight );
                if ( Math.max( topOverflow, bottomOverflow ) === bottomOverflow ) {
                    yorient = 'top';
                } else {
                    yorient = 'bottom';
                }
            }
            this.$widget.addClass( `timepicker-orient-${yorient}` );
            if ( yorient === 'top' ) {
                top += height;
            } else {
                top -= widgetHeight + parseInt( this.$widget.css( 'padding-top' ), 10 );
            }

            this.$widget.css( {
                top,
                left,
                zIndex
            } );
        },

        remove() {
            $( 'document' ).off( '.timepicker' );
            if ( this.$widget ) {
                this.$widget.remove();
            }
            delete this.$element.data().timepicker;
        },

        setDefaultTime( defaultTime ) {
            if ( !this.$element.val() ) {
                if ( defaultTime === 'current' ) {
                    const dTime = new Date();
                    let hours = dTime.getHours();
                    let minutes = dTime.getMinutes();
                    let seconds = dTime.getSeconds();
                    let meridian = this.meridianNotation.am;

                    if ( seconds !== 0 ) {
                        seconds = Math.ceil( dTime.getSeconds() / this.secondStep ) * this.secondStep;
                        if ( seconds === 60 ) {
                            minutes += 1;
                            seconds = 0;
                        }
                    }

                    if ( minutes !== 0 ) {
                        minutes = Math.ceil( dTime.getMinutes() / this.minuteStep ) * this.minuteStep;
                        if ( minutes === 60 ) {
                            hours += 1;
                            minutes = 0;
                        }
                    }

                    if ( this.showMeridian ) {
                        if ( hours === 0 ) {
                            hours = 12;
                        } else if ( hours >= 12 ) {
                            if ( hours > 12 ) {
                                hours = hours - 12;
                            }
                            meridian = this.meridianNotation.pm;
                        } else {
                            meridian = this.meridianNotation.am;
                        }
                    }

                    this.hour = hours;
                    this.minute = minutes;
                    this.second = seconds;
                    this.meridian = meridian;

                    this.update();
                } else if ( defaultTime === false ) {
                    this.hour = 0;
                    this.minute = 0;
                    this.second = 0;
                    this.meridian = this.meridianNotation.am;
                } else {
                    this.setTime( defaultTime );
                }
            } else {
                this.updateFromElementVal();
            }
        },

        setTime( time, ignoreWidget ) {
            if ( !time ) {
                this.clear();

                return;
            }

            let timeMode, timeArray, hour, minute, second, meridian;

            if ( typeof time === 'object' && time.getMonth ) {
                // this is a date object
                hour = time.getHours();
                minute = time.getMinutes();
                second = time.getSeconds();

                if ( this.showMeridian ) {
                    meridian = this.meridianNotation.am;
                    if ( hour > 12 ) {
                        meridian = this.meridianNotation.pm;
                        hour = hour % 12;
                    }

                    if ( hour === 12 ) {
                        meridian = this.meridianNotation.pm;
                    }
                }
            } else {
                let am = this.showMeridian ? this.meridianNotation.am : 'am';
                let pm = this.showMeridian ? this.meridianNotation.pm : 'pm';
                timeMode = ( ( new RegExp( am, 'i' ) ).test( time ) ? 1 : 0 ) + ( ( new RegExp( pm, 'i' ) ).test( time ) ? 2 : 0 ); // 0 = none, 1 = AM, 2 = PM, 3 = BOTH.
                if ( timeMode > 2 ) { // If both are present, fail.
                    this.clear();

                    return;
                }

                timeArray = time.replace( /[^0-9:]/g, '' ).split( ':' );

                hour = timeArray[ 0 ] ? timeArray[ 0 ].toString() : timeArray.toString();

                if ( this.explicitMode && hour.length > 2 && ( hour.length % 2 ) !== 0 ) {
                    this.clear();

                    return;
                }

                minute = timeArray[ 1 ] ? timeArray[ 1 ].toString() : '';
                second = timeArray[ 2 ] ? timeArray[ 2 ].toString() : '';

                // adaptive time parsing
                if ( hour.length > 4 ) {
                    second = hour.slice( -2 );
                    hour = hour.slice( 0, -2 );
                }

                if ( hour.length > 2 ) {
                    minute = hour.slice( -2 );
                    hour = hour.slice( 0, -2 );
                }

                if ( minute.length > 2 ) {
                    second = minute.slice( -2 );
                    minute = minute.slice( 0, -2 );
                }

                hour = parseInt( hour, 10 );
                minute = parseInt( minute, 10 );
                second = parseInt( second, 10 );

                if ( isNaN( hour ) ) {
                    hour = 0;
                }
                if ( isNaN( minute ) ) {
                    minute = 0;
                }
                if ( isNaN( second ) ) {
                    second = 0;
                }

                // Adjust the time based upon unit boundary.
                // NOTE: Negatives will never occur due to time.replace() above.
                if ( second > 59 ) {
                    second = 59;
                }

                if ( minute > 59 ) {
                    minute = 59;
                }

                if ( hour >= this.maxHours ) {
                    // No day/date handling.
                    hour = this.maxHours - 1;
                }

                if ( this.showMeridian ) {
                    if ( hour >= 12 ) {
                        // Force PM.
                        if ( !timeMode ) {
                            timeMode = 2;
                        }
                        hour -= 12;
                    }
                    if ( !timeMode ) {
                        timeMode = 1;
                    }
                    if ( hour === 0 ) {
                        hour = 12; // AM or PM, reset to 12.  0 AM = 12 AM.  0 PM = 12 PM, etc.
                    }
                    meridian = timeMode === 1 ? this.meridianNotation.am : this.meridianNotation.pm;
                } else if ( hour < 12 && timeMode === 2 ) {
                    hour += 12;
                } else {
                    if ( hour >= this.maxHours ) {
                        hour = this.maxHours - 1;
                    } else if ( ( hour < 0 ) || ( hour === 12 && timeMode === 1 ) ) {
                        hour = 0;
                    }
                }
            }

            this.hour = hour;
            if ( this.snapToStep ) {
                this.minute = this.changeToNearestStep( minute, this.minuteStep );
                this.second = this.changeToNearestStep( second, this.secondStep );
            } else {
                this.minute = minute;
                this.second = second;
            }
            this.meridian = meridian;

            this.update( ignoreWidget );
        },

        showWidget() {
            if ( this.isOpen ) {
                return;
            }

            if ( this.$element.is( ':disabled' ) ) {
                return;
            }

            // make sure the widget is in sync with input
            this.setTime( this.$element.val() );
            this.updateWidget();

            // show/hide approach taken by datepicker
            this.$widget.appendTo( this.appendWidgetTo );
            $( document ).on( 'mousedown.timepicker, touchend.timepicker', {
                scope: this
            }, this.handleDocumentClick );

            this.$element.trigger( {
                'type': 'show.timepicker',
                'time': {
                    'value': this.getTime(),
                    'hours': this.hour,
                    'minutes': this.minute,
                    'seconds': this.second,
                    'meridian': this.meridian
                }
            } );

            this.place();
            if ( this.disableFocus ) {
                this.$element.blur();
            }

            if ( this.hour === '' ) {
                if ( this.defaultTime ) {
                    this.setDefaultTime( this.defaultTime );
                }
            }

            if ( this.isOpen === false ) {
                this.$widget.addClass( 'open' );
            }

            this.isOpen = true;
        },

        toggleMeridian() {
            this.meridian = this.meridian === this.meridianNotation.am ? this.meridianNotation.pm : this.meridianNotation.am;
        },

        update( ignoreWidget ) {
            this.updateElement();
            if ( !ignoreWidget ) {
                this.updateWidget();
            }

            this.$element.trigger( {
                'type': 'changeTime.timepicker',
                'time': {
                    'value': this.getTime(),
                    'hours': this.hour,
                    'minutes': this.minute,
                    'seconds': this.second,
                    'meridian': this.meridian
                }
            } );
        },

        updateElement() {
            this.$element.val( this.getTime() );
            this.$element[ 0 ].dispatchEvent( event.Change() );
        },

        updateFromElementVal() {
            this.setTime( this.$element.val() );
        },

        updateWidget() {
            if ( this.$widget === false ) {
                return;
            }

            const hour = this.hour,
                minute = this.minute.toString().length === 1 ? `0${this.minute}` : this.minute,
                second = this.second.toString().length === 1 ? `0${this.second}` : this.second;

            if ( this.showInputs ) {
                this.$widget.find( 'input.timepicker-hour' ).val( hour );
                this.$widget.find( 'input.timepicker-minute' ).val( minute );

                if ( this.showSeconds ) {
                    this.$widget.find( 'input.timepicker-second' ).val( second );
                }
                if ( this.showMeridian ) {
                    this.$widget.find( 'input.timepicker-meridian' ).val( this.meridian );
                }
            } else {
                this.$widget.find( 'span.timepicker-hour' ).text( hour );
                this.$widget.find( 'span.timepicker-minute' ).text( minute );

                if ( this.showSeconds ) {
                    this.$widget.find( 'span.timepicker-second' ).text( second );
                }
                if ( this.showMeridian ) {
                    this.$widget.find( 'span.timepicker-meridian' ).text( this.meridian );
                }
            }
        },

        updateFromWidgetInputs() {
            if ( this.$widget === false ) {
                return;
            }

            const t = `${this.$widget.find( 'input.timepicker-hour' ).val()}:${this.$widget.find( 'input.timepicker-minute' ).val()}${this.showSeconds ? `:${this.$widget.find( 'input.timepicker-second' ).val()}` : ''}${this.showMeridian ? this.$widget.find( 'input.timepicker-meridian' ).val() : ''}`;

            this.setTime( t, true );
        },

        widgetClick( e ) {
            e.stopPropagation();
            e.preventDefault();

            const $input = $( e.target ),
                action = $input.closest( 'a' ).data( 'action' );

            if ( action ) {
                this[ action ]();
            }
            this.update();

            if ( $input.is( 'input' ) ) {
                $input.get( 0 ).setSelectionRange( 0, 2 );
            }
        },

        widgetKeydown( e ) {
            const $input = $( e.target ),
                name = $input.attr( 'class' ).replace( 'timepicker-', '' );

            switch ( e.which ) {
                case 9: //tab
                    if ( e.shiftKey ) {
                        if ( name === 'hour' ) {
                            return this.hideWidget();
                        }
                    } else if ( ( this.showMeridian && name === 'meridian' ) || ( this.showSeconds && name === 'second' ) || ( !this.showMeridian && !this.showSeconds && name === 'minute' ) ) {
                        return this.hideWidget();
                    }
                    break;
                case 27: // escape
                    this.hideWidget();
                    break;
                case 38: // up arrow
                    e.preventDefault();
                    switch ( name ) {
                        case 'hour':
                            this.incrementHour();
                            break;
                        case 'minute':
                            this.incrementMinute();
                            break;
                        case 'second':
                            this.incrementSecond();
                            break;
                        case 'meridian':
                            this.toggleMeridian();
                            break;
                    }
                    this.setTime( this.getTime() );
                    $input.get( 0 ).setSelectionRange( 0, 2 );
                    break;
                case 40: // down arrow
                    e.preventDefault();
                    switch ( name ) {
                        case 'hour':
                            this.decrementHour();
                            break;
                        case 'minute':
                            this.decrementMinute();
                            break;
                        case 'second':
                            this.decrementSecond();
                            break;
                        case 'meridian':
                            this.toggleMeridian();
                            break;
                    }
                    this.setTime( this.getTime() );
                    $input.get( 0 ).setSelectionRange( 0, 2 );
                    break;
            }
        },

        widgetKeyup( e ) {
            if ( ( e.which === 65 ) || ( e.which === 77 ) || ( e.which === 80 ) || ( e.which === 46 ) || ( e.which === 8 ) || ( e.which >= 48 && e.which <= 57 ) || ( e.which >= 96 && e.which <= 105 ) ) {
                this.updateFromWidgetInputs();
            }
        }
    };

    //TIMEPICKER PLUGIN DEFINITION
    $.fn.timepicker = function( option ) {
        const args = Array( ...arguments );
        args.shift();

        return this.each( function() {
            const $this = $( this );
            let data = $this.data( 'timepicker' );
            const options = typeof option === 'object' && option;

            if ( !data ) {
                $this.data( 'timepicker', ( data = new Timepicker( this, $.extend( {}, $.fn.timepicker.defaults, options, $( this ).data() ) ) ) );
            }

            if ( typeof option === 'string' ) {
                data[ option ]( ...args );
            }
        } );
    };

    $.fn.timepicker.defaults = {
        defaultTime: 'current',
        disableFocus: false,
        disableMousewheel: false,
        isOpen: false,
        minuteStep: 15,
        orientation: {
            x: 'auto',
            y: 'auto'
        },
        secondStep: 15,
        snapToStep: false,
        showSeconds: false,
        showInputs: true,
        showMeridian: true,
        meridianNotation: {
            am: 'AM',
            pm: 'PM'
        },
        template: 'dropdown',
        appendWidgetTo: 'body',
        showWidgetOnAddonClick: true,
        icons: {
            up: 'glyphicon glyphicon-chevron-up',
            down: 'glyphicon glyphicon-chevron-down'
        },
        maxHours: 24,
        explicitMode: false
    };

    $.fn.timepicker.Constructor = Timepicker;

    $( document ).on(
        'focus.timepicker.data-api click.timepicker.data-api',
        '[data-provide="timepicker"]',
        function( e ) {
            const $this = $( this );
            if ( $this.data( 'timepicker' ) ) {
                return;
            }
            e.preventDefault();
            // component click requires us to explicitly show it
            $this.timepicker();
        }
    );
} ) )( $, window, document );