import React from "react" import moment, { Moment } from "moment" import { DateTime, loadLocale, Namespace, TempusDominus } from "@eonasdan/tempus-dominus" import "@eonasdan/tempus-dominus/dist/css/tempus-dominus.css" import { localization as arSALocalization, name as arSAName } from "@eonasdan/tempus-dominus/dist/locales/ar-SA" import { localization as arLocalization, name as arName } from "@eonasdan/tempus-dominus/dist/locales/ar" import { localization as caLocalization, name as caName } from "@eonasdan/tempus-dominus/dist/locales/ca" import { localization as csLocalization, name as csName } from "@eonasdan/tempus-dominus/dist/locales/cs" import { localization as deLocalization, name as deName } from "@eonasdan/tempus-dominus/dist/locales/de" import { localization as esLocalization, name as esName } from "@eonasdan/tempus-dominus/dist/locales/es" import { localization as fiLocalization, name as fiName } from "@eonasdan/tempus-dominus/dist/locales/fi" import { localization as frLocalization, name as frName } from "@eonasdan/tempus-dominus/dist/locales/fr" import { localization as hrLocalization, name as hrName } from "@eonasdan/tempus-dominus/dist/locales/hr" import { localization as hyLocalization, name as hyName } from "@eonasdan/tempus-dominus/dist/locales/hy" import { localization as itLocalization, name as itName } from "@eonasdan/tempus-dominus/dist/locales/it" import { localization as nlLocalization, name as nlName } from "@eonasdan/tempus-dominus/dist/locales/nl" import { localization as plLocalization, name as plName } from "@eonasdan/tempus-dominus/dist/locales/pl" import { localization as ptPTLocalization, name as ptPTName } from "@eonasdan/tempus-dominus/dist/locales/pt-PT" import { localization as roLocalization, name as roName } from "@eonasdan/tempus-dominus/dist/locales/ro" import { localization as ruLocalization, name as ruName } from "@eonasdan/tempus-dominus/dist/locales/ru" import { localization as skLocalization, name as skName } from "@eonasdan/tempus-dominus/dist/locales/sk" import { localization as slLocalization, name as slName } from "@eonasdan/tempus-dominus/dist/locales/sl" import { localization as srLatnLocalization, name as srLatnName } from "@eonasdan/tempus-dominus/dist/locales/sr-Latn" import { localization as srLocalization, name as srName } from "@eonasdan/tempus-dominus/dist/locales/sr" import { localization as trLocalization, name as trName } from "@eonasdan/tempus-dominus/dist/locales/tr" import { localization as ukLocalization, name as ukName } from "@eonasdan/tempus-dominus/dist/locales/uk" import { localization as zhCNLocalization, name as zhCNName } from "@eonasdan/tempus-dominus/dist/locales/zh-CN" import { localization as zhHKLocalization, name as zhHKName } from "@eonasdan/tempus-dominus/dist/locales/zh-HK" import { localization as zhMOLocalization, name as zhMOName } from "@eonasdan/tempus-dominus/dist/locales/zh-MO" import { localization as zhTWLocalization, name as zhTWName } from "@eonasdan/tempus-dominus/dist/locales/zh-TW" const localizations: { [key: string]: Namespace } = { [arSAName]: arSALocalization, [arName]: arLocalization, [caName]: caLocalization, [csName]: csLocalization, [deName]: deLocalization, [esName]: esLocalization, [fiName]: fiLocalization, [frName]: frLocalization, [hrName]: hrLocalization, [hyName]: hyLocalization, [itName]: itLocalization, [nlName]: nlLocalization, [plName]: plLocalization, [ptPTName]: ptPTLocalization, pt: ptPTLocalization, [roName]: roLocalization, [ruName]: ruLocalization, [skName]: skLocalization, [slName]: slLocalization, [srLatnName]: srLatnLocalization, [srName]: srLocalization, [trName]: trLocalization, [ukName]: ukLocalization, [zhCNName]: zhCNLocalization, [zhHKName]: zhHKLocalization, [zhMOName]: zhMOLocalization, [zhTWName]: zhTWLocalization } for (const locale in localizations) { // Fix locale that uses "2-digits" instead of "2-digit" if ((localizations[locale] as any).dayViewHeaderFormat.year === "2-digits") { (localizations[locale] as any).dayViewHeaderFormat.year = "2-digit" } // DOW abbreviations are too long in pt-PT if (locale === "pt") { (localizations[locale] as any).locale = "pt-BR" } } // Only enable locales that we've tested. const enabledLocales = ["en", "fr", "es", "pt"] // Load all enabled locales for (const locale of enabledLocales) { loadLocale({ localization: localizations[locale], name: locale }) } /** Applies a tested Tempus Dominus locale to a picker instance. */ export function applyTempusDominusLocale(control: TempusDominus, locale?: string) { if (locale && enabledLocales.includes(locale)) { control.locale(locale) } } /** Props for DateTimePickerComponent. */ export interface DateTimePickerComponentProps { /** Moment.js date format. */ format?: string /** True to show timepicker. Only used when no format is specified. */ timepicker?: boolean /** Show the today button. */ showTodayButton?: boolean /** Show the clear button. */ showClear?: boolean /** Callback on date change. */ onChange: (date: Moment | null) => void /** Called when a key is pressed in the input. */ onKeyDown?: (e: React.KeyboardEvent) => void /** Date as moment. */ date?: Moment | null /** Placeholder for the text input. */ placeholder?: string /** Locale code. Only tested locales are enabled. */ locale?: string /** True to disable the picker. */ disabled?: boolean } /** Shared Tempus Dominus based date/time picker. */ export default class DateTimePickerComponent extends React.Component { static defaultProps = { timepicker: false, showClear: false } private control: TempusDominus | null = null private container: HTMLDivElement | null = null private textRef: HTMLInputElement | null = null /** Focuses the text input. */ focus() { if (this.textRef) { this.textRef.focus() } } /** Gets the Moment.js format used for display and parsing. */ getFormat() { if (!this.props.format) { return this.props.timepicker ? "YYYY-MM-DD HH:mm:ss" : "YYYY-MM-DD" } return this.props.format } componentDidUpdate(prevProps: DateTimePickerComponentProps) { if (!this.control) { return } if (this.pickerOptionsChanged(prevProps)) { const current = this.getCurrentMoment() this.disposeControl() if (this.container) { const initialDate = this.props.date !== undefined ? this.getPropDate() : current this.createControl(this.container, initialDate) } return } if (prevProps.locale !== this.props.locale) { applyTempusDominusLocale(this.control, this.props.locale) } this.syncDateFromProps() } /** True when picker display options changed and the Tempus control must be recreated. */ private pickerOptionsChanged(prevProps: DateTimePickerComponentProps) { return ( prevProps.format !== this.props.format || prevProps.timepicker !== this.props.timepicker || prevProps.showClear !== this.props.showClear || prevProps.showTodayButton !== this.props.showTodayButton ) } /** Gets the currently selected moment from the Tempus control. */ private getCurrentMoment() { return this.control?.dates.lastPicked ? moment(this.control.dates.lastPicked) : null } /** The `date` prop, treating an invalid moment as no date. Tempus cannot render an Invalid Date. */ private getPropDate(): Moment | null { return this.props.date && this.props.date.isValid() ? this.props.date : null } /** Syncs the Tempus control selection from the controlled `date` prop. */ private syncDateFromProps() { if (!this.control) { return } const date = this.getPropDate() const current = this.control.dates.lastPicked ? moment(this.control.dates.lastPicked) : null if ((current && !date) || (!current && date) || (current && date && !current.isSame(date))) { // Tempus redraws the calendar header on every value change and throws when the popup is // open on the clock view, where no calendar grid exists. The value itself is already set // by then try { if (date) { this.control.dates.setValue(DateTime.convert(date.toDate())) } else { this.control.clear() } } catch (err) { console.warn("Date picker display update failed", err) } } } /** Creates the underlying Tempus Dominus control. */ private createControl(elem: HTMLDivElement, initialDate?: Moment | null) { const format = this.getFormat() this.control = new TempusDominus(elem, { display: { buttons: { clear: this.props.showClear, today: this.props.showTodayButton }, components: { date: format.includes("DD") || format.includes("ll") || format.includes("LL"), month: format.includes("MM") || format.includes("ll") || format.includes("LL"), year: true, decades: true, clock: format.includes("HH") || format.includes("lll") || format.includes("LLL"), hours: format.includes("HH") || format.includes("lll") || format.includes("LLL"), minutes: format.includes("mm") || format.includes("lll") || format.includes("LLL"), seconds: format.includes("ss") || format.includes("lll") || format.includes("LLL") }, icons: { time: "fas fa-clock", date: "fas fa-calendar", up: "fas fa-arrow-up", down: "fas fa-arrow-down", next: "fas fa-arrow-right", previous: "fas fa-arrow-left", today: "fas fa-calendar-check", clear: "fas fa-trash", close: "fas fa-times" }, theme: "light" }, localization: { hourCycle: "h23" } }) applyTempusDominusLocale(this.control, this.props.locale) this.control.dates.formatInput = (date) => date ? moment(date).format(format) : "" this.control.dates.setFromInput = (value, index) => { const parsedValue = moment(value, format) if (parsedValue.isValid()) { this.control!.dates.setValue(new DateTime(parsedValue.toDate()), index) this.control!.hide() } else { this.control!.dates.clear() } } if (initialDate) { this.control.dates.setFromInput(initialDate.format(format), 0) } this.control.subscribe(Namespace.events.change, e => { this.props.onChange(e.date ? moment(e.date) : null) }) } /** Disposes the underlying Tempus Dominus control. */ private disposeControl() { if (this.control) { this.control.dispose() this.control = null } } private inputRef = (elem: HTMLDivElement | null) => { if (elem) { this.container = elem this.createControl(elem, this.getPropDate()) } else { this.disposeControl() this.container = null } } render() { return (
{ this.textRef = c }} onChange={() => { }} onKeyDown={this.props.onKeyDown} />
) } }