import { html, css, nothing, PropertyValueMap } from 'lit';
import { customElement, property, query, state } from 'lit/decorators.js';
import { ClassInfo, classMap } from 'lit/directives/class-map.js';
import { live } from 'lit/directives/live.js';
import { DateTime } from 'luxon';
import { OmniFormElement } from '../core/OmniFormElement.js';
import '../calendar/Calendar.js';
import '../icons/Calendar.icon.js';
import '../icons/ChevronLeft.icon.js';
import '../icons/ChevronRight.icon.js';
/**
* Control to get / set a specific date using a calendar.
*
* @import
* ```js
* import '@capitec/omni-components/date-picker';
* ```
*
* @example
* ```html
*
*
* ```
* @element omni-date-picker
*
* Registry of all properties defined by the component.
*
* @fires {CustomEvent<{}>} change - Dispatched when a date is selected.
*
* @cssprop --omni-date-picker-text-align - Date picker input text align.
* @cssprop --omni-date-picker-font-color - Date picker input font color.
* @cssprop --omni-date-picker-font-family - Date picker input font family.
* @cssprop --omni-date-picker-font-size - Date picker input font size.
* @cssprop --omni-date-picker-font-weight - Date picker input font weight.
* @cssprop --omni-date-picker-height - Date picker input height.
* @cssprop --omni-date-picker-padding - Date picker input padding.
* @cssprop --omni-date-picker-width - Date picker width.
* @cssprop --omni-date-picker-min-width - Date picker min width.
*
* @cssprop --omni-date-picker-disabled-font-color - Date picker disabled font color.
*
* @cssprop --omni-date-picker-error-font-color - Date picker error font color.
*
* @cssprop --omni-date-picker-control-padding - Date picker control padding.
* @cssprop --omni-date-picker-control-hover-color - Date picker control hover.
*
* @cssprop --omni-date-picker-control-icon-width - Date picker control icon width.
* @cssprop --omni-date-picker-control-icon-height - Date picker control icon height.
* @cssprop --omni-date-picker-control-icon-color - Date picker control icon color.
*
* @cssprop --omni-date-picker-control-icon-error-color - Date picker control icon error color.
*
* @cssprop --omni-date-picker-control-left-border-width - Date picker control left border width.
* @cssprop --omni-date-picker-control-left-border-color - Date picker control left border color.
*
* @cssprop --omni-date-picker-control-left-focused-border-width - Date picker control left border focused width.
* @cssprop --omni-date-picker-control-left-focused-color - Date picker control left border focused color.
*
* @cssprop --omni-date-picker-control-left-border-error-color - Date picker control left border error color.
*
* @cssprop --omni-date-picker-container-z-index - Date picker container z-index.
*
* @cssprop --omni-date-picker-mobile-picker-dialog-left - Date picker dialog left.
* @cssprop --omni-date-picker-mobile-picker-dialog-right - Date picker dialog right
* @cssprop --omni-date-picker-mobile-picker-dialog-bottom - Date picker dialog bottom
* @cssprop --omni-date-picker-mobile-picker-dialog-background-color - Date picker dialog background color.
*
* @cssprop --omni-date-picker-container-width - Date picker container width.
* @cssprop --omni-date-picker-container-top - Date picker container top.
* @cssprop --omni-date-picker-period-container-border-bottom - Date picker container border bottom.
*
* @cssprop --omni-date-picker-container-render-bottom-top - Date picker container render bottom top.
*
*/
@customElement('omni-date-picker')
export class DatePicker extends OmniFormElement {
@query('#inputField')
private _inputElement?: HTMLInputElement;
private defaultLocale: string = 'en-US';
/**
* The locale used for formatting the output of the Date time picker.
* @attr
*/
@property({ type: String, reflect: true }) locale: string = this.defaultLocale;
/**
* The minimum date inclusively allowed to be selected.
* @attr [min-date]
*/
@property({ type: String, attribute: 'min-date', reflect: true }) minDate?: string;
/**
* The maximum date inclusively allowed to be selected.
* @attr [max-date]
*/
@property({ type: String, attribute: 'max-date', reflect: true }) maxDate?: string;
// Internal state properties for date picker and
@state() private date: DateTime =
this.value && typeof this.value === 'string' ? DateTime.fromISO(this.value).setLocale(this.locale) : DateTime.local();
@state() private _showCalendar: boolean = false;
//Internal state properties for dimensions
@state() private _bottomOfViewport: boolean = false;
@state() private _isMobile: boolean = false;
private readonly _windowClickBound = this._windowClick.bind(this);
private readonly _checkForBottomOfScreenBound = this._checkForBottomOfScreen.bind(this);
private readonly _checkForMobileBound = this._checkforMobile.bind(this);
override connectedCallback() {
super.connectedCallback();
this.addEventListener('click', this._inputClick.bind(this));
window.addEventListener('click', this._windowClickBound);
}
protected override async firstUpdated(): Promise {
await this._checkForBottomOfScreen();
await this._checkforMobile();
window.addEventListener('resize', this._checkForBottomOfScreenBound);
window.addEventListener('scroll', this._checkForBottomOfScreenBound);
window.addEventListener('resize', this._checkForMobileBound);
window.addEventListener('scroll', this._checkForMobileBound);
}
override disconnectedCallback(): void {
super.disconnectedCallback();
window.removeEventListener('click', this._windowClickBound);
window.removeEventListener('resize', this._checkForBottomOfScreenBound);
window.removeEventListener('scroll', this._checkForBottomOfScreenBound);
window.removeEventListener('resize', this._checkForMobileBound);
window.removeEventListener('scroll', this._checkForMobileBound);
}
// Update properties of the Date picker component if user provides a value to the value property or if the locale property is updated.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected override shouldUpdate(_changedProperties: PropertyValueMap | Map): boolean {
if (_changedProperties.has('value')) {
this.date = DateTime.fromISO(this.value).setLocale(this.locale);
}
return true;
}
override focus(options?: FocusOptions | undefined): void {
if (this._inputElement) {
this._inputElement.focus(options);
} else {
super.focus(options);
}
}
// Check to see if the component is at the bottom of the viewport if true set the internal boolean value.
async _checkForBottomOfScreen() {
const distanceFromBottom = (visualViewport?.height as number) - this.getBoundingClientRect().bottom;
if (distanceFromBottom < 270) {
this._bottomOfViewport = true;
} else {
this._bottomOfViewport = false;
}
}
// Check the width of the screen to set the internal mobile boolean to true of false.
async _checkforMobile() {
if (!window.matchMedia ? window.innerWidth >= 767 : window.matchMedia('screen and (min-width: 767px)').matches) {
// Desktop width is at least 767px
this._isMobile = false;
} else {
// Mobile screen less than 767px
this._isMobile = true;
}
}
_inputClick(e: Event) {
if (this.disabled) {
e.preventDefault();
e.stopImmediatePropagation();
return;
}
const pickerContainer = this.renderRoot.querySelector('#picker-container');
const pickerDialog = this.renderRoot.querySelector('#picker-dialog');
//Check that the pickerContainer or pickerDialog is not loaded
if (
!e.composedPath() ||
!(pickerContainer || pickerDialog) ||
!(e.composedPath().includes(pickerContainer as Element) || e.composedPath().includes(pickerDialog as Element))
) {
this._toggleCalendar();
}
}
// https://stackoverflow.com/a/39245638
// Close the item container when clicking outside the date picker component.
_windowClick(e: Event) {
const pickerDialog = this.renderRoot.querySelector('#picker-dialog') as HTMLDialogElement;
const composedPath = e.composedPath();
/**
* Check when the window is clicked to close the container(Desktop) or dialog(Mobile)
* For mobile scenarios check if the dialog is the lowest item in the composed path
*/
if (
composedPath &&
(!composedPath.includes(this) || (this._isMobile && pickerDialog && composedPath.findIndex((p) => p === pickerDialog) === 0)) &&
this._showCalendar
) {
this._toggleCalendar();
}
}
_toggleCalendar() {
if (this._showCalendar) {
this._showCalendar = false;
if (this._isMobile) {
const pickerDialog = this.renderRoot.querySelector('#picker-dialog');
if (pickerDialog) {
pickerDialog.close();
}
}
} else {
this._showCalendar = true;
if (this._isMobile) {
const pickerDialog = this.renderRoot.querySelector('#picker-dialog');
if (pickerDialog) {
pickerDialog.showModal();
}
}
}
}
_dateSelected(e: Event) {
this.date = DateTime.fromJSDate((e).detail.date).setLocale(this.locale);
this.value = this.date.toISODate() as string;
this.dispatchEvent(
new CustomEvent('change', {
detail: {
date: this.date.toJSDate()
}
})
);
this._toggleCalendar();
}
static override get styles() {
return [
super.styles,
css`
/* Added to ensure that component has pointer cursor applied */
:host {
cursor: pointer;
}
.field {
flex: 1 1 auto;
border: none;
background: none;
box-shadow: none;
outline: 0;
padding: 0;
margin: 0;
text-align: var(--omni-date-picker-text-align, left);
color: var(--omni-date-picker-font-color, var(--omni-font-color));
font-family: var(--omni-date-picker-font-family, var(--omni-font-family));
font-size: var(--omni-date-picker-font-size, var(--omni-font-size));
font-weight: var(--omni-date-picker-font-weight, var(--omni-font-weight));
height: var(--omni-date-picker-height, 100%);
padding: var(--omni-date-picker-padding, 10px);
width: var(--omni-date-picker-width);
min-width: var(--omni-date-picker-min-width, 242px);
cursor: pointer;
}
.field.disabled {
color: var(--omni-date-picker-disabled-font-color, #7C7C7C);
}
.field.error {
color: var(--omni-date-picker-error-font-color, var(--omni-font-color));
}
/* Styles for the control and control icon */
.control {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
cursor: pointer;
padding: var(--omni-date-picker-control-padding, 10px 10px);
}
.control:hover {
background-color: var(--omni-date-picker-control-hover-color, var(--omni-accent-hover-color));
}
.control-icon,
::slotted([slot='calendar']){
width: var(--omni-date-picker-control-icon-width, 20px);
height: var(--omni-date-picker-control-icon-height, 20px);
fill: var(--omni-date-picker-control-icon-color, var(--omni-primary-color));
cursor: pointer;
}
.control-icon.error {
fill: var(--omni-date-picker-control-icon-error-color, var(--omni-error-font-color));
}
.left-border {
width: var(--omni-date-picker-control-left-border-width, 2px);
background-color: var(--omni-date-picker-control-left-border-color,var(--omni-form-border-color));
}
.layout:focus-within > .left-border {
width: var(--omni-date-picker-control-left-focused-border-width, 2px);
background-color: var(--omni-date-picker-control-left-focused-color, var(--omni-primary-color));
}
.left-border.error {
background-color: var(--omni-date-picker-control-left-border-error-color, var(--omni-error-font-color));
}
/* Styles related to the picker container*/
.picker-container {
z-index: var(--omni-date-picker-container-z-index, 420);
}
/* Picker dialog mobile*/
@media screen and (max-width: 766px) {
.picker-dialog {
position: fixed;
top: inherit;
width: 100%;
margin: unset;
border-style: none;
padding: unset;
left: var(--omni-date-picker-mobile-picker-dialog-left, 0px);
right: var(--omni-date-picker-mobile-picker-dialog-right, 0px);
bottom: var(--omni-date-picker-mobile-picker-dialog-bottom, 0px);
}
.picker-dialog:modal{
max-width: 100%;
overflow: none;
}
.picker-dialog::backdrop {
background: var(--omni-date-picker-mobile-picker-dialog-background-color, rgba(0, 0, 0, 0.1));
}
}
/* Desktop and landscape tablet device styling, if element is at the bottom of the screen make items render above the input */
@media screen and (min-width: 767px) {
.picker-container {
position: absolute;
cursor: default;
transition: 1s;
width: var(--omni-date-picker-container-width, 100%);
top: var(--omni-date-picker-container-top, 102%);
}
/* Styles if the element is at the bottom of the screen then render the picker on top of the element */
.picker-container.bottom {
top: var(--omni-date-picker-container-render-bottom-top, -2%);
transform: translateY(-100%);
}
}
`
];
}
protected override renderContent() {
const field: ClassInfo = {
field: true,
disabled: this.disabled,
error: this.error as string
};
return html`
`;
}
protected override renderControl() {
const border: ClassInfo = {
'left-border': true,
disabled: this.disabled,
error: this.error as string
};
const control: ClassInfo = {
control: true,
disabled: this.disabled,
error: this.error as string
};
const controlIcon: ClassInfo = {
'control-icon': true,
disabled: this.disabled,
error: this.error as string
};
return html`
`;
}
protected override renderPicker() {
if (this._isMobile) {
return html`
`;
}
if (!this._showCalendar) {
return nothing;
} else {
return html`
this._dateSelected(e)}>
`;
}
}
protected override renderLabel() {
return super.renderLabel(true);
}
}
declare global {
interface HTMLElementTagNameMap {
'omni-date-picker': DatePicker;
}
}