UNPKG

19 kBJavaScriptView Raw
1import React, { Component } from 'react';
2import PropTypes from 'prop-types';
3import { InputGroup, Button } from 'reactstrap';
4import moment from 'moment';
5import pick from 'lodash.pick';
6import {
7 inputType,
8 isoDateFormat,
9} from 'availity-reactstrap-validation/lib/AvValidator/utils';
10import { AvInput } from 'availity-reactstrap-validation';
11import { DateRangePicker } from 'react-dates';
12import 'react-dates/initialize';
13import classNames from 'classnames';
14import Icon from '@availity/icon';
15import { isOutsideRange, limitPropType, isSameDay } from './utils';
16import '../polyfills';
17
18let count = 0;
19
20const relativeRanges = {
21 Today: {
22 startDate: now => now,
23 endDate: now => now,
24 },
25 'Last 7 Days': {
26 startDate: now => now.add(-6, 'd'),
27 endDate: now => now,
28 },
29 'Last 30 Days': {
30 startDate: now => now.add(-29, 'd'),
31 endDate: now => now,
32 },
33 'Last 120 Days': {
34 startDate: now => now.add(-119, 'd'),
35 endDate: now => now,
36 },
37 'Last 6 Months': {
38 startDate: now => now.add(-6, 'M'),
39 endDate: now => now,
40 },
41 'Last 12 Months': {
42 startDate: now => now.add(-12, 'M'),
43 endDate: now => now,
44 },
45};
46
47class AvDateRange extends Component {
48 calendarIconRef = React.createRef();
49
50 constructor(props, context) {
51 super(props, context);
52 const { getDefaultValue } = context.FormCtrl;
53 this.state = {
54 open: false,
55 startValue: props.start.value,
56 endValue: props.end.value,
57 };
58 if (props.type.toLowerCase() === 'date' && inputType.date) {
59 this.state.format = isoDateFormat;
60 } else {
61 this.state.format =
62 (props.validate &&
63 props.validate.dateRange &&
64 props.validate.dateRange.format) ||
65 'MM/DD/YYYY';
66 }
67 if (props.defaultValues) {
68 const { start, end } = props.defaultValues;
69 if (getDefaultValue(props.start.name)) {
70 this.state.startValue = getDefaultValue(props.start.name);
71 } else if (start) {
72 this.state.startValue = moment(new Date())
73 .add(start.value, start.units)
74 .format(this.state.format);
75 }
76 if (getDefaultValue(props.end.name)) {
77 this.state.endValue = getDefaultValue(props.end.name);
78 } else if (end) {
79 this.state.endValue = (end.fromStart
80 ? moment(this.state.startValue, this.state.format)
81 : moment(new Date())
82 )
83 .add(end.value, end.units)
84 .format(this.state.format);
85 } else {
86 this.state.endValue = this.state.endValue || this.state.startValue;
87 }
88 }
89 count += 1;
90 this.guid = `date-range-${count}-btn`;
91 }
92
93 static getDerivedStateFromProps(
94 { start, end },
95 { startValue, endValue, prevStartProp, prevEndProp, format }
96 ) {
97 const newState = {};
98
99 // ensure date values are valid and convert to common format
100 const startMoment = moment(
101 startValue,
102 [isoDateFormat, format, 'MMDDYYYY', 'YYYYMMDD'],
103 true
104 );
105 const endMoment = moment(
106 endValue,
107 [isoDateFormat, format, 'MMDDYYYY', 'YYYYMMDD'],
108 true
109 );
110
111 startValue = startMoment.isValid() && startMoment.format('MM/DD/YYYY');
112 endValue = endMoment.isValid() && endMoment.format('MM/DD/YYYY');
113
114 // evaluate input dates against prop dates
115 if (start.value !== undefined && start.value !== startValue) {
116 newState.startValue = startValue;
117 }
118
119 if (end.value !== undefined && end.value !== endValue) {
120 newState.endValue = endValue;
121 }
122
123 // override if prop date change detected
124 if (prevStartProp !== start.value) {
125 newState.startValue = start.value;
126 newState.prevStartProp = start.value;
127 }
128
129 if (prevEndProp !== end.value) {
130 newState.endValue = end.value;
131 newState.prevEndProp = end.value;
132 }
133
134 return Object.keys(newState).length > 0 ? newState : null;
135 }
136
137 open = () => {
138 if (!this.state.open) {
139 this.setState({ open: true });
140 }
141 };
142
143 close = () => {
144 if (this.state.open) {
145 this.setState({ open: false });
146 }
147 };
148
149 getDateValue = value => {
150 const { format } = this.state;
151 const date = moment(
152 value,
153 [isoDateFormat, format, 'MMDDYYYY', 'YYYYMMDD'],
154 true
155 );
156 if (date.isValid()) return date;
157 return null;
158 };
159
160 validateDistance = () => {
161 const start = this.context.FormCtrl.getInput(
162 this.props.start.name
163 ).getViewValue();
164
165 // We want the view value so not calling from args
166 const end = this.context.FormCtrl.getInput(
167 this.props.end.name
168 ).getViewValue();
169
170 if (start && end && this.props.distance) {
171 const mStart = moment(new Date(start));
172 const mEnd = moment(new Date(end));
173 if (!mStart.isValid() || !mEnd.isValid()) {
174 return true;
175 }
176 const { max, min } = this.props.distance;
177 if (max) {
178 if (mEnd.isAfter(moment(mStart).add(max.value, max.units), 'day')) {
179 return (
180 max.errorMessage ||
181 `The end date must be within ${max.value} ${max.units}${
182 max.value > 1 ? 's' : ''
183 } of the start date`
184 );
185 }
186 }
187 if (min) {
188 if (mEnd.isBefore(mStart.add(min.value, min.units), 'day')) {
189 return (
190 min.errorMessage ||
191 `The end date must be greater than ${min.value} ${min.units}${
192 min.value > 1 ? 's' : ''
193 } of the start date`
194 );
195 }
196 }
197 }
198 return true;
199 };
200
201 onDatesChange = async ({ startDate, endDate }) => {
202 const { format } = this.state;
203 const { start, end, onChange } = this.props;
204
205 const _startDate =
206 (startDate && startDate.format(format)) || this.state.startValue;
207 const _endDate = (endDate && endDate.format(format)) || this.state.endValue;
208
209 if (startDate !== null) {
210 this.context.FormCtrl.getInput(start.name)
211 .getValidatorProps()
212 .onChange(_startDate);
213 }
214
215 if (endDate !== null) {
216 this.context.FormCtrl.getInput(end.name)
217 .getValidatorProps()
218 .onChange(_endDate);
219 }
220
221 this.setState(
222 {
223 startValue: _startDate,
224 endValue: _endDate,
225 },
226 () => {
227 if (onChange) {
228 onChange({
229 start: _startDate,
230 end: _endDate,
231 });
232 }
233
234 if (startDate) {
235 this.context.FormCtrl.validate(start.name);
236 }
237
238 if (endDate) {
239 this.context.FormCtrl.validate(end.name);
240 }
241 }
242 );
243 };
244
245 // For updating when we delete the current input
246 onInputChange = async val => {
247 const { onChange, start, end } = this.props;
248 const { focusedInput, format, startValue, endValue } = this.state;
249 const isStart = focusedInput === 'startDate';
250 const date = moment(
251 val,
252 [isoDateFormat, format, 'MMDDYYYY', 'YYYYMMDD'],
253 true
254 );
255
256 const valueToSet = date.isValid() ? date.format(isoDateFormat) : null;
257
258 this.context.FormCtrl.getInput(isStart ? start.name : end.name)
259 .getValidatorProps()
260 .onChange(valueToSet);
261
262 this.setState(
263 {
264 [isStart ? 'startValue' : 'endValue']: valueToSet,
265 },
266 () => {
267 if (onChange) {
268 onChange({
269 start: isStart ? valueToSet : startValue,
270 end: !isStart ? valueToSet : endValue,
271 });
272 }
273
274 if (isStart && date.isValid()) {
275 this.context.FormCtrl.validate(start.name);
276 this.setState({
277 focusedInput: 'endDate',
278 });
279 } else if (!isStart && date.isValid()) {
280 // this.context.FormCtrl.validate(end.name);
281 this.setState({
282 focusedInput: undefined,
283 });
284 this.context.FormCtrl.setTouched(end.name);
285 }
286 }
287 );
288 };
289
290 syncDates = () => {
291 const { start, end } = this.props;
292 const startTouched = this.context.FormCtrl.isTouched(start.name);
293 const endTouched = this.context.FormCtrl.isTouched(end.name);
294
295 if (!startTouched || !endTouched) {
296 const { startValue, endValue } = this.state;
297 if (!startValue && endValue) {
298 this.setState({ startValue: endValue });
299 this.context.FormCtrl.setTouched(start.name);
300 } else if (startValue && !endValue) {
301 this.setState({ endValue: startValue });
302 this.context.FormCtrl.setTouched(end.name);
303 }
304 }
305 };
306
307 onFocusChange = input => {
308 const { onPickerFocusChange, start, end, autoSync } = this.props;
309 if (autoSync) {
310 this.syncDates();
311 }
312
313 if (input === 'endDate') {
314 this.context.FormCtrl.setTouched(start.name);
315 } else if (!input) {
316 if (!this.context.FormCtrl.isTouched(end.name)) {
317 this.context.FormCtrl.setTouched(end.name);
318 }
319
320 if (!this.context.FormCtrl.isTouched(start.name)) {
321 this.context.FormCtrl.setTouched(start.name);
322 }
323
324 this.context.FormCtrl.validate(start.name);
325 this.context.FormCtrl.validate(end.name);
326 }
327
328 this.setState(
329 {
330 focusedInput: input,
331 },
332 () => {
333 if (onPickerFocusChange) onPickerFocusChange({ focusedInput: input });
334 }
335 );
336 };
337
338 valueParser = value => {
339 if (this.state.format === isoDateFormat) return value;
340 const date = moment(
341 value,
342 [this.state.format, 'MMDDYYYY', 'YYYYMMDD'],
343 true
344 );
345 if (date.isValid()) return date.format(isoDateFormat);
346 return value;
347 };
348
349 valueFormatter = value => {
350 const date = moment(
351 value,
352 [isoDateFormat, this.state.format, 'MMDDYYYY', 'YYYYMMDD'],
353 true
354 );
355 if (date.isValid()) return date.format(this.state.format);
356 return value;
357 };
358
359 afterStartValidate = () => {
360 const start = this.context.FormCtrl.getInput(
361 this.props.start.name
362 ).getViewValue();
363
364 // We want the view value so not calling from args
365 const end = this.context.FormCtrl.getInput(
366 this.props.end.name
367 ).getViewValue();
368
369 const hasStart = start && start !== '';
370 const hasEnd = end && end !== '';
371
372 if (hasStart && hasEnd) {
373 const mStart = moment(new Date(start));
374 const mEnd = moment(new Date(end));
375 if (!mStart.isValid() || !mEnd.isValid()) {
376 return true;
377 }
378
379 if (mStart.isAfter(mEnd)) {
380 return 'Start Date must come before End Date.';
381 }
382 }
383 return true;
384 };
385
386 getInputState = () => {
387 const startValidation = this.context.FormCtrl.getInputState(
388 this.props.start.name
389 );
390 if (startValidation.errorMessage) return startValidation;
391 const endValidation = this.context.FormCtrl.getInputState(
392 this.props.end.name
393 );
394 return endValidation;
395 };
396
397 requireStartIfEnd = () => {
398 const start = this.context.FormCtrl.getInput(
399 this.props.start.name
400 ).getViewValue();
401
402 // We want the view value so not calling from args
403 const end =
404 this.context.FormCtrl.getInput(this.props.end.name) &&
405 this.context.FormCtrl.getInput(this.props.end.name).getViewValue();
406
407 const hasStart = start && start !== '';
408 const hasEnd = end && end !== '';
409
410 if (!hasStart && hasEnd) {
411 return 'Both start and end date are required.';
412 }
413
414 return true;
415 };
416
417 requireEndIfStart = () => {
418 const start = this.context.FormCtrl.getInput(
419 this.props.start.name
420 ).getViewValue();
421
422 // We want the view value so not calling from args
423 const end = this.context.FormCtrl.getInput(
424 this.props.end.name
425 ).getViewValue();
426
427 const hasStart = start && start !== '';
428 const hasEnd = end && end !== '';
429
430 if (hasStart && !hasEnd) {
431 return 'Both start and end date are required.';
432 }
433
434 return true;
435 };
436
437 renderDateRanges = () => {
438 const { ranges: propsRanges } = this.props;
439 const { startValue, endValue, format } = this.state;
440
441 let ranges;
442 if (typeof propsRanges === 'boolean' && propsRanges) {
443 ranges = relativeRanges;
444 } else if (propsRanges) {
445 ranges = Array.isArray(propsRanges)
446 ? pick(relativeRanges, propsRanges)
447 : propsRanges;
448 }
449
450 return ranges ? (
451 <div className="d-flex flex-column ml-2 mt-2">
452 {Object.keys(ranges).map(text => {
453 const { startDate: startDateFunc, endDate: endDateFunc } = ranges[
454 text
455 ];
456
457 const presetStartDate = startDateFunc(moment());
458 const presetEndDate = endDateFunc(moment());
459
460 const isSelected =
461 isSameDay(
462 presetStartDate,
463 moment(startValue, [
464 isoDateFormat,
465 format,
466 'MMDDYYYY',
467 'YYYYMMDD',
468 ])
469 ) &&
470 isSameDay(
471 presetEndDate,
472 moment(endValue, [isoDateFormat, format, 'MMDDYYYY', 'YYYYMMDD'])
473 );
474 return (
475 <Button
476 key={text}
477 className="mt-1 mb-1"
478 color={isSelected ? 'primary' : 'default'}
479 size="sm"
480 onClick={() => {
481 this.onDatesChange({
482 startDate: presetStartDate,
483 endDate: presetEndDate,
484 });
485
486 this.setState({ focusedInput: undefined });
487 this.context.FormCtrl.setTouched(this.props.start.name);
488 this.context.FormCtrl.setTouched(this.props.end.name);
489
490 // // Focucs the calendar icon once clicked because we don't
491 // // want to get back in the loop of opening the calendar
492 this.calendarIconRef.current.parentElement.focus();
493 }}
494 >
495 {text}
496 </Button>
497 );
498 })}
499 </div>
500 ) : null;
501 };
502
503 render() {
504 const {
505 name,
506 className,
507 id,
508 min,
509 max,
510 calendarIcon,
511 datepicker,
512 validate,
513 distance,
514 ...attributes
515 } = this.props;
516 const { startValue, endValue, focusedInput } = this.state;
517 const endValidate = {
518 afterStart: this.afterStartValidate,
519 requireEndIfStart: this.requireEndIfStart,
520 ...validate,
521 ...this.props.end.validate,
522 };
523
524 const startValidate = {
525 requireStartIfEnd: this.requireStartIfEnd,
526 ...validate,
527 ...this.props.start.validate,
528 };
529 if (distance) {
530 endValidate.distance = this.validateDistance;
531 }
532
533 const minDate = validate && validate.min ? validate.min.value : min;
534 const maxDate = validate && validate.max ? validate.max.value : max;
535
536 const startId = `${(id || name).replace(/[^a-zA-Z0-9]/gi, '')}-start`;
537
538 const endId = `${(id || name).replace(/[^a-zA-Z0-9]/gi, '')}-end`;
539
540 const touched =
541 this.context.FormCtrl.isTouched(this.props.start.name) &&
542 this.context.FormCtrl.isTouched(this.props.end.name);
543 const hasError =
544 this.context.FormCtrl.hasError(this.props.start.name) ||
545 this.context.FormCtrl.hasError(this.props.end.name);
546 const isDirty =
547 this.context.FormCtrl.isDirty(this.props.start.name) ||
548 this.context.FormCtrl.isDirty(this.props.end.name);
549 const isBad =
550 this.context.FormCtrl.isBad(this.props.start.name) ||
551 this.context.FormCtrl.isBad(this.props.end.name);
552
553 const validation = this.getInputState();
554
555 const classes = classNames(
556 className,
557 touched ? 'is-touched' : 'is-untouched',
558 isDirty ? 'is-dirty' : 'is-pristine',
559 isBad ? 'is-bad-input' : null,
560 hasError ? 'av-invalid' : 'av-valid',
561 validation.error && 'is-invalid',
562 !startValue && !endValue && 'current-day-highlight',
563 datepicker && 'av-calendar-show'
564 );
565
566 return (
567 <>
568 <AvInput
569 style={{ display: 'none' }}
570 {...this.props.start}
571 validate={{
572 date: true,
573 ...startValidate,
574 }}
575 value={this.state.startValue || ''}
576 type="text"
577 min={minDate}
578 max={maxDate}
579 valueFormatter={this.valueFormatter}
580 valueParser={this.valueParser}
581 />
582 <AvInput
583 style={{ display: 'none' }}
584 {...this.props.end}
585 validate={{
586 date: true,
587 ...endValidate,
588 }}
589 value={this.state.endValue || ''}
590 min={minDate}
591 max={maxDate}
592 valueFormatter={this.valueFormatter}
593 valueParser={this.valueParser}
594 />
595 <InputGroup
596 disabled={attributes.disabled}
597 className={classes}
598 onChange={({ target }) => {
599 const val = target.value;
600 if (target.id === startId || target.id === endId) {
601 this.onInputChange(val);
602 }
603 }}
604 data-testid={`date-range-input-group-${name}`}
605 >
606 <DateRangePicker
607 disabled={attributes.disabled}
608 enableOutsideDays
609 startDate={this.getDateValue(startValue)}
610 startDateId={startId}
611 endDate={this.getDateValue(endValue)}
612 endDateId={endId}
613 calendarInfoPosition="before"
614 renderCalendarInfo={this.renderDateRanges}
615 onDatesChange={this.onDatesChange}
616 focusedInput={focusedInput}
617 onFocusChange={this.onFocusChange}
618 isOutsideRange={isOutsideRange(minDate, maxDate, this.state.format)}
619 customInputIcon={
620 datepicker
621 ? React.cloneElement(calendarIcon, {
622 ref: this.calendarIconRef,
623 onClick: () => {
624 const { focusedInput } = this.state;
625 if (focusedInput) {
626 this.setState({ focusedInput: undefined });
627 }
628 },
629 })
630 : undefined
631 }
632 inputIconPosition="after"
633 customArrowIcon="-"
634 showDefaultInputIcon={datepicker}
635 onClose={this.onClose}
636 numberOfMonths={2}
637 minimumNights={0}
638 {...attributes}
639 />
640 </InputGroup>
641 </>
642 );
643 }
644}
645
646AvDateRange.propTypes = {
647 ...AvInput.propTypes,
648 start: PropTypes.shape(AvInput.propTypes),
649 end: PropTypes.shape(AvInput.propTypes),
650 onChange: PropTypes.func,
651 validate: PropTypes.object,
652 type: PropTypes.string,
653 disabled: PropTypes.bool,
654 max: limitPropType,
655 min: limitPropType,
656 distance: PropTypes.object,
657 ranges: PropTypes.oneOfType([
658 PropTypes.bool,
659 PropTypes.array,
660 PropTypes.object,
661 ]),
662 onPickerFocusChange: PropTypes.func,
663 defaultValues: PropTypes.object,
664 calendarIcon: PropTypes.node,
665 datepicker: PropTypes.bool,
666 autoSync: PropTypes.bool,
667};
668
669AvDateRange.contextTypes = { FormCtrl: PropTypes.object.isRequired };
670
671AvDateRange.defaultProps = {
672 type: 'text',
673 calendarIcon: <Icon name="calendar" />,
674 datepicker: true,
675};
676
677export default AvDateRange;
678
\No newline at end of file