react-native-daterange-picker
Version:
Date range and single date picker for React Native (Supports iOS and Android)
507 lines (484 loc) • 12.7 kB
JavaScript
import momentDefault from "moment";
import PropTypes from "prop-types";
import React, { useState, useEffect, useCallback } from "react";
import {
StyleSheet,
Text,
TouchableOpacity,
TouchableWithoutFeedback,
View,
Image,
} from "react-native";
import Button from "./components/Button";
import Day from "./components/Day";
import Header from "./components/Header";
import { height, width } from "./modules";
import chevronL from "./assets/chevronL.png";
import chevronR from "./assets/chevronR.png";
const DateRangePicker = ({
moment,
startDate,
endDate,
onChange,
displayedDate,
minDate,
date,
maxDate,
range,
dayHeaderTextStyle,
dayHeaderStyle,
backdropStyle,
containerStyle,
selectedStyle,
selectedTextStyle,
disabledStyle,
dayStyle,
dayTextStyle,
disabledTextStyle,
headerTextStyle,
monthButtonsStyle,
headerStyle,
monthPrevButton,
monthNextButton,
children,
buttonContainerStyle,
buttonStyle,
buttonTextStyle,
presetButtons,
open,
}) => {
const [isOpen, setIsOpen] = useState(false);
const [weeks, setWeeks] = useState([]);
const [selecting, setSelecting] = useState(false);
const [dayHeaders, setDayHeaders] = useState([]);
const _moment = moment || momentDefault;
const mergedStyles = {
backdrop: {
...styles.backdrop,
...backdropStyle,
},
container: {
...styles.container,
...containerStyle,
},
header: {
...styles.header,
...headerStyle,
},
headerText: {
...styles.headerText,
...headerTextStyle,
},
monthButtons: {
...styles.monthButtons,
...monthButtonsStyle,
},
buttonContainer: {
...styles.buttonContainer,
...buttonContainerStyle,
},
monthButtons: {
...styles.monthButtons,
...monthButtonsStyle,
},
};
const _onOpen = () => {
if (typeof open !== "boolean") onOpen();
};
const _onClose = () => {
if (typeof open !== "boolean") onClose();
};
const onOpen = () => {
setIsOpen(true);
};
const onClose = () => {
setIsOpen(false);
setSelecting(false);
if (!endDate) {
onChange({
endDate: startDate,
});
}
};
const previousMonth = () => {
onChange({
displayedDate: _moment(displayedDate).subtract(1, "months"),
});
};
const nextMonth = () => {
onChange({
displayedDate: _moment(displayedDate).add(1, "months"),
});
};
const selected = useCallback((_date, _startDate, _endDate, __date) => {
return (
(_startDate &&
_endDate &&
_date.isBetween(_startDate, _endDate, null, "[]")) ||
(_startDate && _date.isSame(_startDate, "day")) ||
(__date && _date.isSame(__date, "day"))
);
}, []);
const disabled = useCallback((_date, _minDate, _maxDate) => {
return (
(_minDate && _date.isBefore(_minDate, "day")) ||
(_maxDate && _date.isAfter(_maxDate, "day"))
);
}, []);
const today = () => {
if (range) {
setSelecting(true);
onChange({
date: null,
startDate: _moment(),
endDate: null,
selecting: true,
displayedDate: _moment(),
});
} else {
setSelecting(false);
onChange({
date: _moment(),
startDate: null,
endDate: null,
displayedDate: _moment(),
});
}
};
const thisWeek = () => {
setSelecting(false);
onChange({
date: null,
startDate: _moment().startOf("week"),
endDate: _moment().endOf("week"),
displayedDate: _moment(),
});
};
const thisMonth = () => {
setSelecting(false);
onChange({
date: null,
startDate: _moment().startOf("month"),
endDate: _moment().endOf("month"),
displayedDate: _moment(),
});
};
const select = useCallback(
(day) => {
let _date = _moment(displayedDate);
_date.set("date", day);
if (range) {
if (selecting) {
if (_date.isBefore(startDate, "day")) {
setSelecting(true);
onChange({ startDate: _date });
} else {
setSelecting(!selecting);
onChange({ endDate: _date });
}
} else {
setSelecting(!selecting);
onChange({
date: null,
endDate: null,
startDate: _date,
});
}
} else {
onChange({
date: _date,
startDate: null,
endDate: null,
});
}
},
[_moment, displayedDate, onChange, range, selecting, startDate]
);
useEffect(() => {
if (typeof open === "boolean") {
if (open && !isOpen) onOpen();
else if (!open && isOpen) onClose();
}
}, [open]);
useEffect(() => {
function populateHeaders() {
let _dayHeaders = [];
for (let i = 0; i <= 6; ++i) {
let day = _moment(displayedDate).weekday(i).format("dddd").substr(0, 2);
_dayHeaders.push(
<Header
key={`dayHeader-${i}`}
day={day}
index={i}
dayHeaderTextStyle={dayHeaderTextStyle}
dayHeaderStyle={dayHeaderStyle}
/>
);
}
return _dayHeaders;
}
function populateWeeks() {
let _weeks = [];
let week = [];
let daysInMonth = displayedDate.daysInMonth();
let startOfMonth = _moment(displayedDate).set("date", 1);
let offset = startOfMonth.weekday();
week = week.concat(
Array.from({ length: offset }, (x, i) => (
<Day empty key={"empty-" + i} />
))
);
for (let i = 1; i <= daysInMonth; ++i) {
let _date = _moment(displayedDate).set("date", i);
let _selected = selected(_date, startDate, endDate, date);
let _disabled = disabled(_date, minDate, maxDate);
week.push(
<Day
key={`day-${i}`}
selectedStyle={selectedStyle}
selectedTextStyle={selectedTextStyle}
disabledStyle={disabledStyle}
dayStyle={dayStyle}
dayTextStyle={dayTextStyle}
disabledTextStyle={disabledTextStyle}
index={i}
selected={_selected}
disabled={_disabled}
select={select}
/>
);
if ((i + offset) % 7 === 0 || i === daysInMonth) {
if (week.length < 7) {
week = week.concat(
Array.from({ length: 7 - week.length }, (x, index) => (
<Day empty key={"empty-" + index} />
))
);
}
_weeks.push(
<View key={"weeks-" + i} style={styles.week}>
{week}
</View>
);
week = [];
}
}
return _weeks;
}
function populate() {
let _dayHeaders = populateHeaders();
let _weeks = populateWeeks();
setDayHeaders(_dayHeaders);
setWeeks(_weeks);
}
populate();
}, [
startDate,
endDate,
date,
_moment,
displayedDate,
dayHeaderTextStyle,
dayHeaderStyle,
selected,
disabled,
minDate,
maxDate,
selectedStyle,
selectedTextStyle,
disabledStyle,
dayStyle,
dayTextStyle,
disabledTextStyle,
select,
]);
const node = (
<View>
<TouchableWithoutFeedback onPress={_onOpen}>
{children ? (
children
) : (
<View>
<Text>Click me to show date picker</Text>
</View>
)}
</TouchableWithoutFeedback>
</View>
);
return isOpen ? (
<>
<View style={mergedStyles.backdrop}>
<TouchableWithoutFeedback
style={styles.closeTrigger}
onPress={_onClose}
>
<View style={styles.closeContainer} />
</TouchableWithoutFeedback>
<View>
<View style={mergedStyles.container}>
<View style={styles.header}>
<TouchableOpacity onPress={previousMonth}>
{monthPrevButton || (
<Image
resizeMode="contain"
style={mergedStyles.monthButtons}
source={chevronL}
></Image>
)}
</TouchableOpacity>
<Text style={mergedStyles.headerText}>
{displayedDate.format("MMMM") +
" " +
displayedDate.format("YYYY")}
</Text>
<TouchableOpacity onPress={nextMonth}>
{monthNextButton || (
<Image
resizeMode="contain"
style={mergedStyles.monthButtons}
source={chevronR}
/>
)}
</TouchableOpacity>
</View>
<View style={styles.calendar}>
{dayHeaders && (
<View style={styles.dayHeaderContainer}>{dayHeaders}</View>
)}
{weeks}
</View>
{presetButtons && (
<View style={mergedStyles.buttonContainer}>
<Button
buttonStyle={buttonStyle}
buttonTextStyle={buttonTextStyle}
onPress={today}
>
Today
</Button>
{range && (
<>
<Button
buttonStyle={buttonStyle}
buttonTextStyle={buttonTextStyle}
onPress={thisWeek}
>
This Week
</Button>
<Button
buttonStyle={buttonStyle}
buttonTextStyle={buttonTextStyle}
onPress={thisMonth}
>
This Month
</Button>
</>
)}
</View>
)}
</View>
</View>
</View>
{node}
</>
) : (
<>{node}</>
);
};
export default DateRangePicker;
DateRangePicker.defaultProps = {
dayHeaders: true,
range: false,
buttons: false,
presetButtons: false,
};
DateRangePicker.propTypes = {
onChange: PropTypes.func.isRequired,
startDate: PropTypes.object,
endDate: PropTypes.object,
displayedDate: PropTypes.object,
minDate: PropTypes.object,
maxDate: PropTypes.object,
backdropStyle: PropTypes.object,
containerStyle: PropTypes.object,
headerTextStyle: PropTypes.object,
monthButtonsStyle: PropTypes.object,
dayTextStyle: PropTypes.object,
dayStyle: PropTypes.object,
headerStyle: PropTypes.object,
buttonTextStyle: PropTypes.object,
buttonStyle: PropTypes.object,
buttonContainerStyle: PropTypes.object,
presetButtons: PropTypes.bool,
};
const styles = StyleSheet.create({
backdrop: {
backgroundColor: "rgba(0,0,0,0.6)",
position: "absolute",
width: width,
height: height,
justifyContent: "center",
alignItems: "center",
zIndex: 2147483647,
},
container: {
backgroundColor: "white",
borderRadius: 8,
width: width * 0.85,
},
closeTrigger: {
width: width,
height: height,
},
closeContainer: {
width: "100%",
height: "100%",
position: "absolute",
},
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
borderBottomColor: "#efefef",
borderBottomWidth: 0.5,
paddingLeft: 20,
paddingRight: 20,
paddingTop: 20,
paddingBottom: 15,
},
calendar: {
paddingTop: 20,
paddingBottom: 20,
width: "100%",
padding: 10,
},
headerText: {
fontSize: 16,
color: "black",
},
monthButtons: {
fontSize: 16,
color: "black",
},
dayHeaderContainer: {
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-evenly",
paddingBottom: 10,
},
week: {
flexDirection: "row",
flexWrap: "wrap",
justifyContent: "space-evenly",
},
buttonContainer: {
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
marginBottom: 10,
},
monthButtons: {
width: 32,
height: 32,
},
});