UNPKG

19.3 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 isSameDay from '../utils/isSameDay';
13import isAfterDay from '../utils/isAfterDay';
14
15import getVisibleDays from '../utils/getVisibleDays';
16
17import toISODateString from '../utils/toISODateString';
18import { addModifier, deleteModifier } from '../utils/modifiers';
19
20import ScrollableOrientationShape from '../shapes/ScrollableOrientationShape';
21import DayOfWeekShape from '../shapes/DayOfWeekShape';
22import CalendarInfoPositionShape from '../shapes/CalendarInfoPositionShape';
23
24import {
25 HORIZONTAL_ORIENTATION,
26 VERTICAL_SCROLLABLE,
27 DAY_SIZE,
28 INFO_POSITION_BOTTOM,
29} from '../constants';
30
31import DayPicker from './DayPicker';
32import getPooledMoment from '../utils/getPooledMoment';
33
34const propTypes = forbidExtraProps({
35 date: momentPropTypes.momentObj,
36 onDateChange: PropTypes.func,
37
38 focused: PropTypes.bool,
39 onFocusChange: PropTypes.func,
40 onClose: PropTypes.func,
41
42 keepOpenOnDateSelect: PropTypes.bool,
43 isOutsideRange: PropTypes.func,
44 isDayBlocked: PropTypes.func,
45 isDayHighlighted: PropTypes.func,
46
47 // DayPicker props
48 renderMonthText: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'),
49 renderMonthElement: mutuallyExclusiveProps(PropTypes.func, 'renderMonthText', 'renderMonthElement'),
50 renderWeekHeaderElement: PropTypes.func,
51 enableOutsideDays: PropTypes.bool,
52 numberOfMonths: PropTypes.number,
53 orientation: ScrollableOrientationShape,
54 withPortal: PropTypes.bool,
55 initialVisibleMonth: PropTypes.func,
56 firstDayOfWeek: DayOfWeekShape,
57 hideKeyboardShortcutsPanel: PropTypes.bool,
58 daySize: nonNegativeInteger,
59 verticalHeight: nonNegativeInteger,
60 noBorder: PropTypes.bool,
61 verticalBorderSpacing: nonNegativeInteger,
62 transitionDuration: nonNegativeInteger,
63 horizontalMonthPadding: nonNegativeInteger,
64
65 navPrev: PropTypes.node,
66 navNext: PropTypes.node,
67
68 onPrevMonthClick: PropTypes.func,
69 onNextMonthClick: PropTypes.func,
70 onOutsideClick: PropTypes.func,
71 renderCalendarDay: PropTypes.func,
72 renderDayContents: PropTypes.func,
73 renderCalendarInfo: PropTypes.func,
74 calendarInfoPosition: CalendarInfoPositionShape,
75
76 // accessibility
77 onBlur: PropTypes.func,
78 isFocused: PropTypes.bool,
79 showKeyboardShortcuts: PropTypes.bool,
80 onTab: PropTypes.func,
81 onShiftTab: PropTypes.func,
82
83 // i18n
84 monthFormat: PropTypes.string,
85 weekDayFormat: PropTypes.string,
86 phrases: PropTypes.shape(getPhrasePropTypes(DayPickerPhrases)),
87 dayAriaLabelFormat: PropTypes.string,
88
89 isRTL: PropTypes.bool,
90});
91
92const defaultProps = {
93 date: undefined, // TODO: use null
94 onDateChange() {},
95
96 focused: false,
97 onFocusChange() {},
98 onClose() {},
99
100 keepOpenOnDateSelect: false,
101 isOutsideRange() {},
102 isDayBlocked() {},
103 isDayHighlighted() {},
104
105 // DayPicker props
106 renderMonthText: null,
107 renderWeekHeaderElement: null,
108 enableOutsideDays: false,
109 numberOfMonths: 1,
110 orientation: HORIZONTAL_ORIENTATION,
111 withPortal: false,
112 hideKeyboardShortcutsPanel: false,
113 initialVisibleMonth: null,
114 firstDayOfWeek: null,
115 daySize: DAY_SIZE,
116 verticalHeight: null,
117 noBorder: false,
118 verticalBorderSpacing: undefined,
119 transitionDuration: undefined,
120 horizontalMonthPadding: 13,
121
122 navPrev: null,
123 navNext: null,
124
125 onPrevMonthClick() {},
126 onNextMonthClick() {},
127 onOutsideClick() {},
128
129 renderCalendarDay: undefined,
130 renderDayContents: null,
131 renderCalendarInfo: null,
132 renderMonthElement: null,
133 calendarInfoPosition: INFO_POSITION_BOTTOM,
134
135 // accessibility
136 onBlur() {},
137 isFocused: false,
138 showKeyboardShortcuts: false,
139 onTab() {},
140 onShiftTab() {},
141
142 // i18n
143 monthFormat: 'MMMM YYYY',
144 weekDayFormat: 'dd',
145 phrases: DayPickerPhrases,
146 dayAriaLabelFormat: undefined,
147
148 isRTL: false,
149};
150
151export default class DayPickerSingleDateController extends React.PureComponent {
152 constructor(props) {
153 super(props);
154
155 this.isTouchDevice = false;
156 this.today = moment();
157
158 this.modifiers = {
159 today: (day) => this.isToday(day),
160 blocked: (day) => this.isBlocked(day),
161 'blocked-calendar': (day) => props.isDayBlocked(day),
162 'blocked-out-of-range': (day) => props.isOutsideRange(day),
163 'highlighted-calendar': (day) => props.isDayHighlighted(day),
164 valid: (day) => !this.isBlocked(day),
165 hovered: (day) => this.isHovered(day),
166 selected: (day) => this.isSelected(day),
167 'first-day-of-week': (day) => this.isFirstDayOfWeek(day),
168 'last-day-of-week': (day) => this.isLastDayOfWeek(day),
169 };
170
171 const { currentMonth, visibleDays } = this.getStateForNewMonth(props);
172
173 this.state = {
174 hoverDate: null,
175 currentMonth,
176 visibleDays,
177 };
178
179 this.onDayMouseEnter = this.onDayMouseEnter.bind(this);
180 this.onDayMouseLeave = this.onDayMouseLeave.bind(this);
181 this.onDayClick = this.onDayClick.bind(this);
182
183 this.onPrevMonthClick = this.onPrevMonthClick.bind(this);
184 this.onNextMonthClick = this.onNextMonthClick.bind(this);
185 this.onMonthChange = this.onMonthChange.bind(this);
186 this.onYearChange = this.onYearChange.bind(this);
187 this.onMultiplyScrollableMonths = this.onMultiplyScrollableMonths.bind(this);
188 this.getFirstFocusableDay = this.getFirstFocusableDay.bind(this);
189 }
190
191 componentDidMount() {
192 this.isTouchDevice = isTouchDevice();
193 }
194
195 componentWillReceiveProps(nextProps) {
196 const {
197 date,
198 focused,
199 isOutsideRange,
200 isDayBlocked,
201 isDayHighlighted,
202 initialVisibleMonth,
203 numberOfMonths,
204 enableOutsideDays,
205 } = nextProps;
206 const {
207 isOutsideRange: prevIsOutsideRange,
208 isDayBlocked: prevIsDayBlocked,
209 isDayHighlighted: prevIsDayHighlighted,
210 numberOfMonths: prevNumberOfMonths,
211 enableOutsideDays: prevEnableOutsideDays,
212 initialVisibleMonth: prevInitialVisibleMonth,
213 focused: prevFocused,
214 date: prevDate,
215 } = this.props;
216 let { visibleDays } = this.state;
217
218 let recomputeOutsideRange = false;
219 let recomputeDayBlocked = false;
220 let recomputeDayHighlighted = false;
221
222 if (isOutsideRange !== prevIsOutsideRange) {
223 this.modifiers['blocked-out-of-range'] = (day) => isOutsideRange(day);
224 recomputeOutsideRange = true;
225 }
226
227 if (isDayBlocked !== prevIsDayBlocked) {
228 this.modifiers['blocked-calendar'] = (day) => isDayBlocked(day);
229 recomputeDayBlocked = true;
230 }
231
232 if (isDayHighlighted !== prevIsDayHighlighted) {
233 this.modifiers['highlighted-calendar'] = (day) => isDayHighlighted(day);
234 recomputeDayHighlighted = true;
235 }
236
237 const recomputePropModifiers = (
238 recomputeOutsideRange || recomputeDayBlocked || recomputeDayHighlighted
239 );
240
241 if (
242 numberOfMonths !== prevNumberOfMonths
243 || enableOutsideDays !== prevEnableOutsideDays
244 || (
245 initialVisibleMonth !== prevInitialVisibleMonth
246 && !prevFocused
247 && focused
248 )
249 ) {
250 const newMonthState = this.getStateForNewMonth(nextProps);
251 const { currentMonth } = newMonthState;
252 ({ visibleDays } = newMonthState);
253 this.setState({
254 currentMonth,
255 visibleDays,
256 });
257 }
258
259 const didDateChange = date !== prevDate;
260 const didFocusChange = focused !== prevFocused;
261
262 let modifiers = {};
263
264 if (didDateChange) {
265 modifiers = this.deleteModifier(modifiers, prevDate, 'selected');
266 modifiers = this.addModifier(modifiers, date, 'selected');
267 }
268
269 if (didFocusChange || recomputePropModifiers) {
270 values(visibleDays).forEach((days) => {
271 Object.keys(days).forEach((day) => {
272 const momentObj = getPooledMoment(day);
273 if (this.isBlocked(momentObj)) {
274 modifiers = this.addModifier(modifiers, momentObj, 'blocked');
275 } else {
276 modifiers = this.deleteModifier(modifiers, momentObj, 'blocked');
277 }
278
279 if (didFocusChange || recomputeOutsideRange) {
280 if (isOutsideRange(momentObj)) {
281 modifiers = this.addModifier(modifiers, momentObj, 'blocked-out-of-range');
282 } else {
283 modifiers = this.deleteModifier(modifiers, momentObj, 'blocked-out-of-range');
284 }
285 }
286
287 if (didFocusChange || recomputeDayBlocked) {
288 if (isDayBlocked(momentObj)) {
289 modifiers = this.addModifier(modifiers, momentObj, 'blocked-calendar');
290 } else {
291 modifiers = this.deleteModifier(modifiers, momentObj, 'blocked-calendar');
292 }
293 }
294
295 if (didFocusChange || recomputeDayHighlighted) {
296 if (isDayHighlighted(momentObj)) {
297 modifiers = this.addModifier(modifiers, momentObj, 'highlighted-calendar');
298 } else {
299 modifiers = this.deleteModifier(modifiers, momentObj, 'highlighted-calendar');
300 }
301 }
302 });
303 });
304 }
305
306 const today = moment();
307 if (!isSameDay(this.today, today)) {
308 modifiers = this.deleteModifier(modifiers, this.today, 'today');
309 modifiers = this.addModifier(modifiers, today, 'today');
310 this.today = today;
311 }
312
313 if (Object.keys(modifiers).length > 0) {
314 this.setState({
315 visibleDays: {
316 ...visibleDays,
317 ...modifiers,
318 },
319 });
320 }
321 }
322
323 componentWillUpdate() {
324 this.today = moment();
325 }
326
327 onDayClick(day, e) {
328 if (e) e.preventDefault();
329 if (this.isBlocked(day)) return;
330 const {
331 onDateChange,
332 keepOpenOnDateSelect,
333 onFocusChange,
334 onClose,
335 } = this.props;
336
337 onDateChange(day);
338 if (!keepOpenOnDateSelect) {
339 onFocusChange({ focused: false });
340 onClose({ date: day });
341 }
342 }
343
344 onDayMouseEnter(day) {
345 if (this.isTouchDevice) return;
346 const { hoverDate, visibleDays } = this.state;
347
348 let modifiers = this.deleteModifier({}, hoverDate, 'hovered');
349 modifiers = this.addModifier(modifiers, day, 'hovered');
350
351 this.setState({
352 hoverDate: day,
353 visibleDays: {
354 ...visibleDays,
355 ...modifiers,
356 },
357 });
358 }
359
360 onDayMouseLeave() {
361 const { hoverDate, visibleDays } = this.state;
362 if (this.isTouchDevice || !hoverDate) return;
363
364 const modifiers = this.deleteModifier({}, hoverDate, 'hovered');
365
366 this.setState({
367 hoverDate: null,
368 visibleDays: {
369 ...visibleDays,
370 ...modifiers,
371 },
372 });
373 }
374
375 onPrevMonthClick() {
376 const { onPrevMonthClick, numberOfMonths, enableOutsideDays } = this.props;
377 const { currentMonth, visibleDays } = this.state;
378
379 const newVisibleDays = {};
380 Object.keys(visibleDays).sort().slice(0, numberOfMonths + 1).forEach((month) => {
381 newVisibleDays[month] = visibleDays[month];
382 });
383
384 const prevMonth = currentMonth.clone().subtract(1, 'month');
385 const prevMonthVisibleDays = getVisibleDays(prevMonth, 1, enableOutsideDays);
386
387 this.setState({
388 currentMonth: prevMonth,
389 visibleDays: {
390 ...newVisibleDays,
391 ...this.getModifiers(prevMonthVisibleDays),
392 },
393 }, () => {
394 onPrevMonthClick(prevMonth.clone());
395 });
396 }
397
398 onNextMonthClick() {
399 const { onNextMonthClick, numberOfMonths, enableOutsideDays } = this.props;
400 const { currentMonth, visibleDays } = this.state;
401
402 const newVisibleDays = {};
403 Object.keys(visibleDays).sort().slice(1).forEach((month) => {
404 newVisibleDays[month] = visibleDays[month];
405 });
406
407 const nextMonth = currentMonth.clone().add(numberOfMonths, 'month');
408 const nextMonthVisibleDays = getVisibleDays(nextMonth, 1, enableOutsideDays);
409
410 const newCurrentMonth = currentMonth.clone().add(1, 'month');
411 this.setState({
412 currentMonth: newCurrentMonth,
413 visibleDays: {
414 ...newVisibleDays,
415 ...this.getModifiers(nextMonthVisibleDays),
416 },
417 }, () => {
418 onNextMonthClick(newCurrentMonth.clone());
419 });
420 }
421
422 onMonthChange(newMonth) {
423 const { numberOfMonths, enableOutsideDays, orientation } = this.props;
424 const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
425 const newVisibleDays = getVisibleDays(
426 newMonth,
427 numberOfMonths,
428 enableOutsideDays,
429 withoutTransitionMonths,
430 );
431
432 this.setState({
433 currentMonth: newMonth.clone(),
434 visibleDays: this.getModifiers(newVisibleDays),
435 });
436 }
437
438 onYearChange(newMonth) {
439 const { numberOfMonths, enableOutsideDays, orientation } = this.props;
440 const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
441 const newVisibleDays = getVisibleDays(
442 newMonth,
443 numberOfMonths,
444 enableOutsideDays,
445 withoutTransitionMonths,
446 );
447
448 this.setState({
449 currentMonth: newMonth.clone(),
450 visibleDays: this.getModifiers(newVisibleDays),
451 });
452 }
453
454 onMultiplyScrollableMonths() {
455 const { numberOfMonths, enableOutsideDays } = this.props;
456 const { currentMonth, visibleDays } = this.state;
457
458 const numberOfVisibleMonths = Object.keys(visibleDays).length;
459 const nextMonth = currentMonth.clone().add(numberOfVisibleMonths, 'month');
460 const newVisibleDays = getVisibleDays(nextMonth, numberOfMonths, enableOutsideDays, true);
461
462 this.setState({
463 visibleDays: {
464 ...visibleDays,
465 ...this.getModifiers(newVisibleDays),
466 },
467 });
468 }
469
470 getFirstFocusableDay(newMonth) {
471 const { date, numberOfMonths } = this.props;
472
473 let focusedDate = newMonth.clone().startOf('month');
474 if (date) {
475 focusedDate = date.clone();
476 }
477
478 if (this.isBlocked(focusedDate)) {
479 const days = [];
480 const lastVisibleDay = newMonth.clone().add(numberOfMonths - 1, 'months').endOf('month');
481 let currentDay = focusedDate.clone();
482 while (!isAfterDay(currentDay, lastVisibleDay)) {
483 currentDay = currentDay.clone().add(1, 'day');
484 days.push(currentDay);
485 }
486
487 const viableDays = days.filter((day) => !this.isBlocked(day) && isAfterDay(day, focusedDate));
488 if (viableDays.length > 0) {
489 ([focusedDate] = viableDays);
490 }
491 }
492
493 return focusedDate;
494 }
495
496 getModifiers(visibleDays) {
497 const modifiers = {};
498 Object.keys(visibleDays).forEach((month) => {
499 modifiers[month] = {};
500 visibleDays[month].forEach((day) => {
501 modifiers[month][toISODateString(day)] = this.getModifiersForDay(day);
502 });
503 });
504
505 return modifiers;
506 }
507
508 getModifiersForDay(day) {
509 return new Set(Object.keys(this.modifiers).filter((modifier) => this.modifiers[modifier](day)));
510 }
511
512 getStateForNewMonth(nextProps) {
513 const {
514 initialVisibleMonth,
515 date,
516 numberOfMonths,
517 orientation,
518 enableOutsideDays,
519 } = nextProps;
520 const initialVisibleMonthThunk = initialVisibleMonth || (date ? () => date : () => this.today);
521 const currentMonth = initialVisibleMonthThunk();
522 const withoutTransitionMonths = orientation === VERTICAL_SCROLLABLE;
523 const visibleDays = this.getModifiers(getVisibleDays(
524 currentMonth,
525 numberOfMonths,
526 enableOutsideDays,
527 withoutTransitionMonths,
528 ));
529 return { currentMonth, visibleDays };
530 }
531
532 addModifier(updatedDays, day, modifier) {
533 return addModifier(updatedDays, day, modifier, this.props, this.state);
534 }
535
536 deleteModifier(updatedDays, day, modifier) {
537 return deleteModifier(updatedDays, day, modifier, this.props, this.state);
538 }
539
540 isBlocked(day) {
541 const { isDayBlocked, isOutsideRange } = this.props;
542 return isDayBlocked(day) || isOutsideRange(day);
543 }
544
545 isHovered(day) {
546 const { hoverDate } = this.state || {};
547 return isSameDay(day, hoverDate);
548 }
549
550 isSelected(day) {
551 const { date } = this.props;
552 return isSameDay(day, date);
553 }
554
555 isToday(day) {
556 return isSameDay(day, this.today);
557 }
558
559 isFirstDayOfWeek(day) {
560 const { firstDayOfWeek } = this.props;
561 return day.day() === (firstDayOfWeek || moment.localeData().firstDayOfWeek());
562 }
563
564 isLastDayOfWeek(day) {
565 const { firstDayOfWeek } = this.props;
566 return day.day() === ((firstDayOfWeek || moment.localeData().firstDayOfWeek()) + 6) % 7;
567 }
568
569 render() {
570 const {
571 numberOfMonths,
572 orientation,
573 monthFormat,
574 renderMonthText,
575 renderWeekHeaderElement,
576 navPrev,
577 navNext,
578 onOutsideClick,
579 onShiftTab,
580 onTab,
581 withPortal,
582 focused,
583 enableOutsideDays,
584 hideKeyboardShortcutsPanel,
585 daySize,
586 firstDayOfWeek,
587 renderCalendarDay,
588 renderDayContents,
589 renderCalendarInfo,
590 renderMonthElement,
591 calendarInfoPosition,
592 isFocused,
593 isRTL,
594 phrases,
595 dayAriaLabelFormat,
596 onBlur,
597 showKeyboardShortcuts,
598 weekDayFormat,
599 verticalHeight,
600 noBorder,
601 transitionDuration,
602 verticalBorderSpacing,
603 horizontalMonthPadding,
604 } = this.props;
605
606 const { currentMonth, visibleDays } = this.state;
607
608 return (
609 <DayPicker
610 orientation={orientation}
611 enableOutsideDays={enableOutsideDays}
612 modifiers={visibleDays}
613 numberOfMonths={numberOfMonths}
614 onDayClick={this.onDayClick}
615 onDayMouseEnter={this.onDayMouseEnter}
616 onDayMouseLeave={this.onDayMouseLeave}
617 onPrevMonthClick={this.onPrevMonthClick}
618 onNextMonthClick={this.onNextMonthClick}
619 onMonthChange={this.onMonthChange}
620 onYearChange={this.onYearChange}
621 onMultiplyScrollableMonths={this.onMultiplyScrollableMonths}
622 monthFormat={monthFormat}
623 withPortal={withPortal}
624 hidden={!focused}
625 hideKeyboardShortcutsPanel={hideKeyboardShortcutsPanel}
626 initialVisibleMonth={() => currentMonth}
627 firstDayOfWeek={firstDayOfWeek}
628 onOutsideClick={onOutsideClick}
629 navPrev={navPrev}
630 navNext={navNext}
631 renderMonthText={renderMonthText}
632 renderWeekHeaderElement={renderWeekHeaderElement}
633 renderCalendarDay={renderCalendarDay}
634 renderDayContents={renderDayContents}
635 renderCalendarInfo={renderCalendarInfo}
636 renderMonthElement={renderMonthElement}
637 calendarInfoPosition={calendarInfoPosition}
638 isFocused={isFocused}
639 getFirstFocusableDay={this.getFirstFocusableDay}
640 onBlur={onBlur}
641 onTab={onTab}
642 onShiftTab={onShiftTab}
643 phrases={phrases}
644 daySize={daySize}
645 isRTL={isRTL}
646 showKeyboardShortcuts={showKeyboardShortcuts}
647 weekDayFormat={weekDayFormat}
648 dayAriaLabelFormat={dayAriaLabelFormat}
649 verticalHeight={verticalHeight}
650 noBorder={noBorder}
651 transitionDuration={transitionDuration}
652 verticalBorderSpacing={verticalBorderSpacing}
653 horizontalMonthPadding={horizontalMonthPadding}
654 />
655 );
656 }
657}
658
659DayPickerSingleDateController.propTypes = propTypes;
660DayPickerSingleDateController.defaultProps = defaultProps;