widget/datetime/datetimepicker-extended.js

import 'openrosa-xpath-evaluator/src/date-extensions';
import Widget from '../../js/widget';
import support from '../../js/support';
import { os, browser } from '../../js/sniffer';
import $ from 'jquery';
import { time as timeFormat } from '../../js/format';
import types from '../../js/types';
import events from '../../js/event';
import '../../js/extend';
import 'bootstrap-datepicker';
import '../time/timepicker';
import '../../js/dropdown.jquery';

/**
 * @augments Widget
 */
class DatetimepickerExtended extends Widget {
    /**
     * @type {string}
     */
    static get selector() {
        return '.question input[type="datetime-local"]:not([readonly])';
    }
    /**
     * @return {boolean} to instantiate or not to instantiate, that is the question
     */
    static condition() {
        return !support.touch || !support.inputTypes[ 'datetime-local' ];
    }

    _init() {
        this.$fakeDateI = this._createFakeDateInput();
        this.$fakeTimeI = this._createFakeTimeInput();

        this.element.classList.add( 'hide' );
        this.element.before( document.createRange().createContextualFragment( '<div class="datetimepicker widget" />' ) );
        const widget = this.question.querySelector( '.widget' );
        widget.append( this.$fakeDateI[ 0 ].closest( '.date' ) );
        widget.append( this.$fakeTimeI[ 0 ].closest( '.timepicker' ) );

        this.$fakeDateI
            .datepicker( {
                format: 'yyyy-mm-dd',
                autoclose: true,
                todayHighlight: true,
                forceParse: false
            } );

        this.$fakeTimeI
            .timepicker( {
                showMeridian: timeFormat.hour12,
                meridianNotation: {
                    am: timeFormat.amNotation,
                    pm: timeFormat.pmNotation
                }
            } );

        this.value = this.originalInputValue;

        this._setFocusHandler( this.$fakeDateI.add( this.$fakeTimeI ) );

        this.$fakeDateI
            .on( 'change changeDate', () => {
                if ( !types.date.validate( this.$fakeDateI[ 0 ].value ) ) {
                    this.$fakeDateI.val( '' ).datepicker( 'update' );
                }
                this.originalInputValue = this.value;

                return false;
            } );

        this.$fakeTimeI
            .on( 'change', () => {
                this.originalInputValue = this.value;

                return false;
            } );

        //reset button
        this.question.querySelector( '.btn-reset' ).addEventListener( 'click', () => {
            const event = this.originalInputValue ? 'change' : '';
            if ( event || this.$fakeDateI.val() || this.$fakeTimeI.val() ) {
                this.$fakeDateI.val( '' ).trigger( event ).datepicker( 'update' );
                this.$fakeTimeI.val( '' ).trigger( event );
            }
        } );
    }

    /**
     * @return {Element} fake date input
     */
    _createFakeDateInput() {
        const $fakeDate = $(
            '<div class="date">' +
            '<input class="ignore" type="text" placeholder="yyyy-mm-dd"/>' +
            '</div>' );

        return $fakeDate.find( 'input' );
    }

    /**
     * @return {Element} fake time input
     */
    _createFakeTimeInput() {
        const $fakeTime = $(
            `<div class="timepicker">
                    <input class="ignore timepicker-default" type="text" placeholder="hh:mm"/>
                </div>` )
            .append( this.resetButtonHtml );

        return $fakeTime.find( 'input' );
    }

    /**
     * @param {jQuery} $els - a set of elements wrapped in jQuery
     */
    _setFocusHandler( $els ) {
        // Handle focus on original input (goTo functionality)
        this.element.addEventListener( events.ApplyFocus().type, () => {
            $els.eq( 0 ).focus();
        } );
    }

    update() {
        const $dateTimeI = $( this.element );
        let val = ( $dateTimeI.val().length > 0 ) ? new Date( $dateTimeI.val() ).toISOLocalString() : '';
        /**
         * fix a bug which is only on safari (#745)
         * If the local timezone is +08:00, for a date value of new Date('2020-10-10T13:10:10') will be:
         * *** in chrome, the value is: Sat Oct 10 2020 13:10:10 GMT+0800 (China Standard Time)
         * *** but in safari, the value is: Sat Oct 10 2020 21:10:10 GMT+0800 (CST)
         * so we have to append the timezone here
         */
        if ( os.macos && browser.safari ) {
            val = ( $dateTimeI.val().length > 0 ) ? ( new Date( $dateTimeI.val() + new Date().timezoneOffsetAsTime() ) ).toISOLocalString() : '';
        }
        if ( val !== this.value ) {
            const vals = val.split( 'T' );
            const dateVal = vals[ 0 ];
            const timeVal = ( vals[ 1 ] && vals[ 1 ].length > 4 ) ? vals[ 1 ].substring( 0, 5 ) : '';

            this.$fakeDateI.datepicker( 'setDate', dateVal );
            this.$fakeTimeI.timepicker( 'setTime', timeVal );
        }
    }

    /**
     * @type {string}
     */
    get value() {
        if ( this.$fakeDateI.val().length > 0 && this.$fakeTimeI.val().length > 3 ) {
            const d = this.$fakeDateI.val().split( '-' );
            const timeModified = timeFormat.hour12 ? types.time.convertMeridian( this.$fakeTimeI.val() ) : this.$fakeTimeI.val();
            const t = timeModified.split( ':' );

            return new Date( d[ 0 ], d[ 1 ] - 1, d[ 2 ], t[ 0 ], t[ 1 ] ).toISOLocalString();
        } else {
            return '';
        }
    }

    set value( value ) {
        /*
          Loaded or default datetime values remain untouched until they are edited. This is done to preserve
          the timezone information (especially for instances-to-edit) if the values are not edited (the
          original entry may have been done in a different time zone than the edit). However,
          values shown in the widget should reflect the local time representation of that value.
         */
        const val = value ? new Date( value ).toISOLocalString() : '';
        const vals = val.split( 'T' );
        const dateVal = vals[ 0 ];
        /**
         * seems the source of issue #649 is in the toISOLocalString function
         * refer: https://github.com/enketo/enketo-xpathjs/blob/master/src/date-extensions.js#L16
         */
        const timeVal = ( vals[ 1 ] && vals[ 1 ].length > 4 ) ? vals[ 1 ].substring( 0, 5 ) : ( dateVal && !vals[ 1 ] ) ? '00:00' : '';
        this.$fakeDateI.datepicker( 'setDate', dateVal );
        this.$fakeTimeI.timepicker( 'setTime', timeVal );
    }
}

export default DatetimepickerExtended;