UNPKG

38.4 kBJSXView Raw
1import React from 'react';
2import PropTypes from 'prop-types';
3import momentPropTypes from 'react-moment-proptypes';
4import { forbidExtraProps, mutuallyExclusiveProps, nonNegativeInteger } from 'airbnb-prop-types';
5import moment from 'moment';
6import values from 'object.values';
7import isTouchDevice from 'is-touch-device';
8
9import { DayPickerPhrases } from '../defaultPhrases';
10import getPhrasePropTypes from '../utils/getPhrasePropTypes';
11
12import isInclusivelyAfterDay from '../utils/isInclusivelyAfterDay';
13import isNextDay from '../utils/isNextDay';
14import isSameDay from '../utils/isSameDay';
15import isAfterDay from '../utils/isAfterDay';
16import isBeforeDay from '../utils/isBeforeDay';
17
18import getVisibleDays from '../utils/getVisibleDays';
19import isDayVisible from '../utils/isDayVisible';
20
21import getSelectedDateOffset from '../utils/getSelectedDateOffset';
22
23import toISODateString from '../utils/toISODateString';
24import { addModifier, deleteModifier } from '../utils/modifiers';
25
26import DisabledShape from '../shapes/DisabledShape';
27import FocusedInputShape from '../shapes/FocusedInputShape';
28import ScrollableOrientationShape from '../shapes/ScrollableOrientationShape';
29import DayOfWeekShape from '../shapes/DayOfWeekShape';
30import CalendarInfoPositionShape from '../shapes/CalendarInfoPositionShape';
31
32import {
33 START_DATE,
34 END_DATE,
35 HORIZONTAL_ORIENTATION,
36 VERTICAL_SCROLLABLE,
37 DAY_SIZE,
38 INFO_POSITION_BOTTOM,
39} from '../constants';
40
41import DayPicker from './DayPicker';
42import getPooledMoment from '../utils/getPooledMoment';
43
44const propTypes = forbidExtraProps({
45 startDate: momentPropTypes.momentObj,
46 endDate: momentPropTypes.momentObj,
47 onDatesChange: PropTypes.func,
48 startDateOffset: PropTypes.func,
49 endDateOffset: PropTypes.func,
50 minDate: momentPropTypes.momentObj,
51 maxDate: momentPropTypes.momentObj,
52
53 focusedInput: FocusedInputShape,
54 onFocusChange: PropTypes.func,
55 onClose: PropTypes.func,
56
57 keepOpenOnDateSelect: PropTypes.bool,
58 minimumNights: PropTypes.number,
59 disabled: DisabledShape,
60 isOutsideRange: PropTypes.func,
61 isDayBlocked: PropTypes.func,
62 isDayHighlighted: PropTypes.func,
63 getMinNightsForHoverDate: PropTypes.func,
64
65 // DayPicker props
66 renderMonthText: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'),
67 renderMonthElement: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'),
68 renderWeekHeaderElement: PropTypes.func,
69 enableOutsideDays: PropTypes.bool,
70 numberOfMonths: PropTypes.number,
71 orientation: ScrollableOrientationShape,
72 withPortal: PropTypes.bool,
73 initialVisibleMonth: PropTypes.func,
74 hideKeyboardShortcutsPanel: PropTypes.bool,
75 daySize: nonNegativeInteger,
76 noBorder: PropTypes.bool,
77 verticalBorderSpacing: nonNegativeInteger,
78 horizontalMonthPadding: nonNegativeInteger,
79
80 navPrev: PropTypes.node,
81 navNext: PropTypes.node,
82 noNavButtons: PropTypes.bool,
83
84 onPrevMonthClick: PropTypes.func,
85 onNextMonthClick: PropTypes.func,
86 onOutsideClick: PropTypes.func,
87 renderCalendarDay: PropTypes.func,
88 renderDayContents: PropTypes.func,
89 renderCalendarInfo: PropTypes.func,
90 renderKeyboardShortcutsButton: PropTypes.func,
91 calendarInfoPosition: CalendarInfoPositionShape,
92 firstDayOfWeek: DayOfWeekShape,
93 verticalHeight: nonNegativeInteger,
94 transitionDuration: nonNegativeInteger,
95
96 // accessibility
97 onBlur: PropTypes.func,
98 isFocused: PropTypes.bool,
99 showKeyboardShortcuts: PropTypes.bool,
100 onTab: PropTypes.func,
101 onShiftTab: PropTypes.func,
102
103 // i18n
104 monthFormat: PropTypes.string,
105 weekDayFormat: PropTypes.string,
106 phrases: PropTypes.shape(getPhrasePropTypes(DayPickerPhrases)),
107 dayAriaLabelFormat: PropTypes.string,
108
109 isRTL: PropTypes.bool,
110});
111
112const defaultProps = {
113 startDate: undefined, // TODO: use null
114 endDate: undefined, // TODO: use null
115 minDate: null,
116 maxDate: null,
117 onDatesChange() {},
118 startDateOffset: undefined,
119 endDateOffset: undefined,
120
121 focusedInput: null,
122 onFocusChange() {},
123 onClose() {},
124
125 keepOpenOnDateSelect: false,
126 minimumNights: 1,
127 disabled: false,
128 isOutsideRange() {},
129 isDayBlocked() {},
130 isDayHighlighted() {},
131 getMinNightsForHoverDate() {},
132
133 // DayPicker props
134 renderMonthText: null,
135 renderWeekHeaderElement: null,
136 enableOutsideDays: false,
137 numberOfMonths: 1,
138 orientation: HORIZONTAL_ORIENTATION,
139 withPortal: false,
140 hideKeyboardShortcutsPanel: false,
141 initialVisibleMonth: null,
142 daySize: DAY_SIZE,
143
144 navPrev: null,
145 navNext: null,
146 noNavButtons: false,
147
148 onPrevMonthClick() {},
149 onNextMonthClick() {},
150 onOutsideClick() {},
151
152 renderCalendarDay: undefined,
153 renderDayContents: null,
154 renderCalendarInfo: null,
155 renderMonthElement: null,
156 renderKeyboardShortcutsButton: undefined,
157 calendarInfoPosition: INFO_POSITION_BOTTOM,
158 firstDayOfWeek: null,
159 verticalHeight: null,
160 noBorder: false,
161 transitionDuration: undefined,
162 verticalBorderSpacing: undefined,
163 horizontalMonthPadding: 13,
164
165 // accessibility
166 onBlur() {},
167 isFocused: false,
168 showKeyboardShortcuts: false,
169 onTab() {},
170 onShiftTab() {},
171
172 // i18n
173 monthFormat: 'MMMM YYYY',
174 weekDayFormat: 'dd',
175 phrases: DayPickerPhrases,
176 dayAriaLabelFormat: undefined,
177
178 isRTL: false,
179};
180
181const getChooseAvailableDatePhrase = (phrases, focusedInput) => {
182 if (focusedInput === START_DATE) {
183 return phrases.chooseAvailableStartDate;
184 }
185 if (focusedInput === END_DATE) {
186 return phrases.chooseAvailableEndDate;
187 }
188 return phrases.chooseAvailableDate;
189};
190
191export default class DayPickerRangeController extends React.PureComponent {
192 constructor(props) {
193 super(props);
194
195 this.isTouchDevice = isTouchDevice();
196 this.today = moment();
197 this.modifiers = {
198 today: (day) => this.isToday(day),
199 blocked: (day) => this.isBlocked(day),
200 'blocked-calendar': (day) => props.isDayBlocked(day),
201 'blocked-out-of-range': (day) => props.isOutsideRange(day),
202 'highlighted-calendar': (day) => props.isDayHighlighted(day),
203 valid: (day) => !this.isBlocked(day),
204 'selected-start': (day) => this.isStartDate(day),
205 'selected-end': (day) => this.isEndDate(day),
206 'blocked-minimum-nights': (day) => this.doesNotMeetMinimumNights(day),
207 'selected-span': (day) => this.isInSelectedSpan(day),
208 'last-in-range': (day) => this.isLastInRange(day),
209 hovered: (day) => this.isHovered(day),
210 'hovered-span': (day) => this.isInHoveredSpan(day),
211 'hovered-offset': (day) => this.isInHoveredSpan(day),
212 'after-hovered-start': (day) => this.isDayAfterHoveredStartDate(day),
213 'first-day-of-week': (day) => this.isFirstDayOfWeek(day),
214 'last-day-of-week': (day) => this.isLastDayOfWeek(day),
215 'hovered-start-first-possible-end': (day, hoverDate) => this.isFirstPossibleEndDateForHoveredStartDate(day, hoverDate),
216 'hovered-start-blocked-minimum-nights': (day, hoverDate) => this.doesNotMeetMinNightsForHoveredStartDate(day, hoverDate),
217 };
218
219 const { currentMonth, visibleDays } = this.getStateForNewMonth(props);
220
221 // initialize phrases
222 // set the appropriate CalendarDay phrase based on focusedInput
223 const chooseAvailableDate = getChooseAvailableDatePhrase(props.phrases, props.focusedInput);
224
225 this.state = {
226 hoverDate: null,
227 currentMonth,
228 phrases: {
229 ...props.phrases,
230 chooseAvailableDate,
231 },
232 visibleDays,
233 disablePrev: this.shouldDisableMonthNavigation(props.minDate, currentMonth),
234 disableNext: this.shouldDisableMonthNavigation(props.maxDate, currentMonth),
235 };
236
237 this.onDayClick = this.onDayClick.bind(this);
238 this.onDayMouseEnter = this.onDayMouseEnter.bind(this);
239 this.onDayMouseLeave = this.onDayMouseLeave.bind(this);
240 this.onPrevMonthClick = this.onPrevMonthClick.bind(this);
241 this.onNextMonthClick = this.onNextMonthClick.bind(this);
242 this.onMonthChange = this.onMonthChange.bind(this);
243 this.onYearChange = this.onYearChange.bind(this);
244 this.onMultiplyScrollableMonths = this.onMultiplyScrollableMonths.bind(this);
245 this.getFirstFocusableDay = this.getFirstFocusableDay.bind(this);
246 }
247
248 componentWillReceiveProps(nextProps) {
249 const {
250 startDate,
251 endDate,
252 focusedInput,
253 getMinNightsForHoverDate,
254 minimumNights,
255 isOutsideRange,
256 isDayBlocked,
257 isDayHighlighted,
258 phrases,
259 initialVisibleMonth,
260 numberOfMonths,
261 enableOutsideDays,
262 } = nextProps;
263
264 const {
265 startDate: prevStartDate,
266 endDate: prevEndDate,
267 focusedInput: prevFocusedInput,
268 minimumNights: prevMinimumNights,
269 isOutsideRange: prevIsOutsideRange,
270 isDayBlocked: prevIsDayBlocked,
271 isDayHighlighted: prevIsDayHighlighted,
272 phrases: prevPhrases,
273 initialVisibleMonth: prevInitialVisibleMonth,
274 numberOfMonths: prevNumberOfMonths,
275 enableOutsideDays: prevEnableOutsideDays,
276 } = this.props;
277
278 const { hoverDate } = this.state;
279 let { visibleDays } = this.state;
280
281 let recomputeOutsideRange = false;
282 let recomputeDayBlocked = false;
283 let recomputeDayHighlighted = false;
284
285 if (isOutsideRange !== prevIsOutsideRange) {
286 this.modifiers['blocked-out-of-range'] = (day) => isOutsideRange(day);
287 recomputeOutsideRange = true;
288 }
289
290 if (isDayBlocked !== prevIsDayBlocked) {
291 this.modifiers['blocked-calendar'] = (day) => isDayBlocked(day);
292 recomputeDayBlocked = true;
293 }
294
295 if (isDayHighlighted !== prevIsDayHighlighted) {
296 this.modifiers['highlighted-calendar'] = (day) => isDayHighlighted(day);
297 recomputeDayHighlighted = true;
298 }
299
300 const recomputePropModifiers = (
301 recomputeOutsideRange || recomputeDayBlocked || recomputeDayHighlighted
302 );
303
304 const didStartDateChange = startDate !== prevStartDate;
305 const didEndDateChange = endDate !== prevEndDate;
306 const didFocusChange = focusedInput !== prevFocusedInput;
307
308 if (
309 numberOfMonths !== prevNumberOfMonths
310 || enableOutsideDays !== prevEnableOutsideDays
311 || (
312 initialVisibleMonth !== prevInitialVisibleMonth
313 && !prevFocusedInput
314 && didFocusChange
315 )
316 ) {
317 const newMonthState = this.getStateForNewMonth(nextProps);
318 const { currentMonth } = newMonthState;
319 ({ visibleDays } = newMonthState);
320 this.setState({
321 currentMonth,
322 visibleDays,
323 });
324 }
325
326 let modifiers = {};
327
328 if (didStartDateChange) {
329 modifiers = this.deleteModifier(modifiers, prevStartDate, 'selected-start');
330 modifiers = this.addModifier(modifiers, startDate, 'selected-start');
331
332 if (prevStartDate) {
333 const startSpan = prevStartDate.clone().add(1, 'day');
334 const endSpan = prevStartDate.clone().add(prevMinimumNights + 1, 'days');
335 modifiers = this.deleteModifierFromRange(modifiers, startSpan, endSpan, 'after-hovered-start');
336 }
337 }
338
339 if (didEndDateChange) {
340 modifiers = this.deleteModifier(modifiers, prevEndDate, 'selected-end');
341 modifiers = this.addModifier(modifiers, endDate, 'selected-end');
342 }
343
344 if (didStartDateChange || didEndDateChange) {
345 if (prevStartDate && prevEndDate) {
346 modifiers = this.deleteModifierFromRange(
347 modifiers,
348 prevStartDate,
349 prevEndDate.clone().add(1, 'day'),
350 'selected-span',
351 );
352 }
353
354 if (startDate && endDate) {
355 modifiers = this.deleteModifierFromRange(
356 modifiers,
357 startDate,
358 endDate.clone().add(1, 'day'),
359 'hovered-span',
360 );
361
362 modifiers = this.addModifierToRange(
363 modifiers,
364 startDate.clone().add(1, 'day'),
365 endDate,
366 'selected-span',
367 );
368 }
369 }
370
371 if (!this.isTouchDevice && didStartDateChange && startDate && !endDate) {
372 const startSpan = startDate.clone().add(1, 'day');
373 const endSpan = startDate.clone().add(minimumNights + 1, 'days');
374 modifiers = this.addModifierToRange(modifiers, startSpan, endSpan, 'after-hovered-start');
375 }
376
377 if (prevMinimumNights > 0) {
378 if (didFocusChange || didStartDateChange || minimumNights !== prevMinimumNights) {
379 const startSpan = prevStartDate || this.today;
380 modifiers = this.deleteModifierFromRange(
381 modifiers,
382 startSpan,
383 startSpan.clone().add(prevMinimumNights, 'days'),
384 'blocked-minimum-nights',
385 );
386
387 modifiers = this.deleteModifierFromRange(
388 modifiers,
389 startSpan,
390 startSpan.clone().add(prevMinimumNights, 'days'),
391 'blocked',
392 );
393 }
394 }
395
396 if (didFocusChange || recomputePropModifiers) {
397 values(visibleDays).forEach((days) => {
398 Object.keys(days).forEach((day) => {
399 const momentObj = getPooledMoment(day);
400 let isBlocked = false;
401
402 if (didFocusChange || recomputeOutsideRange) {
403 if (isOutsideRange(momentObj)) {
404 modifiers = this.addModifier(modifiers, momentObj, 'blocked-out-of-range');
405 isBlocked = true;
406 } else {
407 modifiers = this.deleteModifier(modifiers, momentObj, 'blocked-out-of-range');
408 }
409 }
410
411 if (didFocusChange || recomputeDayBlocked) {
412 if (isDayBlocked(momentObj)) {
413 modifiers = this.addModifier(modifiers, momentObj, 'blocked-calendar');
414 isBlocked = true;
415 } else {
416 modifiers = this.deleteModifier(modifiers, momentObj, 'blocked-calendar');
417 }
418 }
419
420 if (isBlocked) {
421 modifiers = this.addModifier(modifiers, momentObj, 'blocked');
422 } else {
423 modifiers = this.deleteModifier(modifiers, momentObj, 'blocked');
424 }
425
426 if (didFocusChange || recomputeDayHighlighted) {
427 if (isDayHighlighted(momentObj)) {
428 modifiers = this.addModifier(modifiers, momentObj, 'highlighted-calendar');
429 } else {
430 modifiers = this.deleteModifier(modifiers, momentObj, 'highlighted-calendar');
431 }
432 }
433 });
434 });
435 }
436
437 if (!this.isTouchDevice && didFocusChange && hoverDate && !this.isBlocked(hoverDate)) {
438 const minNightsForHoverDate = getMinNightsForHoverDate(hoverDate);
439 if (minNightsForHoverDate > 0 && focusedInput === END_DATE) {
440 modifiers = this.deleteModifierFromRange(
441 modifiers,
442 hoverDate.clone().add(1, 'days'),
443 hoverDate.clone().add(minNightsForHoverDate, 'days'),
444 'hovered-start-blocked-minimum-nights',
445 );
446
447 modifiers = this.deleteModifier(
448 modifiers,
449 hoverDate.clone().add(minNightsForHoverDate, 'days'),
450 'hovered-start-first-possible-end',
451 );
452 }
453
454 if (minNightsForHoverDate > 0 && focusedInput === START_DATE) {
455 modifiers = this.addModifierToRange(
456 modifiers,
457 hoverDate.clone().add(1, 'days'),
458 hoverDate.clone().add(minNightsForHoverDate, 'days'),
459 'hovered-start-blocked-minimum-nights',
460 );
461
462 modifiers = this.addModifier(
463 modifiers,
464 hoverDate.clone().add(minNightsForHoverDate, 'days'),
465 'hovered-start-first-possible-end',
466 );
467 }
468 }
469
470 if (minimumNights > 0 && startDate && focusedInput === END_DATE) {
471 modifiers = this.addModifierToRange(
472 modifiers,
473 startDate,
474 startDate.clone().add(minimumNights, 'days'),
475 'blocked-minimum-nights',
476 );
477
478 modifiers = this.addModifierToRange(
479 modifiers,
480 startDate,
481 startDate.clone().add(minimumNights, 'days'),
482 'blocked',
483 );
484 }
485
486 const today = moment();
487 if (!isSameDay(this.today, today)) {
488 modifiers = this.deleteModifier(modifiers, this.today, 'today');
489 modifiers = this.addModifier(modifiers, today, 'today');
490 this.today = today;
491 }
492
493 if (Object.keys(modifiers).length > 0) {
494 this.setState({
495 visibleDays: {
496 ...visibleDays,
497 ...modifiers,
498 },
499 });
500 }
501
502 if (didFocusChange || phrases !== prevPhrases) {
503 // set the appropriate CalendarDay phrase based on focusedInput
504 const chooseAvailableDate = getChooseAvailableDatePhrase(phrases, focusedInput);
505
506 this.setState({
507 phrases: {
508 ...phrases,
509 chooseAvailableDate,
510 },
511 });
512 }
513 }
514
515 onDayClick(day, e) {
516 const {
517 keepOpenOnDateSelect,
518 minimumNights,
519 onBlur,
520 focusedInput,
521 onFocusChange,
522 onClose,
523 onDatesChange,
524 startDateOffset,
525 endDateOffset,
526 disabled,
527 } = this.props;
528
529 if (e) e.preventDefault();
530 if (this.isBlocked(day)) return;
531
532 let { startDate, endDate } = this.props;
533
534 if (startDateOffset || endDateOffset) {
535 startDate = getSelectedDateOffset(startDateOffset, day);
536 endDate = getSelectedDateOffset(endDateOffset, day);
537
538 if (this.isBlocked(startDate) || this.isBlocked(endDate)) {
539 return;
540 }
541
542 onDatesChange({ startDate, endDate });
543
544 if (!keepOpenOnDateSelect) {
545 onFocusChange(null);
546 onClose({ startDate, endDate });
547 }
548 } else if (focusedInput === START_DATE) {
549 const lastAllowedStartDate = endDate && endDate.clone().subtract(minimumNights, 'days');
550 const isStartDateAfterEndDate = isBeforeDay(lastAllowedStartDate, day)
551 || isAfterDay(startDate, endDate);
552 const isEndDateDisabled = disabled === END_DATE;
553
554 if (!isEndDateDisabled || !isStartDateAfterEndDate) {
555 startDate = day;
556 if (isStartDateAfterEndDate) {
557 endDate = null;
558 }
559 }
560
561 onDatesChange({ startDate, endDate });
562
563 if (isEndDateDisabled && !isStartDateAfterEndDate) {
564 onFocusChange(null);
565 onClose({ startDate, endDate });
566 } else if (!isEndDateDisabled) {
567 onFocusChange(END_DATE);
568 }
569 } else if (focusedInput === END_DATE) {
570 const firstAllowedEndDate = startDate && startDate.clone().add(minimumNights, 'days');
571
572 if (!startDate) {
573 endDate = day;
574 onDatesChange({ startDate, endDate });
575 onFocusChange(START_DATE);
576 } else if (isInclusivelyAfterDay(day, firstAllowedEndDate)) {
577 endDate = day;
578 onDatesChange({ startDate, endDate });
579 if (!keepOpenOnDateSelect) {
580 onFocusChange(null);
581 onClose({ startDate, endDate });
582 }
583 } else if (disabled !== START_DATE) {
584 startDate = day;
585 endDate = null;
586 onDatesChange({ startDate, endDate });
587 } else {
588 onDatesChange({ startDate, endDate });
589 }
590 } else {
591 onDatesChange({ startDate, endDate });
592 }
593
594 onBlur();
595 }
596
597 onDayMouseEnter(day) {
598 /* eslint react/destructuring-assignment: 1 */
599 if (this.isTouchDevice) return;
600 const {
601 startDate,
602 endDate,
603 focusedInput,
604 getMinNightsForHoverDate,
605 minimumNights,
606 startDateOffset,
607 endDateOffset,
608 } = this.props;
609
610 const {
611 hoverDate,
612 visibleDays,
613 dateOffset,
614 } = this.state;
615
616 let nextDateOffset = null;
617
618 if (focusedInput) {
619 const hasOffset = startDateOffset || endDateOffset;
620 let modifiers = {};
621
622 if (hasOffset) {
623 const start = getSelectedDateOffset(startDateOffset, day);
624 const end = getSelectedDateOffset(endDateOffset, day, (rangeDay) => rangeDay.add(1, 'day'));
625
626 nextDateOffset = {
627 start,
628 end,
629 };
630
631 // eslint-disable-next-line react/destructuring-assignment
632 if (dateOffset && dateOffset.start && dateOffset.end) {
633 modifiers = this.deleteModifierFromRange(modifiers, dateOffset.start, dateOffset.end, 'hovered-offset');
634 }
635 modifiers = this.addModifierToRange(modifiers, start, end, 'hovered-offset');
636 }
637
638 if (!hasOffset) {
639 modifiers = this.deleteModifier(modifiers, hoverDate, 'hovered');
640 modifiers = this.addModifier(modifiers, day, 'hovered');
641
642 if (startDate && !endDate && focusedInput === END_DATE) {
643 if (isAfterDay(hoverDate, startDate)) {
644 const endSpan = hoverDate.clone().add(1, 'day');
645 modifiers = this.deleteModifierFromRange(modifiers, startDate, endSpan, 'hovered-span');
646 }
647
648 if (!this.isBlocked(day) && isAfterDay(day, startDate)) {
649 const endSpan = day.clone().add(1, 'day');
650 modifiers = this.addModifierToRange(modifiers, startDate, endSpan, 'hovered-span');
651 }
652 }
653
654 if (!startDate && endDate && focusedInput === START_DATE) {
655 if (isBeforeDay(hoverDate, endDate)) {
656 modifiers = this.deleteModifierFromRange(modifiers, hoverDate, endDate, 'hovered-span');
657 }
658
659 if (!this.isBlocked(day) && isBeforeDay(day, endDate)) {
660 modifiers = this.addModifierToRange(modifiers, day, endDate, 'hovered-span');
661 }
662 }
663
664 if (startDate) {
665 const startSpan = startDate.clone().add(1, 'day');
666 const endSpan = startDate.clone().add(minimumNights + 1, 'days');
667 modifiers = this.deleteModifierFromRange(modifiers, startSpan, endSpan, 'after-hovered-start');
668
669 if (isSameDay(day, startDate)) {
670 const newStartSpan = startDate.clone().add(1, 'day');
671 const newEndSpan = startDate.clone().add(minimumNights + 1, 'days');
672 modifiers = this.addModifierToRange(
673 modifiers,
674 newStartSpan,
675 newEndSpan,
676 'after-hovered-start',
677 );
678 }
679 }
680
681 if (hoverDate && !this.isBlocked(hoverDate)) {
682 const minNightsForPrevHoverDate = getMinNightsForHoverDate(hoverDate);
683 if (minNightsForPrevHoverDate > 0 && focusedInput === START_DATE) {
684 modifiers = this.deleteModifierFromRange(
685 modifiers,
686 hoverDate.clone().add(1, 'days'),
687 hoverDate.clone().add(minNightsForPrevHoverDate, 'days'),
688 'hovered-start-blocked-minimum-nights',
689 );
690
691 modifiers = this.deleteModifier(
692 modifiers,
693 hoverDate.clone().add(minNightsForPrevHoverDate, 'days'),
694 'hovered-start-first-possible-end',
695 );
696 }
697 }
698
699 if (!this.isBlocked(day)) {
700 const minNightsForHoverDate = getMinNightsForHoverDate(day);
701 if (minNightsForHoverDate > 0 && focusedInput === START_DATE) {
702 modifiers = this.addModifierToRange(
703 modifiers,
704 day.clone().add(1, 'days'),
705 day.clone().add(minNightsForHoverDate, 'days'),
706 'hovered-start-blocked-minimum-nights',
707 );
708
709 modifiers = this.addModifier(
710 modifiers,
711 day.clone().add(minNightsForHoverDate, 'days'),
712 'hovered-start-first-possible-end',
713 );
714 }
715 }
716 }
717
718 this.setState({
719 hoverDate: day,
720 dateOffset: nextDateOffset,
721 visibleDays: {
722 ...visibleDays,
723 ...modifiers,
724 },
725 });
726 }
727 }
728
729 onDayMouseLeave(day) {
730 const {
731 startDate,
732 endDate,
733 focusedInput,
734 getMinNightsForHoverDate,
735 minimumNights,
736 } = this.props;
737 const { hoverDate, visibleDays, dateOffset } = this.state;
738 if (this.isTouchDevice || !hoverDate) return;
739
740 let modifiers = {};
741 modifiers = this.deleteModifier(modifiers, hoverDate, 'hovered');
742
743 if (dateOffset) {
744 modifiers = this.deleteModifierFromRange(modifiers, dateOffset.start, dateOffset.end, 'hovered-offset');
745 }
746
747 if (startDate && !endDate && isAfterDay(hoverDate, startDate)) {
748 const endSpan = hoverDate.clone().add(1, 'day');
749 modifiers = this.deleteModifierFromRange(modifiers, startDate, endSpan, 'hovered-span');
750 }
751
752 if (!startDate && endDate && isAfterDay(endDate, hoverDate)) {
753 modifiers = this.deleteModifierFromRange(modifiers, hoverDate, endDate, 'hovered-span');
754 }
755
756 if (startDate && isSameDay(day, startDate)) {
757 const startSpan = startDate.clone().add(1, 'day');
758 const endSpan = startDate.clone().add(minimumNights + 1, 'days');
759 modifiers = this.deleteModifierFromRange(modifiers, startSpan, endSpan, 'after-hovered-start');
760 }
761
762 if (!this.isBlocked(hoverDate)) {
763 const minNightsForHoverDate = getMinNightsForHoverDate(hoverDate);
764 if (minNightsForHoverDate > 0 && focusedInput === START_DATE) {
765 modifiers = this.deleteModifierFromRange(
766 modifiers,
767 hoverDate.clone().add(1, 'days'),
768 hoverDate.clone().add(minNightsForHoverDate, 'days'),
769 'hovered-start-blocked-minimum-nights',
770 );
771
772 modifiers = this.deleteModifier(
773 modifiers,
774 hoverDate.clone().add(minNightsForHoverDate, 'days'),
775 'hovered-start-first-possible-end',
776 );
777 }
778 }
779
780
781 this.setState({
782 hoverDate: null,
783 visibleDays: {
784 ...visibleDays,
785 ...modifiers,
786 },
787 });
788 }
789
790 onPrevMonthClick() {
791 const {
792 enableOutsideDays,
793 maxDate,
794 minDate,
795 numberOfMonths,
796 onPrevMonthClick,
797 } = this.props;
798 const { currentMonth, visibleDays } = this.state;
799
800 const newVisibleDays = {};
801 Object.keys(visibleDays).sort().slice(0, numberOfMonths + 1).forEach((month) => {
802 newVisibleDays[month] = visibleDays[month];
803 });
804
805 const prevMonth = currentMonth.clone().subtract(2, 'months');
806 const prevMonthVisibleDays = getVisibleDays(prevMonth, 1, enableOutsideDays, true);
807
808 const newCurrentMonth = currentMonth.clone().subtract(1, 'month');
809 this.setState({
810 currentMonth: newCurrentMonth,
811 disablePrev: this.shouldDisableMonthNavigation(minDate, newCurrentMonth),
812 disableNext: this.shouldDisableMonthNavigation(maxDate, newCurrentMonth),
813 visibleDays: {
814 ...newVisibleDays,
815 ...this.getModifiers(prevMonthVisibleDays),
816 },
817 }, () => {
818 onPrevMonthClick(newCurrentMonth.clone());
819 });
820 }
821
822 onNextMonthClick() {
823 const {
824 enableOutsideDays,
825 maxDate,
826 minDate,
827 numberOfMonths,
828 onNextMonthClick,
829 } = this.props;
830 const { currentMonth, visibleDays } = this.state;
831
832 const newVisibleDays = {};
833 Object.keys(visibleDays).sort().slice(1).forEach((month) => {
834 newVisibleDays[month] = visibleDays[month];
835 });
836
837 const nextMonth = currentMonth.clone().add(numberOfMonths + 1, 'month');
838 const nextMonthVisibleDays = getVisibleDays(nextMonth, 1, enableOutsideDays, true);
839 const newCurrentMonth = currentMonth.clone().add(1, 'month');
840 this.setState({
841 currentMonth: newCurrentMonth,
842 disablePrev: this.shouldDisableMonthNavigation(minDate, newCurrentMonth),
843 disableNext: this.shouldDisableMonthNavigation(maxDate, newCurrentMonth),
844 visibleDays: {
845 ...newVisibleDays,
846 ...this.getModifiers(nextMonthVisibleDays),
847 },
848 }, () => {
849 onNextMonthClick(newCurrentMonth.clone());
850 });
851 }
852
853 onMonthChange(newMonth) {
854 const { numberOfMonths, enableOutsideDays, orientation } = this.props;
855 const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
856 const newVisibleDays = getVisibleDays(
857 newMonth,
858 numberOfMonths,
859 enableOutsideDays,
860 withoutTransitionMonths,
861 );
862
863 this.setState({
864 currentMonth: newMonth.clone(),
865 visibleDays: this.getModifiers(newVisibleDays),
866 });
867 }
868
869 onYearChange(newMonth) {
870 const { numberOfMonths, enableOutsideDays, orientation } = this.props;
871 const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
872 const newVisibleDays = getVisibleDays(
873 newMonth,
874 numberOfMonths,
875 enableOutsideDays,
876 withoutTransitionMonths,
877 );
878
879 this.setState({
880 currentMonth: newMonth.clone(),
881 visibleDays: this.getModifiers(newVisibleDays),
882 });
883 }
884
885 onMultiplyScrollableMonths() {
886 const { numberOfMonths, enableOutsideDays } = this.props;
887 const { currentMonth, visibleDays } = this.state;
888
889 const numberOfVisibleMonths = Object.keys(visibleDays).length;
890 const nextMonth = currentMonth.clone().add(numberOfVisibleMonths, 'month');
891 const newVisibleDays = getVisibleDays(nextMonth, numberOfMonths, enableOutsideDays, true);
892
893 this.setState({
894 visibleDays: {
895 ...visibleDays,
896 ...this.getModifiers(newVisibleDays),
897 },
898 });
899 }
900
901 getFirstFocusableDay(newMonth) {
902 const {
903 startDate,
904 endDate,
905 focusedInput,
906 minimumNights,
907 numberOfMonths,
908 } = this.props;
909
910 let focusedDate = newMonth.clone().startOf('month');
911 if (focusedInput === START_DATE && startDate) {
912 focusedDate = startDate.clone();
913 } else if (focusedInput === END_DATE && !endDate && startDate) {
914 focusedDate = startDate.clone().add(minimumNights, 'days');
915 } else if (focusedInput === END_DATE && endDate) {
916 focusedDate = endDate.clone();
917 }
918
919 if (this.isBlocked(focusedDate)) {
920 const days = [];
921 const lastVisibleDay = newMonth.clone().add(numberOfMonths - 1, 'months').endOf('month');
922 let currentDay = focusedDate.clone();
923 while (!isAfterDay(currentDay, lastVisibleDay)) {
924 currentDay = currentDay.clone().add(1, 'day');
925 days.push(currentDay);
926 }
927
928 const viableDays = days.filter((day) => !this.isBlocked(day));
929
930 if (viableDays.length > 0) {
931 ([focusedDate] = viableDays);
932 }
933 }
934
935 return focusedDate;
936 }
937
938 getModifiers(visibleDays) {
939 const modifiers = {};
940 Object.keys(visibleDays).forEach((month) => {
941 modifiers[month] = {};
942 visibleDays[month].forEach((day) => {
943 modifiers[month][toISODateString(day)] = this.getModifiersForDay(day);
944 });
945 });
946
947 return modifiers;
948 }
949
950 getModifiersForDay(day) {
951 return new Set(Object.keys(this.modifiers).filter((modifier) => this.modifiers[modifier](day)));
952 }
953
954 getStateForNewMonth(nextProps) {
955 const {
956 initialVisibleMonth,
957 numberOfMonths,
958 enableOutsideDays,
959 orientation,
960 startDate,
961 } = nextProps;
962 const initialVisibleMonthThunk = initialVisibleMonth || (
963 startDate ? () => startDate : () => this.today
964 );
965 const currentMonth = initialVisibleMonthThunk();
966 const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
967 const visibleDays = this.getModifiers(getVisibleDays(
968 currentMonth,
969 numberOfMonths,
970 enableOutsideDays,
971 withoutTransitionMonths,
972 ));
973 return { currentMonth, visibleDays };
974 }
975
976 shouldDisableMonthNavigation(date, visibleMonth) {
977 if (!date) return false;
978
979 const {
980 numberOfMonths,
981 enableOutsideDays,
982 } = this.props;
983
984 return isDayVisible(date, visibleMonth, numberOfMonths, enableOutsideDays);
985 }
986
987 addModifier(updatedDays, day, modifier) {
988 return addModifier(updatedDays, day, modifier, this.props, this.state);
989 }
990
991 addModifierToRange(updatedDays, start, end, modifier) {
992 let days = updatedDays;
993
994 let spanStart = start.clone();
995 while (isBeforeDay(spanStart, end)) {
996 days = this.addModifier(days, spanStart, modifier);
997 spanStart = spanStart.clone().add(1, 'day');
998 }
999
1000 return days;
1001 }
1002
1003 deleteModifier(updatedDays, day, modifier) {
1004 return deleteModifier(updatedDays, day, modifier, this.props, this.state);
1005 }
1006
1007 deleteModifierFromRange(updatedDays, start, end, modifier) {
1008 let days = updatedDays;
1009
1010 let spanStart = start.clone();
1011 while (isBeforeDay(spanStart, end)) {
1012 days = this.deleteModifier(days, spanStart, modifier);
1013 spanStart = spanStart.clone().add(1, 'day');
1014 }
1015
1016 return days;
1017 }
1018
1019 doesNotMeetMinimumNights(day) {
1020 const {
1021 startDate,
1022 isOutsideRange,
1023 focusedInput,
1024 minimumNights,
1025 } = this.props;
1026 if (focusedInput !== END_DATE) return false;
1027
1028 if (startDate) {
1029 const dayDiff = day.diff(startDate.clone().startOf('day').hour(12), 'days');
1030 return dayDiff < minimumNights && dayDiff >= 0;
1031 }
1032 return isOutsideRange(moment(day).subtract(minimumNights, 'days'));
1033 }
1034
1035 doesNotMeetMinNightsForHoveredStartDate(day, hoverDate) {
1036 const {
1037 focusedInput,
1038 getMinNightsForHoverDate,
1039 } = this.props;
1040 if (focusedInput !== END_DATE) return false;
1041
1042 if (hoverDate && !this.isBlocked(hoverDate)) {
1043 const minNights = getMinNightsForHoverDate(hoverDate);
1044 const dayDiff = day.diff(hoverDate.clone().startOf('day').hour(12), 'days');
1045 return dayDiff < minNights && dayDiff >= 0;
1046 }
1047 return false;
1048 }
1049
1050 isDayAfterHoveredStartDate(day) {
1051 const { startDate, endDate, minimumNights } = this.props;
1052 const { hoverDate } = this.state || {};
1053 return !!startDate
1054 && !endDate
1055 && !this.isBlocked(day)
1056 && isNextDay(hoverDate, day)
1057 && minimumNights > 0
1058 && isSameDay(hoverDate, startDate);
1059 }
1060
1061 isEndDate(day) {
1062 const { endDate } = this.props;
1063 return isSameDay(day, endDate);
1064 }
1065
1066 isHovered(day) {
1067 const { hoverDate } = this.state || {};
1068 const { focusedInput } = this.props;
1069 return !!focusedInput && isSameDay(day, hoverDate);
1070 }
1071
1072 isInHoveredSpan(day) {
1073 const { startDate, endDate } = this.props;
1074 const { hoverDate } = this.state || {};
1075
1076 const isForwardRange = !!startDate && !endDate && (
1077 day.isBetween(startDate, hoverDate) || isSameDay(hoverDate, day)
1078 );
1079 const isBackwardRange = !!endDate && !startDate && (
1080 day.isBetween(hoverDate, endDate) || isSameDay(hoverDate, day)
1081 );
1082
1083 const isValidDayHovered = hoverDate && !this.isBlocked(hoverDate);
1084
1085 return (isForwardRange || isBackwardRange) && isValidDayHovered;
1086 }
1087
1088 isInSelectedSpan(day) {
1089 const { startDate, endDate } = this.props;
1090 return day.isBetween(startDate, endDate, 'days');
1091 }
1092
1093 isLastInRange(day) {
1094 const { endDate } = this.props;
1095 return this.isInSelectedSpan(day) && isNextDay(day, endDate);
1096 }
1097
1098 isStartDate(day) {
1099 const { startDate } = this.props;
1100 return isSameDay(day, startDate);
1101 }
1102
1103 isBlocked(day) {
1104 const { isDayBlocked, isOutsideRange } = this.props;
1105 return isDayBlocked(day) || isOutsideRange(day) || this.doesNotMeetMinimumNights(day);
1106 }
1107
1108 isToday(day) {
1109 return isSameDay(day, this.today);
1110 }
1111
1112 isFirstDayOfWeek(day) {
1113 const { firstDayOfWeek } = this.props;
1114 return day.day() === (firstDayOfWeek || moment.localeData().firstDayOfWeek());
1115 }
1116
1117 isLastDayOfWeek(day) {
1118 const { firstDayOfWeek } = this.props;
1119 return day.day() === ((firstDayOfWeek || moment.localeData().firstDayOfWeek()) + 6) % 7;
1120 }
1121
1122 isFirstPossibleEndDateForHoveredStartDate(day, hoverDate) {
1123 const { focusedInput, getMinNightsForHoverDate } = this.props;
1124 if (focusedInput !== END_DATE || !hoverDate || this.isBlocked(hoverDate)) return false;
1125 const minNights = getMinNightsForHoverDate(hoverDate);
1126 const firstAvailableEndDate = hoverDate.clone().add(minNights, 'days');
1127 return isSameDay(day, firstAvailableEndDate);
1128 }
1129
1130 render() {
1131 const {
1132 numberOfMonths,
1133 orientation,
1134 monthFormat,
1135 renderMonthText,
1136 renderWeekHeaderElement,
1137 navPrev,
1138 navNext,
1139 noNavButtons,
1140 onOutsideClick,
1141 withPortal,
1142 enableOutsideDays,
1143 firstDayOfWeek,
1144 renderKeyboardShortcutsButton,
1145 hideKeyboardShortcutsPanel,
1146 daySize,
1147 focusedInput,
1148 renderCalendarDay,
1149 renderDayContents,
1150 renderCalendarInfo,
1151 renderMonthElement,
1152 calendarInfoPosition,
1153 onBlur,
1154 onShiftTab,
1155 onTab,
1156 isFocused,
1157 showKeyboardShortcuts,
1158 isRTL,
1159 weekDayFormat,
1160 dayAriaLabelFormat,
1161 verticalHeight,
1162 noBorder,
1163 transitionDuration,
1164 verticalBorderSpacing,
1165 horizontalMonthPadding,
1166 } = this.props;
1167
1168 const {
1169 currentMonth,
1170 phrases,
1171 visibleDays,
1172 disablePrev,
1173 disableNext,
1174 } = this.state;
1175
1176 return (
1177 <DayPicker
1178 orientation={orientation}
1179 enableOutsideDays={enableOutsideDays}
1180 modifiers={visibleDays}
1181 numberOfMonths={numberOfMonths}
1182 onDayClick={this.onDayClick}
1183 onDayMouseEnter={this.onDayMouseEnter}
1184 onDayMouseLeave={this.onDayMouseLeave}
1185 onPrevMonthClick={this.onPrevMonthClick}
1186 onNextMonthClick={this.onNextMonthClick}
1187 onMonthChange={this.onMonthChange}
1188 onTab={onTab}
1189 onShiftTab={onShiftTab}
1190 onYearChange={this.onYearChange}
1191 onMultiplyScrollableMonths={this.onMultiplyScrollableMonths}
1192 monthFormat={monthFormat}
1193 renderMonthText={renderMonthText}
1194 renderWeekHeaderElement={renderWeekHeaderElement}
1195 withPortal={withPortal}
1196 hidden={!focusedInput}
1197 initialVisibleMonth={() => currentMonth}
1198 daySize={daySize}
1199 onOutsideClick={onOutsideClick}
1200 disablePrev={disablePrev}
1201 disableNext={disableNext}
1202 navPrev={navPrev}
1203 navNext={navNext}
1204 noNavButtons={noNavButtons}
1205 renderCalendarDay={renderCalendarDay}
1206 renderDayContents={renderDayContents}
1207 renderCalendarInfo={renderCalendarInfo}
1208 renderMonthElement={renderMonthElement}
1209 renderKeyboardShortcutsButton={renderKeyboardShortcutsButton}
1210 calendarInfoPosition={calendarInfoPosition}
1211 firstDayOfWeek={firstDayOfWeek}
1212 hideKeyboardShortcutsPanel={hideKeyboardShortcutsPanel}
1213 isFocused={isFocused}
1214 getFirstFocusableDay={this.getFirstFocusableDay}
1215 onBlur={onBlur}
1216 showKeyboardShortcuts={showKeyboardShortcuts}
1217 phrases={phrases}
1218 isRTL={isRTL}
1219 weekDayFormat={weekDayFormat}
1220 dayAriaLabelFormat={dayAriaLabelFormat}
1221 verticalHeight={verticalHeight}
1222 verticalBorderSpacing={verticalBorderSpacing}
1223 noBorder={noBorder}
1224 transitionDuration={transitionDuration}
1225 horizontalMonthPadding={horizontalMonthPadding}
1226 />
1227 );
1228 }
1229}
1230
1231DayPickerRangeController.propTypes = propTypes;
1232DayPickerRangeController.defaultProps = defaultProps;