react-dates
Version:
A responsive and accessible date range picker component built with React
305 lines (249 loc) • 8.17 kB
JSX
import React, { PropTypes } from 'react';
import momentPropTypes from 'react-moment-proptypes';
import moment from 'moment';
import includes from 'array-includes';
import isTouchDevice from '../utils/isTouchDevice';
import isInclusivelyAfterDay from '../utils/isInclusivelyAfterDay';
import isNextDay from '../utils/isNextDay';
import isSameDay from '../utils/isSameDay';
import FocusedInputShape from '../shapes/FocusedInputShape';
import OrientationShape from '../shapes/OrientationShape';
import {
START_DATE,
END_DATE,
HORIZONTAL_ORIENTATION,
} from '../../constants';
import DayPicker from './DayPicker';
const propTypes = {
startDate: momentPropTypes.momentObj,
endDate: momentPropTypes.momentObj,
onDatesChange: PropTypes.func,
focusedInput: FocusedInputShape,
onFocusChange: PropTypes.func,
keepOpenOnDateSelect: PropTypes.bool,
minimumNights: PropTypes.number,
isOutsideRange: PropTypes.func,
isDayBlocked: PropTypes.func,
isDayHighlighted: PropTypes.func,
// DayPicker props
enableOutsideDays: PropTypes.bool,
numberOfMonths: PropTypes.number,
orientation: OrientationShape,
withPortal: PropTypes.bool,
hidden: PropTypes.bool,
initialVisibleMonth: PropTypes.func,
navPrev: PropTypes.node,
navNext: PropTypes.node,
onDayClick: PropTypes.func,
onDayMouseDown: PropTypes.func,
onDayMouseUp: PropTypes.func,
onDayMouseEnter: PropTypes.func,
onDayMouseLeave: PropTypes.func,
onDayTouchStart: PropTypes.func,
onDayTouchEnd: PropTypes.func,
onDayTouchTap: PropTypes.func,
onPrevMonthClick: PropTypes.func,
onNextMonthClick: PropTypes.func,
onOutsideClick: PropTypes.func,
// i18n
monthFormat: PropTypes.string,
};
const defaultProps = {
onDatesChange() {},
focusedInput: null,
onFocusChange() {},
keepOpenOnDateSelect: false,
minimumNights: 1,
isOutsideRange() {},
isDayBlocked() {},
isDayHighlighted() {},
// DayPicker props
enableOutsideDays: false,
numberOfMonths: 1,
orientation: HORIZONTAL_ORIENTATION,
withPortal: false,
hidden: false,
initialVisibleMonth: () => moment(),
navPrev: null,
navNext: null,
onDayClick() {},
onDayMouseDown() {},
onDayMouseUp() {},
onDayMouseEnter() {},
onDayMouseLeave() {},
onDayTouchStart() {},
onDayTouchTap() {},
onDayTouchEnd() {},
onPrevMonthClick() {},
onNextMonthClick() {},
onOutsideClick() {},
// i18n
monthFormat: 'MMMM YYYY',
};
export default class DayPickerRangeController extends React.Component {
constructor(props) {
super(props);
this.state = {
hoverDate: null,
};
this.isTouchDevice = isTouchDevice();
this.today = moment();
this.onDayClick = this.onDayClick.bind(this);
this.onDayMouseEnter = this.onDayMouseEnter.bind(this);
this.onDayMouseLeave = this.onDayMouseLeave.bind(this);
}
componentWillUpdate() {
this.today = moment();
}
onDayClick(day, modifiers, e) {
const { keepOpenOnDateSelect, minimumNights } = this.props;
if (e) e.preventDefault();
if (includes(modifiers, 'blocked')) return;
const { focusedInput } = this.props;
let { startDate, endDate } = this.props;
if (focusedInput === START_DATE) {
this.props.onFocusChange(END_DATE);
startDate = day;
if (isInclusivelyAfterDay(day, endDate)) {
endDate = null;
}
} else if (focusedInput === END_DATE) {
const firstAllowedEndDate = startDate && startDate.clone().add(minimumNights, 'days');
if (!startDate) {
endDate = day;
this.props.onFocusChange(START_DATE);
} else if (isInclusivelyAfterDay(day, firstAllowedEndDate)) {
endDate = day;
if (!keepOpenOnDateSelect) this.props.onFocusChange(null);
} else {
startDate = day;
endDate = null;
}
}
this.props.onDatesChange({ startDate, endDate });
}
onDayMouseEnter(day) {
if (this.isTouchDevice) return;
this.setState({
hoverDate: day,
});
}
onDayMouseLeave() {
if (this.isTouchDevice) return;
this.setState({
hoverDate: null,
});
}
doesNotMeetMinimumNights(day) {
const { startDate, isOutsideRange, focusedInput, minimumNights } = this.props;
if (focusedInput !== END_DATE) return false;
if (startDate) {
const dayDiff = day.diff(startDate, 'days');
return dayDiff < minimumNights && dayDiff >= 0;
}
return isOutsideRange(moment(day).subtract(minimumNights, 'days'));
}
isDayAfterHoveredStartDate(day) {
const { startDate, endDate, minimumNights } = this.props;
const { hoverDate } = this.state;
return !!startDate && !endDate && !this.isBlocked(day) && isNextDay(hoverDate, day) &&
minimumNights > 0 && isSameDay(hoverDate, startDate);
}
isEndDate(day) {
return isSameDay(day, this.props.endDate);
}
isHovered(day) {
return isSameDay(day, this.state.hoverDate);
}
isInHoveredSpan(day) {
const { startDate, endDate } = this.props;
const { hoverDate } = this.state;
const isForwardRange = !!startDate && !endDate &&
(day.isBetween(startDate, hoverDate) ||
isSameDay(hoverDate, day));
const isBackwardRange = !!endDate && !startDate &&
(day.isBetween(hoverDate, endDate) ||
isSameDay(hoverDate, day));
const isValidDayHovered = hoverDate && !this.isBlocked(hoverDate);
return (isForwardRange || isBackwardRange) && isValidDayHovered;
}
isInSelectedSpan(day) {
const { startDate, endDate } = this.props;
return day.isBetween(startDate, endDate);
}
isLastInRange(day) {
return this.isInSelectedSpan(day) && isNextDay(day, this.props.endDate);
}
isStartDate(day) {
return isSameDay(day, this.props.startDate);
}
isBlocked(day) {
const { isDayBlocked, isOutsideRange } = this.props;
return isDayBlocked(day) || isOutsideRange(day) || this.doesNotMeetMinimumNights(day);
}
isToday(day) {
return isSameDay(day, this.today);
}
render() {
const {
isDayBlocked,
isDayHighlighted,
isOutsideRange,
numberOfMonths,
orientation,
monthFormat,
navPrev,
navNext,
onOutsideClick,
onPrevMonthClick,
onNextMonthClick,
withPortal,
enableOutsideDays,
initialVisibleMonth,
focusedInput,
} = this.props;
const modifiers = {
today: day => this.isToday(day),
blocked: day => this.isBlocked(day),
'blocked-calendar': day => isDayBlocked(day),
'blocked-out-of-range': day => isOutsideRange(day),
'blocked-minimum-nights': day => this.doesNotMeetMinimumNights(day),
'highlighted-calendar': day => isDayHighlighted(day),
valid: day => !this.isBlocked(day),
// before anything has been set or after both are set
hovered: day => this.isHovered(day),
// while start date has been set, but end date has not been
'hovered-span': day => this.isInHoveredSpan(day),
'after-hovered-start': day => this.isDayAfterHoveredStartDate(day),
'last-in-range': day => this.isLastInRange(day),
// once a start date and end date have been set
'selected-start': day => this.isStartDate(day),
'selected-end': day => this.isEndDate(day),
'selected-span': day => this.isInSelectedSpan(day),
};
return (
<DayPicker
ref={ref => { this.dayPicker = ref; }}
orientation={orientation}
enableOutsideDays={enableOutsideDays}
modifiers={modifiers}
numberOfMonths={numberOfMonths}
onDayMouseEnter={this.onDayMouseEnter}
onDayMouseLeave={this.onDayMouseLeave}
onDayMouseDown={this.onDayClick}
onDayTouchTap={this.onDayClick}
onPrevMonthClick={onPrevMonthClick}
onNextMonthClick={onNextMonthClick}
monthFormat={monthFormat}
withPortal={withPortal}
hidden={!focusedInput}
initialVisibleMonth={initialVisibleMonth}
onOutsideClick={onOutsideClick}
navPrev={navPrev}
navNext={navNext}
/>
);
}
}
DayPickerRangeController.propTypes = propTypes;
DayPickerRangeController.defaultProps = defaultProps;