react-dates-rtl
Version:
Based on react-dates by airbnb [with RTL support]
402 lines (331 loc) • 11.6 kB
JSX
import React from 'react';
import PropTypes from 'prop-types';
import momentPropTypes from 'react-moment-proptypes';
import { forbidExtraProps, nonNegativeInteger } from 'airbnb-prop-types';
import moment from 'moment';
import { DayPickerPhrases } from '../defaultPhrases';
import getPhrasePropTypes from '../utils/getPhrasePropTypes';
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 ScrollableOrientationShape from '../shapes/ScrollableOrientationShape';
import {
START_DATE,
END_DATE,
HORIZONTAL_ORIENTATION,
DAY_SIZE,
} from '../../constants';
import DayPicker, { defaultProps as DayPickerDefaultProps } from './DayPicker';
const propTypes = forbidExtraProps({
startDate: momentPropTypes.momentObj,
endDate: momentPropTypes.momentObj,
onDatesChange: PropTypes.func,
focusedInput: FocusedInputShape,
onFocusChange: PropTypes.func,
onClose: 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: ScrollableOrientationShape,
withPortal: PropTypes.bool,
initialVisibleMonth: PropTypes.func,
daySize: nonNegativeInteger,
navPrev: PropTypes.node,
navNext: PropTypes.node,
onPrevMonthClick: PropTypes.func,
onNextMonthClick: PropTypes.func,
onOutsideClick: PropTypes.func,
renderDay: PropTypes.func,
renderCalendarInfo: PropTypes.func,
// accessibility
onBlur: PropTypes.func,
isFocused: PropTypes.bool,
showKeyboardShortcuts: PropTypes.bool,
// i18n
monthFormat: PropTypes.string,
phrases: PropTypes.shape(getPhrasePropTypes(DayPickerPhrases)),
isRTL: PropTypes.bool,
});
const defaultProps = {
startDate: undefined, // TODO: use null
endDate: undefined, // TODO: use null
onDatesChange() {},
focusedInput: null,
onFocusChange() {},
onClose() {},
keepOpenOnDateSelect: false,
minimumNights: 1,
isOutsideRange() {},
isDayBlocked() {},
isDayHighlighted() {},
// DayPicker props
enableOutsideDays: false,
numberOfMonths: 1,
orientation: HORIZONTAL_ORIENTATION,
withPortal: false,
initialVisibleMonth: DayPickerDefaultProps.initialVisibleMonth,
daySize: DAY_SIZE,
navPrev: null,
navNext: null,
onPrevMonthClick() {},
onNextMonthClick() {},
onOutsideClick() {},
renderDay: null,
renderCalendarInfo: null,
// accessibility
onBlur() {},
isFocused: false,
showKeyboardShortcuts: false,
// i18n
monthFormat: 'MMMM YYYY',
phrases: DayPickerPhrases,
isRTL: false,
};
export default class DayPickerRangeController extends React.Component {
constructor(props) {
super(props);
this.state = {
hoverDate: null,
phrases: props.phrases,
};
this.isTouchDevice = isTouchDevice();
this.today = moment();
this.onDayClick = this.onDayClick.bind(this);
this.onDayMouseEnter = this.onDayMouseEnter.bind(this);
this.onDayMouseLeave = this.onDayMouseLeave.bind(this);
this.getFirstFocusableDay = this.getFirstFocusableDay.bind(this);
}
componentWillReceiveProps(nextProps) {
const { focusedInput, phrases } = nextProps;
if (focusedInput !== this.props.focusedInput || phrases !== this.props.phrases) {
// set the appropriate CalendarDay phrase based on focusedInput
let chooseAvailableDate = phrases.chooseAvailableDate;
if (focusedInput === START_DATE) {
chooseAvailableDate = phrases.chooseAvailableStartDate;
} else if (focusedInput === END_DATE) {
chooseAvailableDate = phrases.chooseAvailableEndDate;
}
this.setState({
phrases: {
...phrases,
chooseAvailableDate,
},
});
}
}
componentWillUpdate() {
this.today = moment();
}
onDayClick(day, e) {
const { keepOpenOnDateSelect, minimumNights, onBlur } = this.props;
if (e) e.preventDefault();
if (this.isBlocked(day)) return;
const { focusedInput, onFocusChange, onClose } = this.props;
let { startDate, endDate } = this.props;
if (focusedInput === START_DATE) {
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;
onFocusChange(START_DATE);
} else if (isInclusivelyAfterDay(day, firstAllowedEndDate)) {
endDate = day;
if (!keepOpenOnDateSelect) {
onFocusChange(null);
onClose({ startDate, endDate });
}
} else {
startDate = day;
endDate = null;
}
}
this.props.onDatesChange({ startDate, endDate });
onBlur();
}
onDayMouseEnter(day) {
if (this.isTouchDevice) return;
this.setState({
hoverDate: day,
});
}
onDayMouseLeave() {
if (this.isTouchDevice) return;
this.setState({
hoverDate: null,
});
}
getFirstFocusableDay(newMonth) {
const { startDate, endDate, focusedInput, minimumNights, numberOfMonths } = this.props;
let focusedDate = newMonth.clone().startOf('month');
if (focusedInput === START_DATE && startDate) {
focusedDate = startDate.clone();
} else if (focusedInput === END_DATE && !endDate && startDate) {
focusedDate = startDate.clone().add(minimumNights, 'days');
} else if (focusedInput === END_DATE && endDate) {
focusedDate = endDate.clone();
}
if (this.isBlocked(focusedDate)) {
const days = [];
const lastVisibleDay = newMonth.clone().add(numberOfMonths - 1, 'months').endOf('month');
let currentDay = focusedDate.clone();
while (!currentDay.isAfter(lastVisibleDay)) {
currentDay = currentDay.clone().add(1, 'day');
days.push(currentDay);
}
const viableDays = days.filter(day => !this.isBlocked(day) && day.isAfter(focusedDate));
if (viableDays.length > 0) focusedDate = viableDays[0];
}
return focusedDate;
}
doesNotMeetMinimumNights(day) {
const { startDate, isOutsideRange, focusedInput, minimumNights } = this.props;
if (focusedInput !== END_DATE) return false;
if (startDate) {
const dayDiff = day.diff(startDate.clone().startOf('day').hour(12), '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,
daySize,
focusedInput,
renderDay,
renderCalendarInfo,
startDate,
endDate,
onBlur,
isFocused,
showKeyboardShortcuts,
isRTL,
} = this.props;
const { phrases } = this.state;
const modifiers = {
today: day => this.isToday(day),
blocked: day => this.isBlocked(day),
'blocked-calendar': day => isDayBlocked(day),
'blocked-out-of-range': day => isOutsideRange(day),
'highlighted-calendar': day => isDayHighlighted(day),
valid: day => !this.isBlocked(day),
// Modifiers are computed for every CalendarDay, so we omit where
// logically possible.
...startDate && {
'selected-start': day => this.isStartDate(day),
},
...endDate && {
'selected-end': day => this.isEndDate(day),
'blocked-minimum-nights': day => this.doesNotMeetMinimumNights(day),
},
...(startDate && endDate) && {
'selected-span': day => this.isInSelectedSpan(day),
'last-in-range': day => this.isLastInRange(day),
},
...!this.isTouchDevice && {
// 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),
},
};
return (
<DayPicker
ref={(ref) => { this.dayPicker = ref; }}
orientation={orientation}
enableOutsideDays={enableOutsideDays}
modifiers={modifiers}
numberOfMonths={numberOfMonths}
onDayClick={this.onDayClick}
onDayMouseEnter={this.onDayMouseEnter}
onDayMouseLeave={this.onDayMouseLeave}
onPrevMonthClick={onPrevMonthClick}
onNextMonthClick={onNextMonthClick}
monthFormat={monthFormat}
withPortal={withPortal}
hidden={!focusedInput}
initialVisibleMonth={initialVisibleMonth}
daySize={daySize}
onOutsideClick={onOutsideClick}
navPrev={navPrev}
navNext={navNext}
renderDay={renderDay}
renderCalendarInfo={renderCalendarInfo}
isFocused={isFocused}
getFirstFocusableDay={this.getFirstFocusableDay}
onBlur={onBlur}
showKeyboardShortcuts={showKeyboardShortcuts}
phrases={phrases}
isRTL={isRTL}
/>
);
}
}
DayPickerRangeController.propTypes = propTypes;
DayPickerRangeController.defaultProps = defaultProps;