react-native-calendar-picker
Version:
Calendar Picker Component for React Native
617 lines (570 loc) • 19.8 kB
JavaScript
import React, { Component } from 'react';
import { View, Dimensions, Text } from 'react-native';
import { makeStyles } from './makeStyles';
import { Utils } from './Utils';
import HeaderControls from './HeaderControls';
import Weekdays from './Weekdays';
import DaysGridView from './DaysGridView';
import MonthSelector from './MonthSelector';
import YearSelector from './YearSelector';
import Scroller from './Scroller';
import { addMonths } from 'date-fns/addMonths';
import { getMonth } from 'date-fns/getMonth';
import { getYear } from 'date-fns/getYear';
import { isAfter } from 'date-fns/isAfter';
import { isBefore } from 'date-fns/isBefore';
import { isSameDay } from 'date-fns/isSameDay';
import { isSameMonth } from 'date-fns/isSameMonth';
import { startOfMonth } from 'date-fns/startOfMonth';
import { subMonths } from 'date-fns/subMonths';
export default class CalendarPicker extends Component {
constructor(props) {
super(props);
this.numMonthsScroll = 60; // 5 years
this.state = {
currentMonth: null,
currentYear: null,
currentView: props.initialView || 'days',
selectedStartDate: props.selectedStartDate && new Date(props.selectedStartDate),
selectedEndDate: props.selectedEndDate && new Date(props.selectedEndDate),
minDate: props.minDate && new Date(props.minDate),
maxDate: props.maxDate && new Date(props.maxDate),
styles: {},
...this.updateScaledStyles(props),
...this.updateMonthYear(props.initialDate),
...this.updateDisabledDates(props.disabledDates),
...this.updateMinMaxRanges(props.minRangeDuration, props.maxRangeDuration),
...this.createMonths(props, {}),
};
this.state.renderMonthParams = this.createMonthProps(this.state);
Text.defaultProps = {
...Text.defaultProps,
allowFontScaling: props.fontScaling,
}
}
static defaultProps = {
initialDate: new Date(),
scaleFactor: 375,
scrollable: false,
scrollDecelerationRate: 'normal',
onDateChange: () => {
console.log('onDateChange() not provided');
},
enableDateChange: true,
headingLevel: 1,
sundayColor: '#FFFFFF',
customDatesStyles: [],
previousTitle: 'Previous',
nextTitle: 'Next',
selectMonthTitle: 'Select Month in ',
selectYearTitle: 'Select Year',
horizontal: true,
selectedDayStyle: null,
selectedRangeStartStyle: null,
selectedRangeEndStyle: null,
selectedRangeStyle: null,
fontScaling: true,
};
componentDidUpdate(prevProps) {
let doStateUpdate = false;
let newStyles = {};
if (
prevProps.width !== this.props.width ||
prevProps.height !== this.props.height
) {
newStyles = this.updateScaledStyles(this.props);
doStateUpdate = true;
}
let newMonthYear = {};
if (!isSameDay(prevProps.initialDate, this.props.initialDate)) {
newMonthYear = this.updateMonthYear(this.props.initialDate);
doStateUpdate = true;
}
let selectedDateRanges = {};
const { selectedStartDate, selectedEndDate } = this.props;
if (selectedStartDate !== prevProps.selectedStartDate ||
selectedEndDate !== prevProps.selectedEndDate
) {
selectedDateRanges = {
selectedStartDate: selectedStartDate && new Date(selectedStartDate),
selectedEndDate: selectedEndDate && new Date(selectedEndDate)
};
doStateUpdate = true;
}
let disabledDates = {};
if (prevProps.disabledDates !== this.props.disabledDates) {
disabledDates = this.updateDisabledDates(this.props.disabledDates);
doStateUpdate = true;
}
let rangeDurations = {};
if (prevProps.minRangeDuration !== this.props.minRangeDuration ||
prevProps.maxRangeDuration !== this.props.maxRangeDuration
) {
const { minRangeDuration, maxRangeDuration } = this.props;
rangeDurations = this.updateMinMaxRanges(minRangeDuration, maxRangeDuration);
doStateUpdate = true;
}
let minMaxDates = {};
if (prevProps.minDate !== this.props.minDate ||
prevProps.minDate !== this.props.minDate
) {
minMaxDates.minDate = this.props.minDate && new Date(this.props.minDate);
minMaxDates.maxDate = this.props.maxDate && new Date(this.props.maxDate);
doStateUpdate = true;
}
if (prevProps.customDatesStyles !== this.props.customDatesStyles) {
// Update renderMonthParams on customDatesStyles change
doStateUpdate = true;
}
if (doStateUpdate) {
const newState = {
...newStyles,
...newMonthYear,
...selectedDateRanges,
...disabledDates,
...rangeDurations,
...minMaxDates,
};
let renderMonthParams = {};
const _state = { ...this.state, ...newState };
renderMonthParams = this.createMonthProps(_state);
this.setState({ ...newState, renderMonthParams });
}
}
updateScaledStyles = props => {
const {
scaleFactor,
selectedDayColor,
selectedDayTextColor,
todayBackgroundColor,
width,
height,
dayShape
} = props;
// The styles in makeStyles are intially scaled to this width
const containerWidth = width ? width : Dimensions.get('window').width;
const containerHeight = height ? height : Dimensions.get('window').height;
return {
styles: makeStyles({
containerWidth,
containerHeight,
scaleFactor,
selectedDayColor,
selectedDayTextColor,
todayBackgroundColor,
dayShape
})
};
}
updateMonthYear = (initialDate = this.props.initialDate, updateState) => {
const newState = {
currentMonth: parseInt(getMonth(new Date(initialDate))),
currentYear: parseInt(getYear(new Date(initialDate)))
};
if (updateState) {
this.setState(newState);
}
return newState;
}
updateDisabledDates = (_disabledDates = []) => {
let disabledDates = [];
if (_disabledDates) {
if (Array.isArray(_disabledDates)) {
// Convert input date into timestamp
_disabledDates.map(date => {
let thisDate = new Date(date);
thisDate.setHours(12, 0, 0, 0);
disabledDates.push(thisDate.valueOf());
});
}
else if (_disabledDates instanceof Function) {
disabledDates = _disabledDates;
}
}
return { disabledDates };
}
updateMinMaxRanges = (_minRangeDuration, _maxRangeDuration) => {
let minRangeDuration = [];
let maxRangeDuration = [];
if (_minRangeDuration) {
if (Array.isArray(_minRangeDuration)) {
_minRangeDuration.map(mrd => {
let thisDate = new Date(mrd.date);
thisDate.setHours(12, 0, 0, 0);
minRangeDuration.push({
date: thisDate.valueOf(),
minDuration: mrd.minDuration
});
});
} else {
minRangeDuration = _minRangeDuration;
}
}
if (_maxRangeDuration) {
if (Array.isArray(_maxRangeDuration)) {
_maxRangeDuration.map(mrd => {
let thisDate = new Date(mrd.date);
thisDate.setHours(12, 0, 0, 0);
maxRangeDuration.push({
date: thisDate.valueOf(),
maxDuration: mrd.maxDuration
});
});
} else {
maxRangeDuration = _maxRangeDuration;
}
}
return { minRangeDuration, maxRangeDuration };
}
handleOnPressDay = ({ year, month, day }) => {
const {
selectedStartDate: prevSelectedStartDate,
selectedEndDate: prevSelectedEndDate,
} = this.state;
const {
allowRangeSelection,
allowBackwardRangeSelect,
enableDateChange,
onDateChange,
} = this.props;
if (!enableDateChange) {
return;
}
const date = new Date(year, month, day, 12);
if (allowRangeSelection && prevSelectedStartDate && !prevSelectedEndDate) {
if (isAfter(date, prevSelectedStartDate)) {
const selectedStartDate = prevSelectedStartDate;
const selectedEndDate = date;
this.setState({
selectedEndDate,
renderMonthParams: this.createMonthProps({ ...this.state, selectedStartDate, selectedEndDate }),
});
// Sync end date with parent
onDateChange(date, Utils.END_DATE);
}
else if (allowBackwardRangeSelect) { // date is before selectedStartDate
// Flip dates so that start is always before end.
const selectedEndDate = new Date(prevSelectedStartDate);
const selectedStartDate = date;
this.setState({
selectedStartDate,
selectedEndDate,
renderMonthParams: this.createMonthProps({ ...this.state, selectedStartDate, selectedEndDate }),
}, () => {
// Sync both start and end dates with parent *after* state update.
onDateChange(this.state.selectedStartDate, Utils.START_DATE);
onDateChange(this.state.selectedEndDate, Utils.END_DATE);
});
}
} else {
const syncEndDate = !!prevSelectedEndDate;
const selectedStartDate = date;
const selectedEndDate = null;
this.setState({
selectedStartDate,
selectedEndDate,
renderMonthParams: this.createMonthProps({ ...this.state, selectedStartDate, selectedEndDate }),
}, () => {
// Sync start date with parent *after* state update.
onDateChange(this.state.selectedStartDate, Utils.START_DATE);
if (syncEndDate) {
// sync end date with parent - must be cleared if previously set.
onDateChange(null, Utils.END_DATE);
}
});
}
}
handleOnPressPrevious = () => {
const { currentMonth, currentYear } = this.state;
let previousMonth = currentMonth - 1;
let year = currentYear;
// if previousMonth is negative it means the current month is January,
// so we have to go back to previous year and set the current month to December
if (previousMonth < 0) {
previousMonth = 11;
year--;
}
const scrollFinisher = this.props.scrollable && this.scroller.scrollLeft;
this.handleOnPressFinisher({ year, month: previousMonth, scrollFinisher });
}
handleOnPressNext = () => {
const { currentMonth, currentYear } = this.state;
let nextMonth = currentMonth + 1;
let year = currentYear;
// if nextMonth is greater than 11 it means the current month is December,
// so we have to go forward to the next year and set the current month to January
if (nextMonth > 11) {
nextMonth = 0;
year++;
}
const scrollFinisher = this.props.scrollable && this.scroller.scrollRight;
this.handleOnPressFinisher({ year, month: nextMonth, scrollFinisher });
}
handleOnPressFinisher = ({ year, month, scrollFinisher, extraState }) => {
if (scrollFinisher) {
scrollFinisher();
}
else {
const currentMonth = parseInt(month);
const currentYear = parseInt(year);
const renderMonthParams = extraState || {
renderMonthParams: { ...this.state.renderMonthParams, month, year }
};
this.setState({ currentMonth, currentYear, ...renderMonthParams });
}
const currentMonthYear = new Date(year, month, 1, 12);
this.props.onMonthChange && this.props.onMonthChange(currentMonthYear);
}
handleOnPressYear = () => {
this.setState({
currentView: 'years'
});
}
handleOnPressMonth = () => {
this.setState({
currentView: 'months'
});
}
handleOnSelectMonthYear = ({ month, year }) => {
const currentYear = year;
const currentMonth = month;
const scrollableState = this.props.scrollable ? {
...this.createMonths(this.props, { currentYear, currentMonth }),
} : {};
const extraState = {
renderMonthParams: { ...this.state.renderMonthParams, month, year },
currentView: 'days',
...scrollableState,
};
this.handleOnPressFinisher({ month, year, extraState });
}
resetSelections = () => {
this.setState((state) => ({
selectedStartDate: null,
selectedEndDate: null,
renderMonthParams: {
...state.renderMonthParams,
selectedStartDate: null,
selectedEndDate: null,
}
}));
}
createMonthProps = state => {
return {
onPressDay: this.handleOnPressDay,
month: state.currentMonth,
year: state.currentYear,
styles: state.styles,
disabledDates: state.disabledDates,
minDate: state.minDate,
maxDate: state.maxDate,
minRangeDuration: state.minRangeDuration,
maxRangeDuration: state.maxRangeDuration,
selectedStartDate: state.selectedStartDate,
selectedEndDate: state.selectedEndDate,
enableDateChange: this.props.enableDateChange,
firstDay: this.props.startFromMonday ? 1 : this.props.firstDay,
allowRangeSelection: this.props.allowRangeSelection,
allowBackwardRangeSelect: this.props.allowBackwardRangeSelect,
showDayStragglers: this.props.showDayStragglers,
disabledDatesTextStyle: this.props.disabledDatesTextStyle,
textStyle: this.props.textStyle,
todayTextStyle: this.props.todayTextStyle,
selectedDayTextStyle: this.props.selectedDayTextStyle,
selectedRangeStartTextStyle: this.props.selectedRangeStartTextStyle,
selectedRangeEndTextStyle: this.props.selectedRangeEndTextStyle,
selectedDayStyle: this.props.selectedDayStyle,
selectedDisabledDatesTextStyle: this.props.selectedDisabledDatesTextStyle,
selectedRangeStartStyle: this.props.selectedRangeStartStyle,
selectedRangeStyle: this.props.selectedRangeStyle,
selectedRangeEndStyle: this.props.selectedRangeEndStyle,
customDatesStyles: this.props.customDatesStyles,
fontScaling: this.props.fontScaling,
};
}
createMonths = (props, { currentMonth, currentYear }) => {
if (!props.scrollable) {
return [];
}
const {
initialDate,
minDate,
maxDate,
restrictMonthNavigation,
} = props;
let monthsList = [];
let numMonths = this.numMonthsScroll;
let initialScrollerIndex = 0;
// Center start month in scroller. Visible month is either the initialDate
// prop, or the current month & year that has been selected.
let _initialDate = Number.isInteger(currentMonth) && Number.isInteger(currentYear) &&
new Date(currentYear, currentMonth, 1, 12);
_initialDate = _initialDate || initialDate;
let firstScrollerMonth = subMonths(_initialDate, numMonths / 2);
if (minDate && restrictMonthNavigation && isBefore(startOfMonth(firstScrollerMonth), startOfMonth(minDate))) {
firstScrollerMonth = new Date(minDate);
}
for (let i = 0; i < numMonths; i++) {
let month = addMonths(firstScrollerMonth, i);
if (maxDate && restrictMonthNavigation && isAfter(startOfMonth(month), startOfMonth(maxDate))) {
break;
}
if (isSameMonth(month, _initialDate)) {
initialScrollerIndex = i;
}
monthsList.push(month);
}
return {
monthsList,
initialScrollerIndex,
};
}
renderMonth(props) {
return (
<DaysGridView {...props} />
);
}
render() {
const {
currentView,
currentMonth,
currentYear,
minDate,
maxDate,
styles,
monthsList,
renderMonthParams,
initialScrollerIndex,
} = this.state;
const {
startFromMonday,
firstDay,
initialDate,
weekdays,
months,
previousComponent,
nextComponent,
previousTitle,
nextTitle,
previousTitleStyle,
nextTitleStyle,
monthTitleStyle,
yearTitleStyle,
textStyle,
restrictMonthNavigation,
headingLevel,
dayLabelsWrapper,
customDayHeaderStyles,
selectMonthTitle,
selectYearTitle,
monthYearHeaderWrapperStyle,
headerWrapperStyle,
onMonthChange,
scrollable,
horizontal,
scrollDecelarationRate,
} = this.props;
let content;
switch (currentView) {
case 'months':
content = (
<MonthSelector
styles={styles}
textStyle={textStyle}
title={selectMonthTitle}
currentYear={currentYear}
months={months}
minDate={minDate}
maxDate={maxDate}
onSelectMonth={this.handleOnSelectMonthYear}
headingLevel={headingLevel}
/>
);
break;
case 'years':
content = (
<YearSelector
styles={styles}
textStyle={textStyle}
title={selectYearTitle}
initialDate={new Date(initialDate)}
currentMonth={currentMonth}
currentYear={currentYear}
minDate={minDate}
maxDate={maxDate}
restrictNavigation={restrictMonthNavigation}
previousComponent={previousComponent}
nextComponent={nextComponent}
previousTitle={previousTitle}
nextTitle={nextTitle}
previousTitleStyle={previousTitleStyle}
nextTitleStyle={nextTitleStyle}
onSelectYear={this.handleOnSelectMonthYear}
headingLevel={headingLevel}
/>
);
break;
default:
content = (
<View styles={styles.calendar}>
<HeaderControls
styles={styles}
currentMonth={currentMonth}
currentYear={currentYear}
initialDate={new Date(initialDate)}
onPressPrevious={this.handleOnPressPrevious}
onPressNext={this.handleOnPressNext}
onPressMonth={this.handleOnPressMonth}
onPressYear={this.handleOnPressYear}
months={months}
previousComponent={previousComponent}
nextComponent={nextComponent}
previousTitle={previousTitle}
nextTitle={nextTitle}
previousTitleStyle={previousTitleStyle}
nextTitleStyle={nextTitleStyle}
monthTitleStyle={monthTitleStyle}
yearTitleStyle={yearTitleStyle}
textStyle={textStyle}
restrictMonthNavigation={restrictMonthNavigation}
minDate={minDate}
maxDate={maxDate}
headingLevel={headingLevel}
monthYearHeaderWrapperStyle={monthYearHeaderWrapperStyle}
headerWrapperStyle={headerWrapperStyle}
/>
<Weekdays
styles={styles}
firstDay={startFromMonday ? 1 : firstDay}
currentMonth={currentMonth}
currentYear={currentYear}
weekdays={weekdays}
textStyle={textStyle}
dayLabelsWrapper={dayLabelsWrapper}
customDayHeaderStyles={customDayHeaderStyles}
/>
{scrollable ?
<Scroller
ref={scroller => this.scroller = scroller}
data={monthsList}
renderMonth={this.renderMonth}
renderMonthParams={renderMonthParams}
maxSimultaneousMonths={this.numMonthsScroll}
initialRenderIndex={initialScrollerIndex}
minDate={minDate}
maxDate={maxDate}
restrictMonthNavigation={restrictMonthNavigation}
updateMonthYear={this.updateMonthYear}
onMonthChange={onMonthChange}
horizontal={horizontal}
scrollDecelarationRate={scrollDecelarationRate}
/>
:
this.renderMonth(renderMonthParams)
}
</View>
);
}
return content;
}
}