import { html, nothing, PropertyValueMap, unsafeCSS } from "lit";
import { property, query, state } from "lit/decorators.js";
import eleStyle from "./f-date-time-picker.scss?inline";
import globalStyle from "./f-date-time-picker-global.scss?inline";
import { FRoot } from "../../mixins/components/f-root/f-root";
import flatpickr from "flatpickr";
import { Instance } from "flatpickr/dist/types/instance";
import { DateLimit, DateOption } from "flatpickr/dist/types/options";
import { FInput } from "../f-input/f-input";
import { FDiv } from "../f-div/f-div";
import { FText } from "../f-text/f-text";
import { flowElement } from "./../../utils";
import { injectCss } from "@cldcvr/flow-core-config";
injectCss("f-date-time-picker", globalStyle);
export type FDateTimePickerState = "primary" | "default" | "success" | "warning" | "danger";
export type DateValueType = DateOption | DateOption[];
export type DateDisableType = DateLimit[];
export type FDateOption = DateOption;
@flowElement("f-date-time-picker")
export class FDateTimePicker extends FRoot {
/**
* css loaded from scss file
*/
static styles = [
unsafeCSS(eleStyle),
unsafeCSS(globalStyle),
...FDiv.styles,
...FText.styles,
...FInput.styles
];
/**
* @attribute Variants are various visual representations of a date-time-picker field.
*/
@property({ reflect: true, type: String })
variant?: "curved" | "round" | "block" = "curved";
/**
* @attribute Categories are various visual representations of date-time-picker field.
*/
@property({ reflect: true, type: String })
category?: "fill" | "outline" | "transparent" = "fill";
/**
* @attribute The mode property specifies whether the picker will include both date and time or only one of them.
*/
@property({ reflect: true, type: String })
mode?: "date-time" | "date-only" | "time-only" = "date-time";
/**
* @attribute Defines the value of the field. Standard validation rules of date and time are applied on the value.
*/
@property({ reflect: true, type: Date })
value?: DateValueType;
/**
* @attribute Defines the placeholder text for f-date-time-picker
*/
@property({ reflect: true, type: String })
placeholder?: string;
/**
* @attribute The medium size is the default.
*/
@property({ reflect: true, type: String })
size?: "small" | "medium" = "medium";
/**
* @attribute The state helps in indicating the degree of emphasis. By default it is default state.
*/
@property({ reflect: true, type: String })
state?: FDateTimePickerState = "default";
/**
* @attribute Sets the minimum value of the date allowed in the picker
*/
@property({ reflect: true, type: Date })
["min-date"]?: FDateOption;
/**
* @attribute Sets the maximum value of the date allowed in the picker
*/
@property({ reflect: true, type: Date })
["max-date"]?: FDateOption;
/**
* @attribute Sets the certain dates unavailable. There can be multiple options:
1. Disabling specific date(s)
2. Disabling range of dates
3. Disabling dates using a function
*/
@property({ reflect: true, type: String })
["disable-date"]?: DateDisableType = [];
/**
* @attribute Displays a close icon-button on the right side of the input that allows the user to clear the input value
*/
@property({ reflect: true, type: Boolean })
clear?: boolean = false;
/**
* @attribute Allow a range of dates to be selected by choosing start-date and end-date
*/
@property({ reflect: true, type: Boolean })
["is-range"]?: boolean = false;
/**
* @attribute Display the date-time picker always in open state. Not acting as a popover now.
*/
@property({ reflect: true, type: Boolean })
inline?: boolean = false;
/**
* @attribute Shows the week numbers on the picker.
*/
@property({ reflect: true, type: Boolean })
["week-number"]?: boolean = false;
/**
* @attribute Shows the input field in disabled state. Opacity 50%
*/
@property({ reflect: true, type: Boolean })
disabled?: boolean = false;
/**
* @attribute Sets the input field to the loading state.
*/
@property({ reflect: true, type: Boolean })
loading?: boolean = false;
/**
* query selector for input field
*/
@query("f-input")
dateTimePickerElement!: FInput;
/**
* query selector for help slot
*/
@state()
dateParsingError: string | null = null;
/**
* flatpickr instance
*/
flatPickerElement?: Instance;
reqAniFrame?: number;
/**
* conditional placeholder
*/
get placeholderText() {
if (this.mode === "date-only") {
return "Select date";
} else if (this.mode === "time-only") {
return "Select time";
} else {
return "Select date and time";
}
}
/**
* regex for date-time validation on keyboard typing
*/
get regexDateTime() {
if (this.mode === "date-time") {
return /^(0?[1-9]|[12][0-9]|3[01])[/](0?[1-9]|1[012])[/-]\d{4} ([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
} else if (this.mode === "date-only") {
return /^(0?[1-9]|[12][0-9]|3[01])[/](0?[1-9]|1[012])[/-]\d{4}$/;
} else {
return /^([01]?[0-9]|2[0-3]):[0-5][0-9]$/;
}
}
/**
* validation message
*/
get dateValidationMessage() {
return `Please Enter a valid date format: ${
this.mode === "date-time"
? "DD/MM/YYYY HH:ii"
: this.mode === "date-only"
? "DD/MM/YYYY"
: "HH:ii"
}`;
}
/**
* close the date-picker on unmount
*/
disconnectedCallback() {
this.flatPickerElement?.close();
// clear request animation frame if any
if (this.reqAniFrame) {
cancelAnimationFrame(this.reqAniFrame);
}
super.disconnectedCallback();
}
/**
* emit input custom event
*/
handleInput(dateObj: object, dateStr?: string) {
this.dateTimePickerElement.state = this.state;
this.value = dateStr;
this.dispatchInputEvent(dateObj, dateStr);
}
/**
*
* @param dateObj Date as an object
* @param dateStr Date oin string format
*/
dispatchInputEvent(dateObj: object, dateStr?: string) {
const event = new CustomEvent("input", {
detail: {
value: dateObj,
date: dateStr
},
bubbles: true,
composed: true
});
this.dispatchEvent(event);
}
/**
*
* @param e custom-event value having date string
* @returns date object formed from string
*/
dateObjectFromString(e: CustomEvent<{ value: string }>) {
const dateTime = e.detail.value.split(" ");
const date = dateTime[0].split("/");
const time = dateTime[1].split(":");
const dateObjFormation = new Date(
Number(date[2]),
Number(date[1]),
Number(date[0]),
Number(time[0]),
Number(time[1])
);
return dateObjFormation;
}
/**
*
* @param e custom-event value having date string
*/
handleKeyboardInput(
e: CustomEvent<{ value: string | number | undefined; type: "clear" | "input" }>
) {
e.stopPropagation();
if (e.detail?.type === "clear") {
this.flatPickerElement?.clear();
} else {
if (String(e.detail.value).match(this.regexDateTime)) {
//@ts-expect-error value is confirmed to be a string
this.handleInput([this.dateObjectFromString(e)], String(e.detail.value));
this.dateParsingError = null;
} else {
this.dateParsingError = this.dateValidationMessage;
this.dateTimePickerElement.state = "danger";
}
this.dateTimePickerElement.inputElement.focus();
}
}
/**
*
* @param selectedDates selected date object array
* @param dateStr selected date string
*/
datePickerOnChange(selectedDates: Date[], dateStr: string) {
if (this["is-range"]) {
if (selectedDates.length === 2) {
this.handleInput(selectedDates, dateStr);
}
} else if (dateStr !== "") {
this.handleInput(selectedDates, dateStr);
} else if (dateStr === "") {
this.handleInput([], undefined);
}
this.dateParsingError = null;
}
/**
* creates date picker
* @param element element w.r.t which creation of date picker takes place
*/
createDateTimePicker(element: HTMLElement) {
//destroy if it is present
if (this.flatPickerElement) {
this.flatPickerElement.destroy();
this.flatPickerElement = undefined;
}
this.flatPickerElement = flatpickr(element, {
dateFormat:
this.mode === "date-time" ? "d/m/Y H:i" : this.mode === "date-only" ? "d/m/Y" : "H:i",
enableTime: this.mode === "date-time" || this.mode === "time-only",
noCalendar: this.mode === "time-only",
defaultDate: this.value,
mode: this["is-range"] ? "range" : "single",
minDate: this["min-date"],
maxDate: this["max-date"],
disable: this["disable-date"] && this["disable-date"]?.length > 0 ? this["disable-date"] : [],
inline: this.inline,
weekNumbers: this["week-number"],
onChange: (selectedDates, dateStr) => {
this.datePickerOnChange(selectedDates, dateStr);
},
nextArrow:
"",
prevArrow:
""
});
}
/**
* week-number border conditional styling
*/
addWeekNoStyle() {
if (this["week-number"]) {
(this.flatPickerElement?.rContainer as HTMLDivElement).style.borderLeft =
"1px solid var(--color-border-secondary";
}
}
render() {
return html`
{
this.flatPickerElement?.close();
this.dateTimePickerElement.inputElement.focus();
}}
@blur=${(e: FocusEvent) => {
if (this.flatPickerElement?.isOpen) {
e.stopPropagation();
}
}}
@input=${this.handleKeyboardInput}
?read-only=${this["is-range"]}
>
${this.dateParsingError
? html`${this.dateParsingError}`
: nothing}`;
}
protected updated(changedProperties: PropertyValueMap | Map) {
super.updated(changedProperties);
if (!this.inline) {
// clear request animation frame if any
if (this.reqAniFrame) {
cancelAnimationFrame(this.reqAniFrame);
}
this.reqAniFrame = requestAnimationFrame(() => {
this.createDateTimePicker(this.dateTimePickerElement.inputWrapperElement);
this.dateTimePickerElement.value = this.flatPickerElement?.input.value;
this.addWeekNoStyle();
});
} else {
this.createDateTimePicker(this.dateTimePickerElement);
}
}
}
/**
* Required for typescript
*/
declare global {
interface HTMLElementTagNameMap {
"f-date-time-picker": FDateTimePicker;
}
}