@unicef-polymer/etools-unicef
Version:
eTools UNICEF library of reusable components
721 lines (705 loc) • 22.8 kB
JavaScript
'use strict';
import { __decorate } from "tslib";
import { LitElement, html } from 'lit';
import { property, customElement } from 'lit/decorators.js';
import '../etools-icons/etools-icon';
import '../etools-button/etools-button';
import '@a11y/focus-trap';
import './calendar-lite';
import '../etools-input/etools-input';
import dayjs from 'dayjs';
const dateLib = dayjs || window.moment;
if (!dateLib) {
throw new Error('DatepickerLite: dayjs or moment is not loaded');
}
let openedDatepickerLiteElems = window.openedDatepickerLiteElems || [];
let openedDatepickerLiteElemsCloseTimeout = window.openedDatepickerLiteElemsCloseTimeout || null;
const controlFormat = 'YYYY-MM-DD';
const _closeDatepickers = (keepOpenDatepicker) => {
openedDatepickerLiteElems.forEach((datePicker) => {
if (datePicker.calendar.opened && keepOpenDatepicker !== datePicker.calendar) {
datePicker.calendar.opened = false;
}
});
openedDatepickerLiteElems =
keepOpenDatepicker && keepOpenDatepicker.opened ? [{ keepOpen: false, calendar: keepOpenDatepicker }] : [];
};
const _getClickedDatepicker = (e) => {
let clickedDatepicker = e.target.tagName.toLowerCase() === 'datepicker-lite' ? e.target : e.target.closest('datepicker-lite');
if (!clickedDatepicker) {
const openedDatepikerMap = openedDatepickerLiteElems.find((d) => d.keepOpen === true);
if (openedDatepikerMap && openedDatepikerMap.keepOpen) {
clickedDatepicker = openedDatepikerMap.calendar;
}
}
return clickedDatepicker;
};
const _handleDatepickerLiteCloseOnClickOrTap = (e) => {
if (openedDatepickerLiteElems.length === 0 || openedDatepickerLiteElemsCloseTimeout !== null) {
return;
}
// timeout is used for debouncing Event and MouseEvent
openedDatepickerLiteElemsCloseTimeout = setTimeout(() => {
const clickedDatepicker = _getClickedDatepicker(e);
openedDatepickerLiteElemsCloseTimeout = null;
if (!(openedDatepickerLiteElems.length === 1 && openedDatepickerLiteElems[0] === clickedDatepicker)) {
_closeDatepickers(clickedDatepicker);
}
}, 10);
};
document.addEventListener('tap', _handleDatepickerLiteCloseOnClickOrTap);
document.addEventListener('click', _handleDatepickerLiteCloseOnClickOrTap);
/**
* @customElement
* @polymer
* @appliesMixin GestureEventListeners
*/
let DatePickerLite = class DatePickerLite extends LitElement {
set value(value) {
if (this._value !== value) {
this._value = value;
this._valueChanged(this.value);
}
}
get value() {
return this._value;
}
set monthInput(monthInput) {
this._monthInput = monthInput;
this.computeDate(this.monthInput, this.dayInput, this.yearInput);
}
get monthInput() {
return this._monthInput;
}
set dayInput(dayInput) {
this._dayInput = dayInput;
this.computeDate(this.monthInput, this.dayInput, this.yearInput);
}
get dayInput() {
return this._dayInput;
}
set yearInput(yearInput) {
this._yearInput = yearInput;
this.computeDate(this.monthInput, this.dayInput, this.yearInput);
}
get yearInput() {
return this._yearInput;
}
render() {
// language=HTML
return html `
<style>
:host {
display: block;
max-width: 100%;
}
sl-input {
width: 180px;
max-width: 100%;
}
:host(:not([readonly])) etools-icon {
cursor: pointer;
--etools-icon-fill-color: var(--secondary-text-color);
}
etools-icon[slot='prefix'] {
margin-inline-end: 8px;
}
etools-icon[slot='suffix'] {
margin-inline-start: 8px;
--etools-icon-font-size: var(--etools-font-size-20, 20px);
}
.clear-btn,
.close-btn {
margin: 10px;
}
.clear-btn::part(base) {
background: var(--datepiker-lite-clear-btn-bg, #ff4747);
color: #fff;
padding: 6px;
}
.close-btn {
padding: 6px;
}
.monthInput {
width: 35px;
}
.dayInput {
width: 25px;
}
.yearInput {
width: 40px;
}
.actions {
display: flex;
justify-content: flex-end;
}
*[hidden] {
display: none;
}
calendar-lite {
z-index: 130;
margin-top: -10px;
}
#dateDisplayinputContainer:not([readonly]) {
cursor: pointer;
}
</style>
<etools-input
.label="${this.label}"
id="dateDisplayinputContainer"
always-float-label
?disabled="${this.disabled}"
?readonly="${this.readonly}"
?required="${this.required}"
?invalid="${this.invalid}"
prevent-user-direct-input
value="${this.formatDateForDisplay(this.value, this.readonly)}"
.errorMessage="${this.errorMessage}"
="${this._toggelOnKeyPressFromPaperInput}"
="${this.toggleCalendarFromPaperInput}"
>
<etools-icon
="${this._toggelOnKeyPressFromIcon}"
?readonly="${this.readonly}"
name="date-range"
title="Toggle calendar"
tabindex="${this._getTabindexByReadonly(this.readonly)}"
="${this.toggleCalendarFromIcon}"
slot="prefix"
part="dp-etools-icon"
></etools-icon>
${this.getXBtnHTML()}
</etools-input>
<focus-trap>
<calendar-lite
id="calendar"
part="dp-calendar"
-changed="${this.datePicked}"
.date="${this.inputDate}"
.minDate="${this.minDate}"
.maxDate="${this.maxDate}"
?hidden="${!this.opened}"
="${this.closeCalendarOnEsc}"
>
<div class="actions" slot="actions">${this.getActionsHTML()}</div>
</calendar-lite>
</focus-trap>
`;
}
constructor() {
super();
this.readonly = false;
this.required = false;
this.errorMessage = 'Invalid date';
this.disabled = false;
this.invalid = false;
this.opened = false;
this.clearBtnInsideDr = false;
this.closeOnSelect = true;
this.autoValidate = false;
this.minDateErrorMsg = 'Date is earlier than min date';
this.maxDateErrorMsg = 'Date exceeds max date';
this.requiredErrorMsg = 'This field is required';
this.selectedDateDisplayFormat = 'D MMM YYYY';
this.inputDateFormat = '';
}
connectedCallback() {
super.connectedCallback();
this.addEventListener('click', () => {
if (openedDatepickerLiteElems.length === 0) {
return;
}
for (let i = 0; i < openedDatepickerLiteElems.length; i++) {
if (openedDatepickerLiteElems[i].calendar === this && this.opened) {
openedDatepickerLiteElems[i].keepOpen = true;
break;
}
}
});
}
updated(changedProperties) {
// Invalid state should be reset when auto-validate becomes false as a
// result of user cancelling the edit of the form
if (changedProperties.has('autoValidate') && !this.autoValidate) {
setTimeout(() => {
this.invalid = false;
}, 50);
}
}
getActionsHTML() {
return html `${this.getClearBtnHTML()} ${this.getCloseBtnHTML()}`;
}
getClearBtnHTML() {
return this.readonly
? html ``
: html `<etools-button class="clear-btn" ="${this._clearData}" ?hidden="${!this.clearBtnInsideDr}"
>Clear</etools-button
>`;
}
getCloseBtnHTML() {
return this.closeOnSelect
? html ``
: html `<etools-button class="close-btn" ="${this.toggleCalendar}">Close</etools-button>`;
}
getXBtnHTML() {
const showXBtn = this.showXBtn(this.readonly, this.disabled, this.value);
return showXBtn
? html ` <etools-icon
name="clear"
slot="suffix"
="${this._clearData}"
title="Clear"
tabindex="0"
?hidden="${this.clearBtnInsideDr}"
="${this.activateOnEnterAndSpace}"
part="dp-etools-icon"
></etools-icon>`
: html ``;
}
activateOnEnterAndSpace(event) {
if ((event.key === ' ' && !event.ctrlKey) || event.key === 'Enter') {
// Cancel the default action, if needed
event.preventDefault();
// Trigger the button element with a click
event.target.click();
return false;
}
return true;
}
closeCalendarOnEsc(event) {
if (event.key === 'Escape') {
event.preventDefault();
event.target.closest('focus-trap').parentNode.host.opened = false;
}
}
_getDateString(date) {
let month = '' + (date.getMonth() + 1);
let day = '' + date.getDate();
const year = date.getFullYear();
month = month.length < 2 ? '0' + month : month;
day = day.length < 2 ? '0' + day : day;
return [year, month, day].join('-');
}
_getTabindexByReadonly(readOnly) {
return readOnly ? '-1' : '0';
}
_triggerDateChangeCustomEvent(date) {
if (this.fireDateHasChanged) {
this.dispatchEvent(new CustomEvent('date-has-changed', {
detail: { date: date },
bubbles: true,
composed: true
}));
}
}
datePicked(event) {
if (this._clearDateInProgress) {
this._clearDateInProgress = false;
return;
}
const date = event.detail.value;
if (!date) {
return;
}
this._setDayMonthYearInInputElements(date);
this.value = this._getDateString(date);
if (this.closeOnSelect) {
_closeDatepickers();
}
this._stopDateCompute = false;
}
_setDayMonthYearInInputElements(date) {
let month = '' + (date.getMonth() + 1);
let day = '' + date.getDate();
const year = date.getFullYear();
month = month.length < 2 ? '0' + month : month;
day = day.length < 2 ? '0' + day : day;
this._stopDateCompute = true;
this.monthInput = Number(month);
this.dayInput = Number(day);
this.yearInput = Number(year);
}
computeDate(month, day, year) {
if (this.autoValidate) {
// this.set('_stopDateCompute', false);
this.invalid = !this._isValidYear() || !this._isValidMonth() || !this._isValidDay();
if (this.invalid) {
this.errorMessage = 'Invalid date';
}
}
if (month !== undefined && day !== undefined && year !== undefined) {
if (this._stopDateCompute) {
// prevent setting wrong value when year/month/day are set by datepiker in datePicked
return;
}
if (this.monthInput || this.dayInput || this.yearInput) {
if (this._isValidYear() && this._isValidMonth() && this._isValidDay() && this._enteredDateIsValid()) {
const newDate = new Date(year, month - 1, day);
this.inputDate = newDate;
this.value = year + '-' + month + '-' + day;
}
}
else {
this.value = null;
}
}
}
toggleCalendarFromPaperInput() {
this.toggleCalendar();
}
toggleCalendarFromIcon(e) {
var _a, _b;
e.stopImmediatePropagation();
(_a = this.shadowRoot.querySelector('#dateDisplayinputContainer')) === null || _a === void 0 ? void 0 : _a.click();
if (this.opened) {
(_b = this.shadowRoot.querySelector('#dateDisplayinputContainer')) === null || _b === void 0 ? void 0 : _b.focus();
}
}
toggleCalendar() {
if (!this.readonly && !this.disabled) {
this.shadowRoot.querySelector('#calendar').style.marginTop = `${this._getCalendarMarginTop()}px`;
this.opened = !this.opened;
if (openedDatepickerLiteElems.length > 0) {
_closeDatepickers();
}
if (this.opened) {
openedDatepickerLiteElems.push({ keepOpen: true, calendar: this });
}
}
}
_getCalendarMarginTop() {
let marginTop = -10;
const calendarHeight = 305;
const datepickerPosition = this.shadowRoot.querySelector('#dateDisplayinputContainer').getBoundingClientRect();
const availableHeightBelow = document.querySelector('body').getBoundingClientRect().height - datepickerPosition.bottom;
if (availableHeightBelow < calendarHeight && datepickerPosition.top > availableHeightBelow) {
// show calendar on top of the control
marginTop = 0 - (datepickerPosition.height + calendarHeight);
}
return marginTop;
}
_toggelOnKeyPressFromPaperInput(event) {
this._toggelOnKeyPress(event);
}
_toggelOnKeyPressFromIcon(event) {
event.stopImmediatePropagation();
this._toggelOnKeyPress(event);
}
_toggelOnKeyPress(event) {
if (!this.readonly) {
if (event.which === 13 || event.button === 0) {
this.toggleCalendar();
}
}
}
_clearData(e) {
if (e) {
e.preventDefault();
e.stopImmediatePropagation();
}
this._clearDateInProgress = true;
this.inputDate = null;
this.monthInput = undefined;
this.dayInput = undefined;
this.yearInput = undefined;
this.value = null;
if (this.autoValidate) {
this.validate();
}
else {
this.invalid = false;
}
this._triggerDateChangeCustomEvent(this.value);
setTimeout(() => {
this._clearDateInProgress = false;
}, 0);
}
_isValidYear() {
if (this.yearInput !== undefined) {
return this.yearInput >= 1970 && this.yearInput < 9999 && String(this.yearInput).length === 4;
}
return false;
}
_isValidMonth() {
return Number(this.monthInput) >= 1 && Number(this.monthInput) <= 12;
}
_isValidDay() {
return this.dayInput >= 1 && this.dayInput <= 31;
}
_enteredDateIsValid() {
const newDate = new Date(this.yearInput, this.monthInput - 1, this.dayInput);
const newYear = newDate.getFullYear();
const newMonth = newDate.getMonth() + 1;
const newDay = newDate.getDate();
const valid = newMonth === Number(this.monthInput) && newDay === Number(this.dayInput) && newYear === Number(this.yearInput);
if (!valid) {
this.errorMessage = 'Invalid date';
}
return valid;
}
validate() {
let valid = true;
valid = this.requiredValidation() && this.maxDateValidation() && this.minDateValidation();
if (valid) {
if (this.yearInput || this.monthInput || this.dayInput) {
valid = this._enteredDateIsValid();
}
}
this.invalid = !valid;
return valid;
}
maxDateValidation() {
if (this.maxDate && this.value) {
const valid = dateLib(this.value, controlFormat).toDate() <= this.maxDate;
if (!valid) {
this.errorMessage = this.maxDateErrorMsg;
}
return valid;
}
return true;
}
minDateValidation() {
if (this.minDate && this.value) {
const valid = dateLib(this.value, controlFormat).toDate() >= this.minDate;
if (!valid) {
this.errorMessage = this.minDateErrorMsg;
}
return valid;
}
return true;
}
requiredValidation() {
if (this.required) {
const valid = this._isValidMonth() && this._isValidDay() && this._isValidYear() && this._enteredDateIsValid();
if (!valid) {
this.errorMessage = this.requiredErrorMsg
? this.requiredErrorMsg
: this.maxDate
? 'This field is required'
: this.errorMessage;
}
return valid;
}
return true;
}
_valueChanged(newValue) {
if (!newValue) {
if (this.monthInput || this.dayInput || this.yearInput) {
this._clearData();
}
return;
}
if (this.inputDateFormat) {
const formattedDate = dateLib(newValue, this.inputDateFormat, true);
if (formattedDate.isValid()) {
newValue = formattedDate.format(controlFormat);
}
}
const dData = newValue.split('-');
if (dData.length !== 3) {
// value need to be yyyy-mm-dd format
return;
}
this._stopDateCompute = true;
this.monthInput = dData[1];
this.dayInput = dData[2].slice(0, 2);
this.yearInput = dData[0];
this._stopDateCompute = false;
const d = new Date(dData[0], Number(dData[1]) - 1, dData[2]);
if (d.toString() !== 'Invalid Date') {
this.inputDate = d;
this._triggerDateChangeCustomEvent(this.value);
}
}
_handleOnBlur() {
if (this.autoValidate) {
this.validate();
}
}
formatDateForDisplay(selectedDt, readonly) {
if (!selectedDt) {
return readonly ? '—' : '';
}
return dateLib(selectedDt, controlFormat).format(this.selectedDateDisplayFormat);
}
showXBtn(readonly, disabled, selectedDt) {
return !readonly && !disabled && selectedDt;
}
};
__decorate([
property({
type: String
})
], DatePickerLite.prototype, "value", null);
__decorate([
property({
type: Boolean,
reflect: true
})
], DatePickerLite.prototype, "readonly", void 0);
__decorate([
property({
type: Boolean,
reflect: true
})
], DatePickerLite.prototype, "required", void 0);
__decorate([
property({
type: String,
reflect: true,
attribute: 'error-message'
})
], DatePickerLite.prototype, "errorMessage", void 0);
__decorate([
property({
type: Boolean,
reflect: true
})
], DatePickerLite.prototype, "disabled", void 0);
__decorate([
property({
type: String
})
], DatePickerLite.prototype, "label", void 0);
__decorate([
property({
type: Number
})
], DatePickerLite.prototype, "monthInput", null);
__decorate([
property({
type: Number
})
], DatePickerLite.prototype, "dayInput", null);
__decorate([
property({
type: Number
})
], DatePickerLite.prototype, "yearInput", null);
__decorate([
property({
type: Boolean
})
], DatePickerLite.prototype, "invalid", void 0);
__decorate([
property({
type: Date
// notify: true
})
], DatePickerLite.prototype, "inputDate", void 0);
__decorate([
property({
type: Boolean
})
], DatePickerLite.prototype, "opened", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
attribute: 'clear-btn-inside-dr'
})
], DatePickerLite.prototype, "clearBtnInsideDr", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
attribute: 'close-on-select'
})
], DatePickerLite.prototype, "closeOnSelect", void 0);
__decorate([
property({
type: Boolean
})
], DatePickerLite.prototype, "_clearDateInProgress", void 0);
__decorate([
property({
type: Boolean
})
], DatePickerLite.prototype, "_stopDateCompute", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
attribute: 'auto-validate'
})
], DatePickerLite.prototype, "autoValidate", void 0);
__decorate([
property({
type: Date,
reflect: true,
attribute: 'min-date',
converter: {
fromAttribute: (value) => {
if (typeof value === 'string') {
return dateLib(value, controlFormat).toDate();
}
return value;
}
}
})
], DatePickerLite.prototype, "minDate", void 0);
__decorate([
property({
type: Date,
reflect: true,
attribute: 'max-date',
converter: {
fromAttribute: (value) => {
if (typeof value === 'string') {
return dateLib(value, controlFormat).toDate();
}
return value;
}
}
})
], DatePickerLite.prototype, "maxDate", void 0);
__decorate([
property({
type: Boolean,
reflect: true,
attribute: 'fire-date-has-changed'
})
], DatePickerLite.prototype, "fireDateHasChanged", void 0);
__decorate([
property({
type: String,
reflect: true,
attribute: 'min-date-error-msg'
})
], DatePickerLite.prototype, "minDateErrorMsg", void 0);
__decorate([
property({
type: String,
reflect: true,
attribute: 'max-date-error-msg'
})
], DatePickerLite.prototype, "maxDateErrorMsg", void 0);
__decorate([
property({
type: String,
reflect: true,
attribute: 'required-error-msg'
})
], DatePickerLite.prototype, "requiredErrorMsg", void 0);
__decorate([
property({
// to display selected date in a different format than default 'YYYY-MM-DD'
// Ex: other option would be 'D MMM YYYY'
type: String,
reflect: true,
attribute: 'selected-date-display-format'
})
], DatePickerLite.prototype, "selectedDateDisplayFormat", void 0);
__decorate([
property({
// datepicker works internally with date in format 'YYYY-MM-DD', in case input
// value has a different format, this can be specified using this property
type: String,
reflect: true,
attribute: 'input-date-format'
})
], DatePickerLite.prototype, "inputDateFormat", void 0);
DatePickerLite = __decorate([
customElement('datepicker-lite')
], DatePickerLite);
export { DatePickerLite };