react-native-calendar-strip
Version:
Easy to use and visually stunning calendar component for React Native
638 lines (574 loc) • 22.1 kB
JavaScript
/**
* Created by bogdanbegovic on 8/20/16.
*/
import React, { Component } from "react";
import PropTypes from "prop-types";
import { View, Animated, PixelRatio } from "react-native";
import moment from "moment";
import CalendarHeader from "./CalendarHeader";
import CalendarDay from "./CalendarDay";
import WeekSelector from "./WeekSelector";
import Scroller from "./Scroller";
import styles from "./Calendar.style.js";
/*
* Class CalendarStrip that is representing the whole calendar strip and contains CalendarDay elements
*
*/
class CalendarStrip extends Component {
static propTypes = {
style: PropTypes.any,
innerStyle: PropTypes.any,
calendarColor: PropTypes.string,
numDaysInWeek: PropTypes.number,
scrollable: PropTypes.bool,
scrollerPaging: PropTypes.bool,
externalScrollView: PropTypes.func,
startingDate: PropTypes.any,
selectedDate: PropTypes.any,
onDateSelected: PropTypes.func,
onWeekChanged: PropTypes.func,
onWeekScrollStart: PropTypes.func,
onWeekScrollEnd: PropTypes.func,
onHeaderSelected: PropTypes.func,
updateWeek: PropTypes.bool,
useIsoWeekday: PropTypes.bool,
minDate: PropTypes.any,
maxDate: PropTypes.any,
datesWhitelist: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
datesBlacklist: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
headerText: PropTypes.string,
markedDates: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
scrollToOnSetSelectedDate: PropTypes.bool,
showMonth: PropTypes.bool,
showDayName: PropTypes.bool,
showDayNumber: PropTypes.bool,
showDate: PropTypes.bool,
dayComponent: PropTypes.any,
leftSelector: PropTypes.any,
rightSelector: PropTypes.any,
iconLeft: PropTypes.any,
iconRight: PropTypes.any,
iconStyle: PropTypes.any,
iconLeftStyle: PropTypes.any,
iconRightStyle: PropTypes.any,
iconContainer: PropTypes.any,
maxDayComponentSize: PropTypes.number,
minDayComponentSize: PropTypes.number,
dayComponentHeight: PropTypes.number,
responsiveSizingOffset: PropTypes.number,
calendarHeaderContainerStyle: PropTypes.any,
calendarHeaderStyle: PropTypes.any,
calendarHeaderFormat: PropTypes.string,
calendarHeaderPosition: PropTypes.oneOf(["above", "below"]),
calendarAnimation: PropTypes.object,
daySelectionAnimation: PropTypes.object,
customDatesStyles: PropTypes.oneOfType([PropTypes.array, PropTypes.func]),
dateNameStyle: PropTypes.any,
dateNumberStyle: PropTypes.any,
dayContainerStyle: PropTypes.any,
weekendDateNameStyle: PropTypes.any,
weekendDateNumberStyle: PropTypes.any,
highlightDateNameStyle: PropTypes.any,
highlightDateNumberStyle: PropTypes.any,
highlightDateNumberContainerStyle: PropTypes.any,
highlightDateContainerStyle: PropTypes.any,
disabledDateNameStyle: PropTypes.any,
disabledDateNumberStyle: PropTypes.any,
markedDatesStyle: PropTypes.object,
disabledDateOpacity: PropTypes.number,
styleWeekend: PropTypes.bool,
locale: PropTypes.object,
shouldAllowFontScaling: PropTypes.bool,
useNativeDriver: PropTypes.bool,
upperCaseDays: PropTypes.bool,
};
static defaultProps = {
numDaysInWeek: 7,
useIsoWeekday: true,
showMonth: true,
showDate: true,
updateWeek: true,
iconLeft: require("./img/left-arrow-black.png"),
iconRight: require("./img/right-arrow-black.png"),
calendarHeaderFormat: "MMMM YYYY",
calendarHeaderPosition: "above",
datesWhitelist: undefined,
datesBlacklist: undefined,
disabledDateOpacity: 0.3,
customDatesStyles: [],
responsiveSizingOffset: 0,
innerStyle: { flex: 1 },
maxDayComponentSize: 80,
minDayComponentSize: 10,
shouldAllowFontScaling: true,
markedDates: [],
useNativeDriver: true,
scrollToOnSetSelectedDate: true,
upperCaseDays: true,
};
constructor(props) {
super(props);
this.numDaysScroll = 366; // prefer even number divisible by 3
if (props.locale) {
if (props.locale.name && props.locale.config) {
moment.updateLocale(props.locale.name, props.locale.config);
} else {
throw new Error(
"Locale prop is not in the correct format. \b Locale has to be in form of object, with params NAME and CONFIG!"
);
}
}
const startingDate = this.getInitialStartingDate();
const selectedDate = this.setLocale(this.props.selectedDate);
this.state = {
startingDate,
selectedDate,
datesList: [],
dayComponentWidth: 0,
height: 0,
monthFontSize: 0,
selectorSize: 0,
numVisibleDays: props.numDaysInWeek,
};
this.animations = [];
this.layout = {};
}
//Receiving props and set date states, minimizing state updates.
componentDidUpdate(prevProps, prevState) {
let startingDate = {};
let selectedDate = {};
let days = {};
let updateState = false;
if (!this.compareDates(prevProps.startingDate, this.props.startingDate) ||
!this.compareDates(prevProps.selectedDate, this.props.selectedDate) ||
prevProps.datesBlacklist !== this.props.datesBlacklist ||
prevProps.datesWhitelist !== this.props.datesWhitelist ||
prevProps.markedDates !== this.props.markedDates ||
prevProps.customDatesStyles !== this.props.customDatesStyles )
{
// Protect against undefined startingDate prop
let _startingDate = this.props.startingDate || this.state.startingDate;
startingDate = { startingDate: this.setLocale(_startingDate)};
selectedDate = { selectedDate: this.setLocale(this.props.selectedDate)};
days = this.createDays(startingDate.startingDate, selectedDate.selectedDate);
updateState = true;
}
if (updateState) {
this.setState({...startingDate, ...selectedDate, ...days });
}
}
shouldComponentUpdate(nextProps, nextState) {
// Extract selector icons since JSON.stringify fails on React component circular refs
let _nextProps = Object.assign({}, nextProps);
let _props = Object.assign({}, this.props);
delete _nextProps.leftSelector;
delete _nextProps.rightSelector;
delete _props.leftSelector;
delete _props.rightSelector;
return (
JSON.stringify(this.state) !== JSON.stringify(nextState) ||
JSON.stringify(_props) !== JSON.stringify(_nextProps) ||
this.props.leftSelector !== nextProps.leftSelector ||
this.props.rightSelector !== nextProps.rightSelector
);
}
// Check whether two datetimes are of the same value. Supports Moment date,
// JS date, or ISO 8601 strings.
// Returns true if the datetimes values are the same; false otherwise.
compareDates = (date1, date2) => {
if (date1 && date1.valueOf && date2 && date2.valueOf)
{
return moment(date1).isSame(date2, "day");
} else {
return JSON.stringify(date1) === JSON.stringify(date2);
}
}
//Function that checks if the locale is passed to the component and sets it to the passed date
setLocale = date => {
let _date = date && moment(date);
if (_date) {
_date.set({ hour: 12}); // keep date the same regardless of timezone shifts
if (this.props.locale) {
_date = _date.locale(this.props.locale.name);
}
}
return _date;
}
getInitialStartingDate = () => {
if (this.props.startingDate) {
return this.setLocale(this.props.startingDate);
} else {
// Fallback when startingDate isn't provided. However selectedDate
// may also be undefined, defaulting to today's date.
let date = this.setLocale(moment(this.props.selectedDate));
return this.props.useIsoWeekday ? date.startOf("isoweek") : date;
}
}
//Set startingDate to the previous week
getPreviousWeek = () => {
if (this.props.scrollable) {
this.scroller.scrollLeft();
return;
}
this.animations = [];
const previousWeekStartDate = this.state.startingDate.clone().subtract(1, "w");
const days = this.createDays(previousWeekStartDate);
this.setState({ startingDate: previousWeekStartDate, ...days });
}
//Set startingDate to the next week
getNextWeek = () => {
if (this.props.scrollable) {
this.scroller.scrollRight();
return;
}
this.animations = [];
const nextWeekStartDate = this.state.startingDate.clone().add(1, "w");
const days = this.createDays(nextWeekStartDate);
this.setState({ startingDate: nextWeekStartDate, ...days });
}
// Set the current visible week to the selectedDate
// When date param is undefined, an update always occurs (e.g. initialize)
updateWeekStart = (newStartDate, originalStartDate = this.state.startingDate) => {
if (!this.props.updateWeek) {
return originalStartDate;
}
let startingDate = moment(newStartDate).startOf("day");
let daysDiff = startingDate.diff(originalStartDate.startOf("day"), "days");
if (daysDiff === 0) {
return originalStartDate;
}
let addOrSubtract = daysDiff > 0 ? "add" : "subtract";
let adjustWeeks = daysDiff / 7;
adjustWeeks =
adjustWeeks > 0
? Math.floor(adjustWeeks)
: Math.ceil(Math.abs(adjustWeeks));
startingDate = originalStartDate[addOrSubtract](adjustWeeks, "w");
return this.setLocale(startingDate);
}
// updateWeekView allows external callers to update the visible week.
updateWeekView = date => {
if (this.props.scrollable) {
this.scroller.scrollToDate(date);
return;
}
this.animations = [];
let startingDate = moment(date);
startingDate = this.props.useIsoWeekday ? startingDate.startOf("isoweek") : startingDate;
const days = this.createDays(startingDate);
this.setState({startingDate, ...days});
}
//Handling press on date/selecting date
onDateSelected = selectedDate => {
let newState;
if (this.props.scrollable) {
newState = { selectedDate };
}
else {
newState = {
selectedDate,
...this.createDays(this.state.startingDate, selectedDate),
};
}
this.setState(() => newState);
const _selectedDate = selectedDate && selectedDate.clone();
this.props.onDateSelected && this.props.onDateSelected(_selectedDate);
}
// Get the currently selected date (Moment JS object)
getSelectedDate = () => {
if (!this.state.selectedDate || this.state.selectedDate.valueOf() === 0) {
return; // undefined (no date has been selected yet)
}
return this.state.selectedDate;
}
// Set the selected date. To clear the currently selected date, pass in 0.
setSelectedDate = date => {
let mDate = moment(date);
this.onDateSelected(mDate);
if (this.props.scrollToOnSetSelectedDate) {
// Scroll to selected date, centered in the week
const scrolledDate = moment(mDate);
scrolledDate.subtract(Math.floor(this.props.numDaysInWeek / 2), "days");
this.scroller.scrollToDate(scrolledDate);
}
}
// Gather animations from each day. Sequence animations must be started
// together to work around bug in RN Animated with individual starts.
registerAnimation = animation => {
this.animations.push(animation);
if (this.animations.length >= this.state.days.length) {
if (this.props.calendarAnimation?.type.toLowerCase() === "sequence") {
Animated.sequence(this.animations).start();
}
else {
Animated.parallel(this.animations).start();
}
}
}
// Responsive sizing based on container width.
// Debounce to prevent rapid succession of onLayout calls from thrashing.
onLayout = event => {
if (event.nativeEvent.layout.width === this.layout.width) {
return;
}
if (this.onLayoutTimer) {
clearTimeout(this.onLayoutTimer);
}
this.layout = event.nativeEvent.layout;
this.onLayoutTimer = setTimeout(() => {
this.onLayoutDebounce(this.layout);
this.onLayoutTimer = null;
}, 100);
}
onLayoutDebounce = layout => {
const {
numDaysInWeek,
responsiveSizingOffset,
maxDayComponentSize,
minDayComponentSize,
showMonth,
showDate,
scrollable,
dayComponentHeight,
} = this.props;
let csWidth = PixelRatio.roundToNearestPixel(layout.width);
let dayComponentWidth = csWidth / numDaysInWeek + responsiveSizingOffset;
dayComponentWidth = Math.min(dayComponentWidth, maxDayComponentSize);
dayComponentWidth = Math.max(dayComponentWidth, minDayComponentSize);
let numVisibleDays = numDaysInWeek;
let marginHorizontal;
if (scrollable) {
numVisibleDays = Math.floor(csWidth / dayComponentWidth);
// Scroller requires spacing between days
marginHorizontal = Math.round(dayComponentWidth * 0.05);
dayComponentWidth = Math.round(dayComponentWidth * 0.9);
}
let monthFontSize = Math.round(dayComponentWidth / 3.2);
let selectorSize = Math.round(dayComponentWidth / 2.5);
let height = showMonth ? monthFontSize : 0;
height += showDate ? dayComponentHeight || dayComponentWidth : 0;
selectorSize = Math.min(selectorSize, height);
this.setState({
dayComponentWidth,
dayComponentHeight: dayComponentHeight || dayComponentWidth,
height,
monthFontSize,
selectorSize,
marginHorizontal,
numVisibleDays,
},
() => this.setState( {...this.createDays(this.state.startingDate)} ));
}
getItemLayout = (data, index) => {
const length = this.state.height * 1.05; //include margin
return { length, offset: length * index, index }
}
updateMonthYear = (weekStartDate, weekEndDate) => {
this.setState({
weekStartDate,
weekEndDate,
});
}
createDayProps = selectedDate => {
return {
selectedDate,
onDateSelected: this.onDateSelected,
scrollable: this.props.scrollable,
datesWhitelist: this.props.datesWhitelist,
datesBlacklist: this.props.datesBlacklist,
showDayName: this.props.showDayName,
showDayNumber: this.props.showDayNumber,
dayComponent: this.props.dayComponent,
calendarColor: this.props.calendarColor,
dateNameStyle: this.props.dateNameStyle,
dateNumberStyle: this.props.dateNumberStyle,
dayContainerStyle: this.props.dayContainerStyle,
weekendDateNameStyle: this.props.weekendDateNameStyle,
weekendDateNumberStyle: this.props.weekendDateNumberStyle,
highlightDateNameStyle: this.props.highlightDateNameStyle,
highlightDateNumberStyle: this.props.highlightDateNumberStyle,
highlightDateNumberContainerStyle: this.props.highlightDateNumberContainerStyle,
highlightDateContainerStyle: this.props.highlightDateContainerStyle,
disabledDateNameStyle: this.props.disabledDateNameStyle,
disabledDateNumberStyle: this.props.disabledDateNumberStyle,
markedDatesStyle: this.props.markedDatesStyle,
disabledDateOpacity: this.props.disabledDateOpacity,
styleWeekend: this.props.styleWeekend,
calendarAnimation: this.props.calendarAnimation,
registerAnimation: this.registerAnimation,
daySelectionAnimation: this.props.daySelectionAnimation,
useNativeDriver: this.props.useNativeDriver,
customDatesStyles: this.props.customDatesStyles,
markedDates: this.props.markedDates,
height: this.state.dayComponentHeight,
width: this.state.dayComponentWidth,
marginHorizontal: this.state.marginHorizontal,
allowDayTextScaling: this.props.shouldAllowFontScaling,
upperCaseDays: this.props.upperCaseDays,
}
}
createDays = (startingDate, selectedDate = this.state.selectedDate) => {
const {
numDaysInWeek,
useIsoWeekday,
scrollable,
minDate,
maxDate,
onWeekChanged,
} = this.props;
let _startingDate = startingDate;
let days = [];
let datesList = [];
let numDays = numDaysInWeek;
let initialScrollerIndex;
if (scrollable) {
numDays = this.numDaysScroll;
// Center start date in scroller.
_startingDate = startingDate.clone().subtract(numDays/2, "days");
if (minDate && _startingDate.isBefore(minDate, "day")) {
_startingDate = moment(minDate);
}
}
for (let i = 0; i < numDays; i++) {
let date;
if (useIsoWeekday) {
// isoWeekday starts from Monday
date = this.setLocale(_startingDate.clone().isoWeekday(i + 1));
} else {
date = this.setLocale(_startingDate.clone().add(i, "days"));
}
if (scrollable) {
if (maxDate && date.isAfter(maxDate, "day")) {
break;
}
if (date.isSame(startingDate, "day")) {
initialScrollerIndex = i;
}
datesList.push({date});
}
else {
days.push(this.renderDay({
date,
key: date.format("YYYY-MM-DD"),
...this.createDayProps(selectedDate),
}));
datesList.push({date});
}
}
const newState = {
days,
datesList,
initialScrollerIndex,
};
if (!scrollable) {
const weekStartDate = datesList[0].date;
const weekEndDate = datesList[this.state.numVisibleDays - 1].date;
newState.weekStartDate = weekStartDate;
newState.weekEndDate = weekEndDate;
const _weekStartDate = weekStartDate && weekStartDate.clone();
const _weekEndDate = weekEndDate && weekEndDate.clone();
onWeekChanged && onWeekChanged(_weekStartDate, _weekEndDate);
}
// else Scroller sets weekStart/EndDate and fires onWeekChanged.
return newState;
}
renderDay(props) {
return (
<CalendarDay {...props} />
);
}
renderHeader() {
return ( this.props.showMonth &&
<CalendarHeader
calendarHeaderFormat={this.props.calendarHeaderFormat}
calendarHeaderContainerStyle={this.props.calendarHeaderContainerStyle}
calendarHeaderStyle={this.props.calendarHeaderStyle}
onHeaderSelected={this.props.onHeaderSelected}
weekStartDate={this.state.weekStartDate}
weekEndDate={this.state.weekEndDate}
fontSize={this.state.monthFontSize}
allowHeaderTextScaling={this.props.shouldAllowFontScaling}
headerText={this.props.headerText}
/>
);
}
renderWeekView(days) {
if (this.props.scrollable && this.state.datesList.length) {
return (
<Scroller
ref={scroller => this.scroller = scroller}
data={this.state.datesList}
pagingEnabled={this.props.scrollerPaging}
renderDay={this.renderDay}
renderDayParams={{...this.createDayProps(this.state.selectedDate)}}
maxSimultaneousDays={this.numDaysScroll}
initialRenderIndex={this.state.initialScrollerIndex}
minDate={this.props.minDate}
maxDate={this.props.maxDate}
updateMonthYear={this.updateMonthYear}
onWeekChanged={this.props.onWeekChanged}
onWeekScrollStart={this.props.onWeekScrollStart}
onWeekScrollEnd={this.props.onWeekScrollEnd}
externalScrollView={this.props.externalScrollView}
/>
);
}
return days;
}
render() {
// calendarHeader renders above or below of the dates & left/right selectors if dates are shown.
// However if dates are hidden, the header shows between the left/right selectors.
return (
<View
style={[
styles.calendarContainer,
{ backgroundColor: this.props.calendarColor },
this.props.style
]}
>
<View style={[this.props.innerStyle, { height: this.state.height }]}>
{this.props.showDate && this.props.calendarHeaderPosition === "above" &&
this.renderHeader()
}
<View style={styles.datesStrip}>
<WeekSelector
controlDate={this.props.minDate}
iconComponent={this.props.leftSelector}
iconContainerStyle={this.props.iconContainer}
iconInstanceStyle={this.props.iconLeftStyle}
iconStyle={this.props.iconStyle}
imageSource={this.props.iconLeft}
onPress={this.getPreviousWeek}
weekStartDate={this.state.weekStartDate}
weekEndDate={this.state.weekEndDate}
size={this.state.selectorSize}
/>
<View onLayout={this.onLayout} style={styles.calendarDates}>
{this.props.showDate ? (
this.renderWeekView(this.state.days)
) : (
this.renderHeader()
)}
</View>
<WeekSelector
controlDate={this.props.maxDate}
iconComponent={this.props.rightSelector}
iconContainerStyle={this.props.iconContainer}
iconInstanceStyle={this.props.iconRightStyle}
iconStyle={this.props.iconStyle}
imageSource={this.props.iconRight}
onPress={this.getNextWeek}
weekStartDate={this.state.weekStartDate}
weekEndDate={this.state.weekEndDate}
size={this.state.selectorSize}
/>
</View>
{this.props.showDate && this.props.calendarHeaderPosition === "below" &&
this.renderHeader()
}
</View>
</View>
);
}
}
export default CalendarStrip;