UNPKG

enketo-core

Version:

Extensible Enketo form engine

232 lines (201 loc) 7.13 kB
import $ from 'jquery'; import Widget from '../../js/widget'; import support from '../../js/support'; 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; if (!$fakeDateI.closest('label').hasClass('readonly')) { $fakeDateI.on('change paste', (e) => { let convertedValue = ''; let value = e.type === 'paste' ? getPasteData(e.originalEvent ?? e) : this.displayedValue; 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, except 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;