This repository has been archived on 2024-10-24. You can view files and clone it, but cannot push or open issues or pull requests.
fradrive-old/frontend/src/utils/form/datepicker.js

248 lines
9.5 KiB
JavaScript

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;
}
}