import datetime from 'tail.datetime'; import { Utility } from '../../core/utility'; import moment from 'moment'; const KEYCODE_ESCAPE = 27; // 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 and a format string and parses the given date to a Date object. * If the date string is not valid (i.e. cannot be parsed with the given format string), returns undefined. * @param {*} dateStr string representation of a date * @param {*} dateFormat format string of the date */ function parseDateWithFormat(dateStr, dateFormat) { const parsedMomentDate = moment(dateStr, dateFormat); if (parsedMomentDate.isValid()) return parsedMomentDate.toDate(); } /** * 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); 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': { // minimize overlaps with other date inputs position: 'right', // set default time to 00:00:00 timeHours: 0, timeMinutes: 0, timeSeconds: 0, // german settings // TODO: hardcoded, get from current language / settings locale: 'de', 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; constructor(element) { if (!element) { throw new Error('Datepicker utility needs to be passed an element!'); } if (element.classList.contains(DATEPICKER_INITIALIZED_CLASS)) { return false; } // 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!'); } // initialize tail.datetime (datepicker) instance this.datepickerInstance = datetime(this._element, { ...datepickerGlobalConfig, ...datepickerConfig }); // 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 if something other than the instance was clicked (i.e. if the target is not within the datepicker instance and if any previously clicked calendar view was replaced (is not in the window anymore) because it was clicked). YES, I KNOW window.addEventListener('click', event => { if (!this.datepickerInstance.dt.contains(event.target) && window.document.contains(event.target)) { 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()); // format any existing dates to fancy display format on pageload this.formatElementValue(true); } 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) { const dp = this.datepickerInstance; if (this._element.value) { if (toFancy) { const parsedDate = parseDateWithFormat(this._element.value, FORM_DATE_FORMAT[this.elementType]); if (parsedDate) dp.selectDate(); } else { this._element.value = this.unformat(); } } } /** * Returns a datestring in internal format from the current state of the input element value. */ unformat() { return reformatDateString(this._element.value, FORM_DATE_FORMAT_MOMENT[this.elementType], FORM_DATE_FORMAT[this.elementType]); } /** * 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; } }