UNPKG

samagra-x-enketo-core

Version:

Extensible Enketo form engine

213 lines (181 loc) 6.71 kB
import Widget from '../../js/widget'; import support from '../../js/support'; import $ from 'jquery'; import types from '../../js/types'; import { isNumber, getPasteData } from '../../js/utils'; import 'bootstrap-datepicker'; import '../../js/dropdown.jquery'; /** * Extends eternicode's bootstrap-datepicker without changing the original. * https://github.com/eternicode/bootstrap-datepicker * * @augments Widget */ class DatepickerExtended extends Widget { /** * @type {string} */ static get selector() { return '.question input[type="date"]'; } /** * @type {boolean} */ static condition() { return !support.touch || !support.inputTypes.date; } _init() { this.settings = ( this.props.appearances.includes( 'year' ) ) ? { format: 'yyyy', startView: 'decade', minViewMode: 'years' } : ( this.props.appearances.includes( 'month-year' ) ) ? { format: 'yyyy-mm', startView: 'year', minViewMode: 'months' } : { format: 'yyyy-mm-dd', startView: 'month', minViewMode: 'days' }; this.$fakeDateI = this._createFakeDateInput( this.settings.format ); this._setChangeHandler( this.$fakeDateI ); this._setFocusHandler( this.$fakeDateI ); this._setResetHandler( this.$fakeDateI ); this.enable(); this.value = this.element.value; // It is much easier to first enable and disable, and not as bad it seems, since readonly will become dynamic eventually. if ( this.props.readonly ) { this.disable(); } } /** * Creates fake date input elements * * @param {string} format - The date format * @return {jQuery} The jQuery-wrapped fake date input element */ _createFakeDateInput( format ) { const $dateI = $( this.element ); const $fakeDate = $( `<div class="widget date"><input class="ignore input-small" type="text" placeholder="${format}" /></div>` ) .append( this.resetButtonHtml ); const $fakeDateI = $fakeDate.find( 'input' ); $dateI.hide().before( $fakeDate ); return $fakeDateI; } /** * Copy manual changes that were not detected by bootstrap-datepicker (one without pressing Enter) to original date input field * * @param {jQuery} $fakeDateI - Fake date input element */ _setChangeHandler( $fakeDateI ) { const settings = this.settings; $fakeDateI.on( 'change paste', e => { let convertedValue = ''; let value = e.type === 'paste' ? getPasteData( e ) : this.value; if ( value.length > 0 ) { // Note: types.date.convert considers numbers to be a number of days since the epoch // as this is what the XPath evaluator may return. // For user-entered input, we want to consider a Number value to be incorrect, expect for year input. if ( isNumber( value ) && settings.format !== 'yyyy' ) { convertedValue = ''; } else { value = this._toActualDate( value ); convertedValue = types.date.convert( value ); } } $fakeDateI.val( this._toDisplayDate( convertedValue ) ).datepicker( 'update' ); // Here we have to do something unusual to prevent native inputs from automatically // changing 2012-12-32 into 2013-01-01 // convertedValue is '' for invalid 2012-12-32 if ( convertedValue === '' && e.type === 'paste' ) { e.stopImmediatePropagation(); } // Avoid triggering unnecessary change events as they mess up sensitive custom applications (OC) if ( this.originalInputValue !== convertedValue ) { this.originalInputValue = convertedValue; } return false; } ); } /** * Reset button handler * * @param {jQuery} $fakeDateI - Fake date input element */ _setResetHandler( $fakeDateI ) { $fakeDateI.next( '.btn-reset' ).on( 'click', () => { if ( this.originalInputValue ) { this.value = ''; } } ); } /** * Handler for focus events. * These events on the original input are used to check whether to display the 'required' message * * @param {jQuery} $fakeDateI - Fake date input element */ _setFocusHandler( $fakeDateI ) { // Handle focus on original input (goTo functionality) $( this.element ).on( 'applyfocus', () => { $fakeDateI[ 0 ].focus(); } ); } /** * @param {string} [date] - date * @return {string} the actual date */ _toActualDate( date = '' ) { date = date.trim(); return date && this.settings.format === 'yyyy' && date.length < 5 ? `${date}-01-01` : ( date && this.settings.format === 'yyyy-mm' && date.length < 8 ? `${date}-01` : date ); } /** * @param {string} [date] - date * @return {string} the display date */ _toDisplayDate( date = '' ) { date = date.trim(); return date && this.settings.format === 'yyyy' ? date.substring( 0, 4 ) : ( this.settings.format === 'yyyy-mm' ? date.substring( 0, 7 ) : date ); } disable() { this.$fakeDateI.datepicker( 'destroy' ); this.$fakeDateI.prop( 'disabled', true ); this.$fakeDateI.next( '.btn-reset' ).prop( 'disabled', true ); } enable() { this.$fakeDateI.datepicker( { format: this.settings.format, autoclose: true, todayHighlight: true, startView: this.settings.startView, minViewMode: this.settings.minViewMode, forceParse: false } ); this.$fakeDateI.prop( 'disabled', false ); this.$fakeDateI.next( '.btn-reset' ).prop( 'disabled', false ); } update() { this.value = this.element.value; } /** * @type {string} */ get displayedValue() { return this.question.querySelector( '.widget input' ).value; } /** * @type {string} */ get value() { return this._toActualDate( this.displayedValue ); } set value( date ) { if ( this.$fakeDateI[ 0 ].disabled ) { this.$fakeDateI[ 0 ].value = this._toDisplayDate( date ); } else { this.$fakeDateI.datepicker( 'setDate', this._toDisplayDate( date ) ); } } } export default DatepickerExtended;