import isNil from 'lodash/isNil'; import invoke from 'lodash/invoke'; import moment, { Moment } from 'moment'; import * as PropTypes from 'prop-types'; import * as React from 'react'; import isString from 'lodash/isString'; import { BasePickerOnChangeData, } from '../pickers/BasePicker'; import DayPicker from '../pickers/dayPicker/DayPicker'; import MonthPicker from '../pickers/monthPicker/MonthPicker'; import YearPicker from '../pickers/YearPicker'; import InputView from '../views/InputView'; import BaseInput, { BaseInputProps, BaseInputPropTypes, BaseInputState, DateRelatedProps, DateRelatedPropTypes, DisableValuesProps, DisableValuesPropTypes, EnableValuesProps, EnableValuesPropTypes, MinMaxValueProps, MinMaxValuePropTypes, MultimodeProps, MultimodePropTypes, MarkedValuesProps, MarkedValuesPropTypes, } from './BaseInput'; import { tick, } from '../lib'; import { buildValue, parseArrayOrValue, parseValue, dateValueToString, } from './parse'; import { getDisabledMonths, getDisabledYears, } from './shared'; type CalendarMode = 'year' | 'month' | 'day'; function getNextMode(currentMode: CalendarMode) { if (currentMode === 'year') { return 'month'; } if (currentMode === 'month') { return 'day'; } return 'year'; } function getPrevMode(currentMode: CalendarMode) { if (currentMode === 'day') { return 'month'; } if (currentMode === 'month') { return 'year'; } return 'day'; } export interface DateInputProps extends BaseInputProps, DateRelatedProps, MultimodeProps, DisableValuesProps, EnableValuesProps, MarkedValuesProps, MinMaxValueProps { /** Display mode to start. */ startMode?: CalendarMode; } export type DateInputOnChangeData = DateInputProps; interface DateInputState extends BaseInputState { mode: CalendarMode; year: number; month: number; date: number; } class DateInput extends BaseInput { /** * Component responsibility: * - parse input value * - handle underlying picker change */ public static readonly defaultProps = { ...BaseInput.defaultProps, dateFormat: 'DD-MM-YYYY', startMode: 'day', preserveViewMode: true, icon: 'calendar', }; public static readonly propTypes = { ...BaseInputPropTypes, ...DateRelatedPropTypes, ...MultimodePropTypes, ...DisableValuesPropTypes, ...EnableValuesPropTypes, ...MarkedValuesPropTypes, ...MinMaxValuePropTypes, ...{ /** Display mode to start. */ startMode: PropTypes.oneOf(['year', 'month', 'day']), }, }; constructor(props: DateInputProps) { super(props); const parsedValue = parseValue(props.value, props.dateFormat, props.localization); this.state = { mode: props.startMode, popupIsClosed: true, year: parsedValue ? parsedValue.year() : undefined, month: parsedValue ? parsedValue.month() : undefined, date: parsedValue ? parsedValue.date() : undefined, }; } public componentDidUpdate = (prevProps: DateInputProps) => { // update internal date if ``value`` prop changed and successuffly parsed if (prevProps.value !== this.props.value) { const parsed = parseValue(this.props.value, this.props.dateFormat, this.props.localization); if (parsed) { this.setState({ year: parsed.year(), month: parsed.month(), date: parsed.date(), }); } } } public render() { const { value, dateFormat, initialDate, disable, enable, maxDate, minDate, preserveViewMode, startMode, closable, markColor, marked, localization, onChange, ...rest } = this.props; return ( this.getPicker()} value={dateValueToString(value, dateFormat, localization)} /> ); } private parseInternalValue(): Moment { /* Creates moment instance from values stored in component's state (year, month, date) in order to pass this moment instance to underlying picker. Return undefined if none of these state fields has value. */ const { year, month, date, } = this.state; if (!isNil(year) || !isNil(month) || !isNil(date)) { return moment({ year, month, date }); } } private getPicker = () => { const { value, initialDate, dateFormat, disable, minDate, maxDate, enable, inline, marked, markColor, localization, tabIndex, pickerWidth, pickerStyle, } = this.props; const pickerProps = { isPickerInFocus: this.isPickerInFocus, isTriggerInFocus: this.isTriggerInFocus, inline, onCalendarViewMount: this.onCalendarViewMount, closePopup: this.closePopup, tabIndex, pickerWidth, pickerStyle, onChange: this.handleSelect, onHeaderClick: this.switchToPrevMode, initializeWith: buildValue(this.parseInternalValue(), initialDate, localization, dateFormat), value: buildValue(value, null, localization, dateFormat, null), enable: parseArrayOrValue(enable, dateFormat, localization), minDate: parseValue(minDate, dateFormat, localization), maxDate: parseValue(maxDate, dateFormat, localization), localization, }; const disableParsed = parseArrayOrValue(disable, dateFormat, localization); const markedParsed = parseArrayOrValue(marked, dateFormat, localization); const { mode } = this.state; if (mode === 'year') { return ( ); } if (mode === 'month') { return ( ); } return ; } private switchToNextModeUndelayed = (): void => { this.setState(({ mode }) => { return { mode: getNextMode(mode) }; }, this.onModeSwitch); } private switchToNextMode = (): void => { tick(this.switchToNextModeUndelayed); } private switchToPrevModeUndelayed = (): void => { this.setState(({ mode }) => { return { mode: getPrevMode(mode) }; }, this.onModeSwitch); } private switchToPrevMode = (): void => { tick(this.switchToPrevModeUndelayed); } private onFocus = (): void => { if (!this.props.preserveViewMode) { this.setState({ mode: this.props.startMode }); } } private handleSelect = (e, { value }: BasePickerOnChangeData) => { if (this.state.mode === 'day' && this.props.closable) { this.closePopup(); } this.setState((prevState) => { const { mode, } = prevState; if (mode === 'day') { const outValue = moment(value).format(this.props.dateFormat); invoke(this.props, 'onChange', e, { ...this.props, value: outValue }); } return { year: value.year, month: value.month, date: value.date, }; }, () => this.state.mode !== 'day' && this.switchToNextMode()); } /** Keeps internal state in sync with input field value. */ /* When value is set, set internal states based on its value. When value is empty/invalid, set internal states based on initialDate. When value and initalDate are empty/invalid, set internal states to undefined so that the picker will be reset (otherwise the picker will display wrongly when we manually delete the input) */ private onInputValueChange = (e, { value }) => { let year: number; let month: number; let date: number; const parsedValue = moment(value, this.props.dateFormat); if (parsedValue.isValid()) { year = parsedValue.year(); month = parsedValue.month(); date = parsedValue.date(); } else if (this.props.initialDate) { if (isString(this.props.initialDate)) { const parsedInitialDate = moment(this.props.initialDate, this.props.dateFormat); if (parsedInitialDate.isValid()) { year = parsedInitialDate.year(); month = parsedInitialDate.month(); date = parsedInitialDate.date(); } } else if (moment.isMoment(this.props.initialDate) && this.props.initialDate.isValid()) { year = this.props.initialDate.year(); month = this.props.initialDate.month(); date = this.props.initialDate.date(); } else if (moment.isDate(this.props.initialDate)) { year = this.props.initialDate.getFullYear(); month = this.props.initialDate.getMonth(); date = this.props.initialDate.getDate(); } } this.setState({ year, month, date, }); invoke(this.props, 'onChange', e, { ...this.props, value }); } } export default DateInput;