UNPKG

@availity/reactstrap-validation-date

Version:

Wrapper for react-date-range to work with availity-reactstrap-validation

441 lines (416 loc) 12.4 kB
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { Button, Popover, InputGroupAddon } from 'reactstrap'; import dayjs from 'dayjs'; import isBetween from 'dayjs/plugin/isBetween'; import customParseFormat from 'dayjs/plugin/customParseFormat'; import pick from 'lodash/pick'; import { inputType, isoDateFormat, } from 'availity-reactstrap-validation/lib/AvValidator/utils'; import { AvInput } from 'availity-reactstrap-validation'; import { DateRange } from 'react-date-range'; import Icon from '@availity/icon'; import AvDate from './AvDate'; dayjs.extend(isBetween); dayjs.extend(customParseFormat); let count = 0; const relativeRanges = { 'Last 7 Days': { startDate: now => now.add(-6, 'd'), endDate: now => now, }, 'Last 30 Days': { startDate: now => now.add(-29, 'd'), endDate: now => now, }, 'Last Calendar Month': { startDate: now => now.startOf('month').add(-1, 'M'), endDate: now => now.startOf('month').add(-1, 'd'), }, 'Last 120 Days': { startDate: now => now.add(-119, 'd'), endDate: now => now, }, 'Last 6 Months': { startDate: now => now.add(-6, 'M'), endDate: now => now, }, 'Last 12 Months': { startDate: now => now.add(-12, 'M'), endDate: now => now, }, }; const theme = { DateRange: { background: '#ffffff' }, Calendar: { color: '#4d4f53' }, MonthButton: { background: 'none' }, MonthArrowPrev: { borderRightColor: '#4d4f53' }, MonthArrowNext: { borderLeftColor: '#4d4f53' }, DaySelected: { background: '#2261b5', borderColor: '#143a6c', }, DayActive: { background: '#2261b5', boxShadow: 'none', }, DayInRange: { background: '#0093e8', color: '#fff', }, DayHover: { background: '#e8ebeb', color: '#4d4f53', }, }; export default class AvDateRange extends Component { static propTypes = { start: PropTypes.shape(AvInput.propTypes), end: PropTypes.shape(AvInput.propTypes), onChange: PropTypes.func, validate: PropTypes.object, type: PropTypes.string, disabled: PropTypes.bool, max: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date), PropTypes.instanceOf(dayjs), ]), min: PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.instanceOf(Date), PropTypes.instanceOf(dayjs), ]), distance: PropTypes.object, ranges: PropTypes.oneOfType([PropTypes.array, PropTypes.object]), defaultValues: PropTypes.object, theme: PropTypes.object, calendarIcon: PropTypes.node, hideIcon: PropTypes.bool, }; static contextTypes = { FormCtrl: PropTypes.object.isRequired }; static defaultProps = { type: 'text', calendarIcon: <Icon name="calendar" />, hideIcon: false, }; constructor(props, context) { super(props, context); const { getDefaultValue } = context.FormCtrl; this.state = { open: false, startValue: props.start.value, endValue: props.end.value, }; if (props.type.toLowerCase() === 'date' && inputType.date) { this.state.format = isoDateFormat; } else { this.state.format = (props.validate && props.validate.dateRange && props.validate.dateRange.format) || 'MM/DD/YYYY'; } if (props.defaultValues) { const { start, end } = props.defaultValues; if (getDefaultValue(props.start.name)) { this.state.startValue = getDefaultValue(props.start.name); } else if (start) { this.state.startValue = dayjs(new Date()) .add(start.value, start.units) .format(this.state.format); } if (getDefaultValue(props.end.name)) { this.state.endValue = getDefaultValue(props.end.name); } else if (end) { this.state.endValue = (end.fromStart ? dayjs(this.state.startValue, this.state.format) : dayjs(new Date()) ) .add(end.value, end.units) .format(this.state.format); } else { this.state.endValue = this.state.endValue || this.state.startValue; } } count += 1; this.guid = `date-range-${count}-btn`; } getStartRef = el => { this.startRef = el; if (this.props.start.innerRef) { if (typeof this.props.start.innerRef === 'function') { this.props.start.innerRef(el); } else { this.props.start.innerRef.current = el; } } }; getEndRef = el => { this.endRef = el; if (this.props.end.innerRef) { if (typeof this.props.end.innerRef === 'function') { this.props.end.innerRef(el); } else { this.props.end.innerRef.current = el; } } }; togglePicker = () => { this.setState(prevState => ({ open: !prevState.open })); }; onStartFieldChange = event => { const startValue = event && event.target ? event.target.value : event; this.setState({ startValue, open: false }, () => { if (this.props.onChange) { this.props.onChange(event, { start: startValue, end: this.state.endValue, }); } }); }; onEndFieldChange = event => { const endValue = event && event.target ? event.target.value : event; this.setState({ endValue, open: false }, () => { if (this.props.onChange) { this.props.onChange(event, { start: this.state.startValue, end: endValue, }); } }); }; toggle = () => { this.setState(prevState => ({ open: !prevState.open })); }; open = () => { if (!this.state.open) { this.setState({ open: true }); } }; close = () => { if (this.state.open) { this.setState({ open: false }); } }; validateDistance = end => { const start = this.context.FormCtrl.getInput( this.props.start.name ).getViewValue(); if (start && end && this.props.distance) { const mStart = dayjs(new Date(start)); const mEnd = dayjs(new Date(end)); if (!mStart.isValid() || !mEnd.isValid()) { return true; } const { max, min } = this.props.distance; if (max) { if (!mEnd.isBefore(mStart.add(max.value, max.units), 'day')) { return ( max.errorMessage || `The end date must be within ${max.value} ${ max.units } of the start date` ); } } if (min) { if (mEnd.isAfter(mStart.add(min.value, min.units), 'day')) { return ( min.errorMessage || `The end date must be greater than ${min.value} ${ min.units } of the start date` ); } } } return true; }; checkDistanceValidation = () => { this.context.FormCtrl.validate(this.props.end.name); }; syncStartEnd = event => { let endValue = this.context.FormCtrl.getInput(this.props.end.name).getViewValue() || this.state.startValue; if ( dayjs(this.state.startValue, this.state.format).isAfter( new Date(endValue) ) ) { endValue = this.state.startValue; } if (this.state.endValue !== endValue) { this.setState({ endValue }, () => { if (this.props.onChange) { this.props.onChange(event, { start: this.state.startValue, end: endValue, }); } }); } if (!this.context.FormCtrl.isTouched(this.props.end.name)) { this.context.FormCtrl.setTouched(this.props.end.name); } this.checkDistanceValidation(endValue, { [this.props.start.name]: this.state.startValue, }); }; syncEndStart = event => { let startValue = this.context.FormCtrl.getInput(this.props.start.name).getViewValue() || this.state.endValue; if ( dayjs(this.state.endValue, this.state.format).isBefore( new Date(startValue) ) ) { startValue = this.state.endValue; } if (this.state.startValue !== startValue) { this.setState({ startValue }, () => { if (this.props.onChange) { this.props.onChange(event, { start: startValue, end: this.state.endValue, }); } }); } if (!this.context.FormCtrl.isTouched(this.props.start.name)) { this.context.FormCtrl.setTouched(this.props.start.name); } }; onPickerChange = range => { if (!this.context.FormCtrl.isTouched(this.props.start.name)) { this.context.FormCtrl.setTouched(this.props.start.name); } if (!this.context.FormCtrl.isTouched(this.props.end.name)) { this.context.FormCtrl.setTouched(this.props.end.name); } const startValue = range.startDate.format(this.state.format); const endValue = range.endDate.format(this.state.format); this.setState( { startValue, endValue, open: false, }, () => { if (this.props.onChange) { this.props.onChange(range, { start: startValue, end: endValue, }); } this.checkDistanceValidation(endValue, { [this.props.start.name]: startValue, }); } ); }; render() { const endValidate = { ...this.props.validate, ...this.props.end.validate, }; if (this.props.distance) { endValidate.distance = this.validateDistance; } return ( <div className="input-group input-group-date-range"> <AvDate placeholder={this.state.format.toLowerCase()} type={this.props.type} className={this.props.disabled ? 'border-right-0' : undefined} {...this.props.start} innerRef={this.getStartRef} validate={{ ...this.props.validate, ...this.props.start.validate }} onChange={this.onStartFieldChange} value={this.state.startValue || ''} disabled={this.props.disabled} onBlur={this.syncStartEnd} aria-label="From Date" datepicker={false} /> <InputGroupAddon addonType="append" className="input-group-prepend input-group-addon-dash" > - </InputGroupAddon> <AvDate placeholder={this.state.format.toLowerCase()} type={this.props.type} className={this.props.disabled ? 'border-left-0' : undefined} {...this.props.end} innerRef={this.getEndRef} validate={endValidate} disabled={this.props.disabled} onChange={this.onEndFieldChange} value={this.state.endValue || ''} onBlur={this.syncEndStart} aria-label="To Date" datepicker={false} /> {!this.props.hideIcon && ( <InputGroupAddon addonType="append"> <Button id={this.guid} color="light" type="button" disabled={this.props.disabled} onClick={this.togglePicker} style={{ lineHeight: this.state.format === isoDateFormat ? '1.4' : undefined, zIndex: 'auto', }} > {this.props.calendarIcon} <span className="sr-only">Toggle Calendar</span> </Button> </InputGroupAddon> )} <Popover placement="top" target={this.guid} isOpen={this.state.open} toggle={this.toggle} className="popover-calendar-range" > <DateRange startDate={this.state.startValue} endDate={ dayjs(new Date(this.state.endValue)).isValid() ? this.state.endValue : null } maxDate={this.props.max} minDate={this.props.min} ranges={ // eslint-disable-next-line no-nested-ternary this.props.ranges !== undefined ? Array.isArray(this.props.ranges) ? pick(relativeRanges, this.props.ranges) : this.props.ranges : relativeRanges } onChange={this.onPickerChange} format={this.state.format} theme={this.props.theme || theme} twoStepChange linkedCalendars /> </Popover> </div> ); } }