UNPKG

react-native-calendar-strip

Version:

Easy to use and visually stunning calendar component for React Native

638 lines (574 loc) 22.1 kB
/** * 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;