254 lines
9.9 KiB
JavaScript
254 lines
9.9 KiB
JavaScript
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;
|
|
}
|
|
}
|