import datetime from 'tail.datetime'; import { Utility } from '../../core/utility'; import moment from 'moment'; const KEYCODE_ESCAPE = 27; const Z_INDEX_MODAL = 9999; // INTERNAL (Uni2work specific) formats for formatting dates and/or times const FORM_DATE_FORMAT = { 'date': moment.HTML5_FMT.DATE, 'time': moment.HTML5_FMT.TIME_SECONDS, 'datetime-local': moment.HTML5_FMT.DATETIME_LOCAL_SECONDS, }; // FANCY (tail.datetime specific) formats for displaying dates and/or times const FORM_DATE_FORMAT_DATE_DT = 'dd.mm.YYYY'; const FORM_DATE_FORMAT_TIME_DT = 'HH:ii:ss'; // FANCY (moment specific) formats for displaying dates and/or times const FORM_DATE_FORMAT_DATE_MOMENT = 'DD.MM.YYYY'; const FORM_DATE_FORMAT_TIME_MOMENT = 'HH:mm:ss'; const FORM_DATE_FORMAT_MOMENT = { 'date': FORM_DATE_FORMAT_DATE_MOMENT, 'time': FORM_DATE_FORMAT_TIME_MOMENT, 'datetime-local': `${FORM_DATE_FORMAT_DATE_MOMENT} ${FORM_DATE_FORMAT_TIME_MOMENT}`, }; /** * Takes a string representation of a date, an input ('previous') format and a desired output format and returns a reformatted date string. * If the date string is not valid (i.e. cannot be parsed with the given input format string), returns the original date string; * @param {*} dateStr string representation of a date (needs to be in format formatIn) * @param {*} formatIn input format string * @param {*} formatOut format string of the desired output date string */ function reformatDateString(dateStr, formatIn, formatOut) { const parsedMomentDate = moment(dateStr, [formatIn, formatOut]); return parsedMomentDate.isValid() ? parsedMomentDate.format(formatOut) : dateStr; } const DATEPICKER_UTIL_SELECTOR = 'input[type="date"], input[type="time"], input[type="datetime-local"]'; const DATEPICKER_INITIALIZED_CLASS = 'datepicker--initialized'; const DATEPICKER_CONFIG = { 'global': { // set default time to 00:00:00 timeHours: 0, timeMinutes: 0, timeSeconds: 0, weekStart: 1, dateFormat: FORM_DATE_FORMAT_DATE_DT, timeFormat: FORM_DATE_FORMAT_TIME_DT, // prevent the instance from closing when selecting a date before selecting a time stayOpen: true, // hide the close button (we handle closing the datepicker manually by clicking outside) closeButton: false, // disable the decades view because nobody will ever need it (i.e. cap the switch to the more relevant year view) viewDecades: false, }, 'datetime-local': {}, 'date': { // disable date picker timeFormat: false, }, 'time': { // disable time picker dateFormat: false, }, }; @Utility({ selector: DATEPICKER_UTIL_SELECTOR, }) export class Datepicker { // singleton Map that maps a formID to a Map of Datepicker objects static datepickerCollections; datepickerInstance; _element; elementType; _locale; constructor(element) { if (!element) { throw new Error('Datepicker utility needs to be passed an element!'); } if (element.classList.contains(DATEPICKER_INITIALIZED_CLASS)) { return false; } this._locale = window.App.i18n.getDatetimeLocale(); // initialize datepickerCollections singleton if not already done if (!Datepicker.datepickerCollections) { Datepicker.datepickerCollections = new Map(); } this._element = element; // store the previously set type to select the input format this.elementType = this._element.getAttribute('type'); // get all relevant config options for this datepicker type const datepickerGlobalConfig = DATEPICKER_CONFIG['global']; const datepickerConfig = DATEPICKER_CONFIG[this.elementType]; // manually set the type attribute to text because datepicker handles displaying the date this._element.setAttribute('type', 'text'); // additional position config (optional data-datepicker-position attribute in html) that can specialize the global config const datepickerPosition = this._element.dataset.datepickerPosition; if (datepickerPosition) { datepickerGlobalConfig.position = datepickerPosition; } if (!datepickerConfig || !FORM_DATE_FORMAT[this.elementType]) { throw new Error('Datepicker utility called on unsupported element!'); } // FIXME dirty hack below; fix tail.datetime instead // get date object from internal format before datetime does nasty things with it var parsedMomentDate = moment(this._element.value, [ FORM_DATE_FORMAT[this.elementType], FORM_DATE_FORMAT_MOMENT[this.elementType] ], true); if (parsedMomentDate && parsedMomentDate.isValid()) { parsedMomentDate = parsedMomentDate.toDate(); } else { parsedMomentDate = undefined; } // initialize tail.datetime (datepicker) instance and let it do weird stuff with the element value this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig, locale: this._locale }); // reset date to something sane if (parsedMomentDate) this.datepickerInstance.selectDate(parsedMomentDate); // insert the datepicker element (dt) after the form this._element.form.parentNode.insertBefore(this.datepickerInstance.dt, this._element.form.nextSibling); // if the input element is in any open modal, increase the z-index of the datepicker if (this._element.closest('.modal--open')) { this.datepickerInstance.dt.style.zIndex = Z_INDEX_MODAL; } // register this datepicker instance with the formID of the given element in the datepicker collection const formID = this._element.form.id; const elemID = this._element.id; if (!Datepicker.datepickerCollections.has(formID)) { // insert a new key value pair if the formID key is not there already Datepicker.datepickerCollections.set(formID, new Map([[elemID, this]])); } else { // otherwise, insert this instance into the Map Datepicker.datepickerCollections.get(formID).set(elemID, this); } // mark the form input element as initialized this._element.classList.add(DATEPICKER_INITIALIZED_CLASS); const setDatepickerDate = () => { // try to parse the current input element value with fancy and internal format string const parsedMomentDate = moment(this._element.value, FORM_DATE_FORMAT_MOMENT[this.elementType]); const parsedMomentDateInternal = moment(this._element.value, FORM_DATE_FORMAT[this.elementType]); // only set the datepicker date if the input is either in valid fancy format or in valid internal format if (parsedMomentDate.isValid()) { this.datepickerInstance.selectDate(parsedMomentDate.toDate()); } else if (parsedMomentDateInternal.isValid()) { this.datepickerInstance.selectDate(parsedMomentDateInternal.toDate()); } // reregister change event to prevent event loop this._element.addEventListener('change', setDatepickerDate, { once: true }); }; // change the selected date in the tail.datetime instance if the value of the input element is changed this._element.addEventListener('change', setDatepickerDate, { once: true }); // close the instance on focus loss of the corresponding input element // (only close the instance if source and target IDs differ, since interactions with the opened datepicker also triggers this event in some cases) this._element.addEventListener('focusout', event => { if (!event.relatedTarget || event.relatedTarget.id !== event.srcElement.id) this.datepickerInstance.close(); }); // close the datepicker on escape keydown events this._element.addEventListener('keydown', event => { if (event.keyCode === KEYCODE_ESCAPE) { this.datepickerInstance.close(); } }); // format the date value of the form input element of this datepicker before form submission this._element.form.addEventListener('submit', () => this.formatElementValue()); } destroy() { this.datepickerInstance.remove(); } /** * Formats the value of this input element from datepicker format (i.e. DATEPICKER_CONFIG.dateFormat + " " + datetime.defaults.timeFormat) to Uni2work internal date format (i.e. FORM_DATE_FORMAT) required for form submission * @param {*} toFancy optional target format switch (boolean value; default is false). If set to a truthy value, formats the element value to fancy instead of internal date format. */ formatElementValue(toFancy) { if (this._element.value) { this._element.value = this.unformat(toFancy); } } /** * Returns a datestring in internal format from the current state of the input element value. * @param {*} toFancy Format date from internal to fancy or vice versa. When omitted, toFancy is falsy and results in fancy -> internal */ unformat(toFancy) { const formatIn = toFancy ? FORM_DATE_FORMAT[this.elementType] : FORM_DATE_FORMAT_MOMENT[this.elementType]; const formatOut = toFancy ? FORM_DATE_FORMAT_MOMENT[this.elementType] : FORM_DATE_FORMAT[this.elementType]; return reformatDateString(this._element.value, formatIn, formatOut); } /** * Takes a Form and a FormData and returns a new FormData with all dates formatted to uni2work date format. This function will not change the value of the date input elements. * @param {*} form Form for which all dates will be formatted in the FormData * @param {*} formData Initial FormData */ static unformatAll(form, formData) { // only proceed if there are any datepickers and if both form and formData are defined if (Datepicker.datepickerCollections && form && formData) { // if the form has no id, assign one randomly if (!form.id) { form.id = `f${Math.floor(Math.random() * 100000)}`; } const formId = form.id; if (Datepicker.datepickerCollections.has(formId)) { const datepickerInstances = Datepicker.datepickerCollections.get(formId); datepickerInstances.forEach(instance => { formData.set(instance._element.name, instance.unformat()); }); } } // return the (possibly changed) FormData return formData; } }