/** * Date picker component * * @author Fedorov Platon * @date 2021-06-25 */ import * as React from 'react'; import {POPOVER_BOUNDARIES} from '../../constants'; import {Calendar} from '../calendar/Calendar'; import {CalendarProps} from '../calendar/Calendar.types'; import {DropDown} from '../dropdown/DropDown'; import {safeInvoke} from '../../utils/safeInvoke'; import moment from 'moment'; import * as styles from './datePicker.m.scss'; import {Input} from '../input/Input'; import {AdditionalTemplates, IDateTemplate, TemplateDate} from './AdditionalTemplates'; import {parseNoSeparatedDate} from '../../utils/parseNoSeparatedDate'; const defaultProps = { mode: 'month', pickerType: 'date', showSuffix: true }; export { IDateTemplate }; export type DatePickerProps = Omit & { autoFocus?: boolean; allowClear?: boolean; errorMessage?: React.ReactNode; viewFormat: string; valueFormat: string; isReadOnly?: boolean; isDisabled?: boolean; placeholderInvalidDate?: string; placeholder?: string; mode?: 'month' | 'year'; showSuffix?: boolean; onBlur?: () => void; isOpen?: boolean; isUtc?: boolean; isDefaultOpen?: boolean; onVisibleChange?: (visible: boolean) => void; value?: string; defaultValue?: string; onChange?: (date: string) => void; pickerType: 'date' | 'dateTime'; additionalDateTemplates?: IDateTemplate[]; errorTimeMessage: string; errorDateMessage: string; } interface IState { isOpen: boolean; text: string; date: moment.Moment | undefined; time: string; dateIsValid: boolean; } export class DatePicker extends React.Component { static defaultProps = defaultProps; constructor (props: DatePickerProps) { super(props); this.state = { ...this.initialState, isOpen: props.isDefaultOpen || props.isOpen || false }; } input = React.createRef(); inputContainer = React.createRef(); inputTime = React.createRef(); calendar = React.createRef(); isPanelChanged = false; private getMoment (value?: string, valueFormat?: string): moment.Moment { return this.isUTC ? moment.utc(value, valueFormat) : moment(value, valueFormat); } private get isUTC () { return this.props.isUtc || false; } get viewFormat (): string { return this.props.viewFormat; } get valueFormat (): string { return this.props.valueFormat; } get viewMask (): string { return this.viewFormat.replace(/\w/g, '9'); } get viewEmptyMask (): string { return this.viewFormat.replace(/\w/g, '_'); } get initialState () { const props = this.props; let text = ''; let date: moment.Moment | undefined; if (props.value && props.value.length > 0) { const parsedDate = this.getMoment(props.value, props.valueFormat); if (parsedDate.isValid()) { text = parsedDate.format(this.viewFormat); date = parsedDate; } else { text = props.value; } } return { text: text, date: date, time: date ? date.format('HH:mm:ss') : '', dateIsValid: Boolean(date?.isValid()) }; } override componentDidUpdate (prevProps:Readonly, prevState:Readonly, snapshot?:any) { if (this.props.value !== prevProps.value) { this.setState({...this.initialState}); } } isValidRange = (date: moment.Moment) => { if (!this.props.validRange) { return true; } const validRange = this.props.validRange; return date.diff(validRange[0]) >= 0 && date.diff(validRange[1]) <= 0; } closeDropDown = () => { const text = this.state.text; const isOpen = this.props.isOpen === undefined ? this.state.isOpen : this.props.isOpen; if (text === this.viewEmptyMask) { this.onChange(undefined); } else { this.onChange(this.state.date); } if (isOpen) { safeInvoke(this.props.onVisibleChange, false); } } openDropDown = () => { this.setState({isOpen: true}); safeInvoke(this.props.onVisibleChange, true); } onKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'Tab': { this.closeDropDown(); break; } case 'Enter': { if (this.state.isOpen) { this.closeDropDown(); } else { this.openDropDown(); } break; } } } onInputFocus = () => { const isClose = this.props.isOpen === false || this.state.isOpen === false; if (isClose) { this.openDropDown(); } } onInputBlur = () => { if (this.state.isOpen === false) { safeInvoke(this.props.onBlur); } } onInputChange = (e: React.ChangeEvent) => { if (e.type === 'blur') { e.stopPropagation(); e.preventDefault(); return; } const text = e.currentTarget.value; const date = parseNoSeparatedDate(text, this.viewFormat, this.isUTC); if (date && date.isValid() && (this.props.disabledDate === undefined || !this.props.disabledDate(date) && this.isValidRange(date)) ) { this.setState({ date: date, time: date ? date.format('HH:mm:ss') : '', dateIsValid: true, isOpen: true, text: text }); } else { this.setState({ dateIsValid: false, isOpen: true, text: text }); } } onDropDownClose = (e: React.SyntheticEvent | Event) => { const isEsc = e instanceof KeyboardEvent; if (isEsc || (this.inputContainer.current && e.target instanceof Node && !this.inputContainer.current.contains(e.target)) ) { this.closeDropDown(); if (!isEsc) { safeInvoke(this.props.onBlur); } else { this.input.current?.focus(); } } } onChange = (date?: moment.Moment, closePopup: boolean = true) => { if (this.state.time === '') { date?.startOf('date'); } this.setState((prevState) => { return { isOpen: !closePopup ? prevState.isOpen : false, date: date, text: date ? date.format(this.viewFormat) : '', time: date ? date.format('HH:mm:ss') : '', dateIsValid: true }; }); const setDate = date ? date.format(this.valueFormat) : ''; if (this.props.value !== setDate) { safeInvoke(this.props.onChange, date ? date.format(this.valueFormat) : ''); } } onCalendarSelect = (date: moment.Moment) => { let isDisabled: boolean = false; if (this.props.disabledDate) { isDisabled = this.props.disabledDate(date); } if (!isDisabled) { this.onChange(date, !this.isPanelChanged); } if (!this.isPanelChanged) { this.input.current?.focus(); safeInvoke(this.props.onVisibleChange, false); } this.isPanelChanged = false; } onCalendarPanelChange = (date: moment.Moment, mode: 'year' | 'month') => { this.isPanelChanged = true; safeInvoke(this.props.onPanelChange, date, mode); } onCalendarClose = () => { this.input.current?.focus(); this.closeDropDown(); } onAdditionalTemplateClick = (dateArg: TemplateDate) => { let date: moment.Moment | string | undefined; if (Array.isArray(dateArg)) { date = dateArg[0]; } else { date = dateArg; } this.input.current?.focus(); if (typeof date === 'string') { this.onChange(this.getMoment(date, this.valueFormat)); } else { this.onChange(date); } safeInvoke(this.props.onVisibleChange, false); } onTimeInputBlur = () => { const value = this.state.time; if (value === '__:__:__' && this.state.date) { const date = this.state.date.clone(); date.hours(0); date.minutes(0); date.seconds(0); this.onChange(date, false); return; } if (/^(([01][0-9])|(2[0-3])):[0-5][0-9]:[0-5][0-9]$/.test(value)) { const [h, m, s] = value.split(':').map((item) => parseInt(item)); if (this.state.date === undefined) { this.onChange(this.getMoment(value, 'HH:mm:ss'), false); return; } const date = this.state.date.clone(); if (date.hours() !== h || date.minutes() !== m || date.seconds() !== s) { date.hours(h); date.minutes(m); date.seconds(s); this.onChange(date, false); } } else { const date = this.state.date; this.setState({ time: date ? date.format('HH:mm:ss') : '' }); } } onTimeInputKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { this.inputTime.current?.blur(); this.input.current?.focus(); this.setState({isOpen: false}); } } onTimeInputChange = (e: React.ChangeEvent) => { this.setState({time: e.target.value}); } override render () { const { value, autoFocus, allowClear, errorMessage, isReadOnly, isDisabled, placeholder, placeholderInvalidDate, viewFormat, valueFormat, onChange, errorTimeMessage, ...props } = this.props; const isOpen = this.props.isOpen === undefined ? this.state.isOpen : this.props.isOpen; let defaultValue: moment.Moment | undefined = this.props.defaultValue ? this.getMoment(this.props.defaultValue) : undefined; let date: moment.Moment | undefined; let text: string = ''; // we need to fixed types in data record attributes (there could be null in value) if (value === undefined || value === null) { date = this.state.date; text = this.state.text; } else { if (document.activeElement === this.input.current || isOpen) { text = this.state.text; date = this.state.date; } else if (value !== '') { date = this.getMoment(value, this.valueFormat); text = date.format(this.viewFormat); } } const isValid = date === undefined || (date.isValid() && this.state.dateIsValid); const additionalTemplates: IDateTemplate[] = this.props.additionalDateTemplates ? this.props.additionalDateTemplates : []; const dateIsInvalid = !isValid ? this.props.errorDateMessage : ''; const errMessage = errorMessage ? errorMessage : dateIsInvalid; return (
)} isOpen={isOpen} boundaries={POPOVER_BOUNDARIES.DOCUMENT} onClose={this.onDropDownClose} isDisabled={isDisabled || isReadOnly} >
{ this.props.pickerType === 'dateTime' && (
) }
); } }