UNPKG

react-native-calendar-strip

Version:

Easy to use and visually stunning calendar component for React Native

603 lines (547 loc) 19 kB
/** * Created by bogdanbegovic on 8/20/16. */ import React, { Component } from "react"; import PropTypes from "prop-types"; import { View, Animated, Easing } from "react-native"; import moment from "moment"; import CalendarHeader from "./CalendarHeader"; import CalendarDay from "./CalendarDay"; import WeekSelector from "./WeekSelector"; import styles from "./Calendar.style.js"; /* * Class CalendarStrip that is representing the whole calendar strip and contains CalendarDay elements * */ export default class CalendarStrip extends Component { static propTypes = { style: PropTypes.any, innerStyle: PropTypes.any, calendarColor: PropTypes.string, startingDate: PropTypes.any, selectedDate: PropTypes.any, onDateSelected: PropTypes.func, onWeekChanged: PropTypes.func, useIsoWeekday: PropTypes.bool, minDate: PropTypes.any, maxDate: PropTypes.any, datesWhitelist: PropTypes.array, datesBlacklist: PropTypes.array, showMonth: PropTypes.bool, showDayName: PropTypes.bool, showDayNumber: PropTypes.bool, showDate: PropTypes.bool, 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, responsiveSizingOffset: PropTypes.number, calendarHeaderStyle: PropTypes.any, calendarHeaderFormat: PropTypes.string, calendarAnimation: PropTypes.object, daySelectionAnimation: PropTypes.object, customDatesStyles: PropTypes.array, dateNameStyle: PropTypes.any, dateNumberStyle: PropTypes.any, weekendDateNameStyle: PropTypes.any, weekendDateNumberStyle: PropTypes.any, highlightDateNameStyle: PropTypes.any, highlightDateNumberStyle: PropTypes.any, disabledDateNameStyle: PropTypes.any, disabledDateNumberStyle: PropTypes.any, disabledDateOpacity: PropTypes.number, styleWeekend: PropTypes.bool, locale: PropTypes.object }; static defaultProps = { useIsoWeekday: true, showMonth: true, showDate: true, iconLeft: require("./img/left-arrow-black.png"), iconRight: require("./img/right-arrow-black.png"), calendarHeaderFormat: "MMMM YYYY", datesWhitelist: undefined, datesBlacklist: undefined, disabledDateOpacity: 0.3, customDatesStyles: [], responsiveSizingOffset: 0, innerStyle: { flex: 1 }, maxDayComponentSize: 80, minDayComponentSize: 10 }; constructor(props) { super(props); this.numDaysInWeek = 7; if (props.locale) { if (props.locale.name && props.locale.config) { moment.locale(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(moment(this.props.selectedDate)); const weekData = this.updateWeekData(startingDate, selectedDate); this.state = { startingDate, selectedDate, ...weekData, dayComponentWidth: 0, height: 0, monthFontSize: 0, selectorSize: 0 }; this.resetAnimation(); this.componentDidMount = this.componentDidMount.bind(this); this.componentWillUpdate = this.componentWillUpdate.bind(this); this.updateWeekData = this.updateWeekData.bind(this); this.getPreviousWeek = this.getPreviousWeek.bind(this); this.getNextWeek = this.getNextWeek.bind(this); this.onDateSelected = this.onDateSelected.bind(this); this.isDateSelected = this.isDateSelected.bind(this); this.animate = this.animate.bind(this); this.resetAnimation = this.resetAnimation.bind(this); } componentDidMount() { // Animate showing of CalendarDay elements this.animate(); } //Receiving props and set date states, minimizing state updates. componentWillReceiveProps(nextProps) { let selectedDate = {}, startingDate = {}, weekData = {}; let updateState = false; if (!this.compareDates(nextProps.selectedDate, this.props.selectedDate)) { updateState = true; selectedDate = { selectedDate: this.setLocale(moment(nextProps.selectedDate)) }; startingDate = { startingDate: this.updateWeekStart(selectedDate.selectedDate) }; weekData = this.updateWeekData( startingDate.startingDate, selectedDate.selectedDate, nextProps ); } if ( !updateState && !this.compareDates(nextProps.startingDate, this.props.startingDate) ) { updateState = true; startingDate = this.setLocale(moment(nextProps.startingDate)); startingDate = { startingDate: this.updateWeekStart(startingDate) }; weekData = this.updateWeekData( startingDate.startingDate, this.state.selectedDate, nextProps ); } if ( !updateState && (JSON.stringify(nextProps.datesBlacklist) !== JSON.stringify(this.props.datesBlacklist) || JSON.stringify(nextProps.datesWhitelist) !== JSON.stringify(this.props.datesWhitelist) || JSON.stringify(nextProps.customDatesStyles) !== JSON.stringify(this.props.customDatesStyles)) ) { updateState = true; // No need to update week start here startingDate = { startingDate: this.setLocale(moment(nextProps.startingDate)) }; weekData = this.updateWeekData( startingDate.startingDate, this.state.selectedDate, nextProps ); } if (updateState) { this.setState({ ...selectedDate, ...startingDate, ...weekData }); } } //Only animate CalendarDays if the selectedDate is the same //Prevents animation on pressing on a date componentWillUpdate(nextProps, nextState) { if (nextState.selectedDate === this.state.selectedDate) { this.resetAnimation(); this.animate(); } } // 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 && date1.valueOf() === date2.valueOf() ) { return true; } 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 moment instance setLocale(momentInstance) { if (this.props.locale) { momentInstance.locale(this.props.locale.name); } return momentInstance; } getInitialStartingDate() { if (this.props.startingDate) { return this.setLocale(moment(this.props.startingDate)); } else { return this.setLocale(moment(this.props.selectedDate)).isoWeekday(1); } } //Set startingDate to the previous week getPreviousWeek() { const previousWeekStartDate = this.state.startingDate .clone() .subtract(1, "w"); if (this.props.onWeekChanged) { if (this.props.useIsoWeekday) { this.props.onWeekChanged( previousWeekStartDate.clone().startOf("isoweek") ); } else { this.props.onWeekChanged(previousWeekStartDate.clone()); } } let weekData = this.updateWeekData(previousWeekStartDate); this.setState({ startingDate: previousWeekStartDate, ...weekData }); } //Set startingDate to the next week getNextWeek() { const nextWeekStartDate = this.state.startingDate.clone().add(1, "w"); if (this.props.onWeekChanged) { if (this.props.useIsoWeekday) { this.props.onWeekChanged(nextWeekStartDate.clone().startOf("isoweek")); } else { this.props.onWeekChanged(nextWeekStartDate.clone()); } } let weekData = this.updateWeekData(nextWeekStartDate); this.setState({ startingDate: nextWeekStartDate, ...weekData }); } // 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) { 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 startingDate; } // Get & update week states for the week based on the startingDate updateWeekData( startingDate, selectedDate = this.state.selectedDate, props = this.props ) { const me = this; let datesForWeek = []; let datesAllowedForWeek = []; let datesSelectedForWeek = []; let datesCustomStylesForWeek = []; for (let i = 0; i < this.numDaysInWeek; i++) { let date; if (props.useIsoWeekday) { // isoWeekday starts from Monday date = me.setLocale(startingDate.clone().isoWeekday(i + 1)); } else { date = me.setLocale(startingDate.clone().add(i, "days")); } datesForWeek.push(date); datesAllowedForWeek.push(this.isDateAllowed(date, props)); datesSelectedForWeek.push(this.isDateSelected(date, selectedDate)); datesCustomStylesForWeek.push(this.getCustomDateStyle(date, props)); } return { datesForWeek, datesAllowedForWeek, datesSelectedForWeek, datesCustomStylesForWeek }; } //Handling press on date/selecting date onDateSelected(selectedDate) { this.setState({ selectedDate, ...this.updateWeekData(this.state.startingDate, selectedDate) }); this.props.onDateSelected && this.props.onDateSelected(selectedDate); } // Check whether date is allowed isDateAllowed(date, props = this.props) { // datesBlacklist entries override datesWhitelist if (props.datesBlacklist !== undefined) { for (let disallowed of props.datesBlacklist) { // Blacklist start/end object if (disallowed.start && disallowed.end) { if (date.isBetween(disallowed.start, disallowed.end, "day", "[]")) { return false; } } else { if (date.isSame(disallowed, "day")) { return false; } } } } if (props.datesWhitelist === undefined) { return true; } // Whitelist for (let allowed of props.datesWhitelist) { // start/end object if (allowed.start && allowed.end) { if (date.isBetween(allowed.start, allowed.end, "day", "[]")) { return true; } } else { if (date.isSame(allowed, "day")) { return true; } } } return false; } //Function to check if provided date is the same as selected one, hence date is selected //using isSame moment query with 'day' param so that it check years, months and day isDateSelected(date, selectedDate = this.state.selectedDate) { return date.isSame(selectedDate, "day"); } // Get the currently selected date (Moment JS object) getSelectedDate(date) { if (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); // Update week view only if date is not cleared (0). if (date !== 0) { this.updateWeekStart(mDate); } } getCustomDateStyle(date, props = this.props) { for (let customDateStyle of props.customDatesStyles) { if (customDateStyle.endDate) { // Range if ( date.isBetween( customDateStyle.startDate, customDateStyle.endDate, "day", "[]" ) ) { return customDateStyle; } } else { // Single date if (date.isSame(customDateStyle.startDate, "day")) { return customDateStyle; } } } } //Function for reseting animations resetAnimation() { this.animatedValue = []; for (let i = 0; i < this.numDaysInWeek; i++) { this.animatedValue.push(new Animated.Value(0)); } } //Function to animate showing the CalendarDay elements. //Possible cases for animations are sequence and parallel animate() { if (this.props.calendarAnimation) { let animations = []; for (let i = 0; i < this.numDaysInWeek; i++) { animations.push( Animated.timing(this.animatedValue[i], { toValue: 1, duration: this.props.calendarAnimation.duration, easing: Easing.linear }) ); } if (this.props.calendarAnimation.type.toLowerCase() === "sequence") { Animated.sequence(animations).start(); } else { if (this.props.calendarAnimation.type.toLowerCase() === "parallel") { Animated.parallel(animations).start(); } else { throw new Error( "CalendarStrip Error! Type of animation is incorrect!" ); } } } } // Responsive sizing based on container width. onLayout(event) { let csWidth = event.nativeEvent.layout.width; let numElements = this.numDaysInWeek; if ( Array.isArray(this.props.leftSelector) && this.props.leftSelector.length > 0 ) { numElements++; } if ( Array.isArray(this.props.rightSelector) && this.props.rightSelector.length > 0 ) { numElements++; } let dayComponentWidth = (csWidth / numElements) + this.props.responsiveSizingOffset; dayComponentWidth = Math.min( dayComponentWidth, this.props.maxDayComponentSize ); dayComponentWidth = Math.max( dayComponentWidth, this.props.minDayComponentSize ); let monthFontSize = Math.round(dayComponentWidth / 3.2); let selectorSize = Math.round(dayComponentWidth / 2.5); let height = this.props.showMonth ? monthFontSize : 0; height += this.props.showDate ? dayComponentWidth : 0; // assume square element sizes selectorSize = Math.min(selectorSize, height); this.setState({ dayComponentWidth, height, monthFontSize, selectorSize }); } render() { let datesForWeek = this.state.datesForWeek; let datesRender = []; for (let i = 0; i < datesForWeek.length; i++) { let enabled = this.state.datesAllowedForWeek[i]; let calendarDay = ( <CalendarDay date={datesForWeek[i]} selected={this.state.datesSelectedForWeek[i]} enabled={enabled} showDayName={this.props.showDayName} showDayNumber={this.props.showDayNumber} onDateSelected={() => enabled && this.onDateSelected(datesForWeek[i])} calendarColor={this.props.calendarColor} dateNameStyle={this.props.dateNameStyle} dateNumberStyle={this.props.dateNumberStyle} weekendDateNameStyle={this.props.weekendDateNameStyle} weekendDateNumberStyle={this.props.weekendDateNumberStyle} highlightDateNameStyle={this.props.highlightDateNameStyle} highlightDateNumberStyle={this.props.highlightDateNumberStyle} disabledDateNameStyle={this.props.disabledDateNameStyle} disabledDateNumberStyle={this.props.disabledDateNumberStyle} disabledDateOpacity={this.props.disabledDateOpacity} styleWeekend={this.props.styleWeekend} daySelectionAnimation={this.props.daySelectionAnimation} customStyle={this.state.datesCustomStylesForWeek[i]} size={this.state.dayComponentWidth} /> ); datesRender.push( this.props.calendarAnimation ? <Animated.View key={i} style={{ opacity: this.animatedValue[i], flex: 1 }} > {calendarDay} </Animated.View> : <View key={i} style={{ flex: 1 }}> {calendarDay} </View> ); } let calendarHeader = this.props.showMonth && <CalendarHeader calendarHeaderFormat={this.props.calendarHeaderFormat} calendarHeaderStyle={this.props.calendarHeaderStyle} datesForWeek={this.state.datesForWeek} fontSize={this.state.monthFontSize} />; // calendarHeader renders above 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 }]} onLayout={this.onLayout.bind(this)} > {this.props.showDate && calendarHeader} <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} weekEndDate={ this.state.datesForWeek[this.state.datesForWeek.length - 1] } weekStartDate={this.state.datesForWeek[0]} size={this.state.selectorSize} /> {this.props.showDate ? <View style={styles.calendarDates}> {datesRender} </View> : calendarHeader} <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} weekEndDate={ this.state.datesForWeek[this.state.datesForWeek.length - 1] } weekStartDate={this.state.datesForWeek[0]} size={this.state.selectorSize} /> </View> </View> </View> ); } }