UNPKG

react-native-toggle-calendar

Version:

Horizontal as well as Grid calendar built on top of react-native-calendars

491 lines (443 loc) 16.6 kB
import React, {Component} from 'react'; import { View, ViewPropTypes, ScrollView, Dimensions, ActivityIndicator, Platform } from 'react-native'; import PropTypes from 'prop-types'; import XDate from 'xdate'; import dateutils from '../dateutils'; import {xdateToData, parseDate} from '../interface'; import styleConstructor from './style'; import Day from './day/basic'; import UnitDay from './day/period'; import MultiDotDay from './day/multi-dot'; import MultiPeriodDay from './day/multi-period'; import SingleDay from './day/custom'; import CalendarHeader from './header'; import shouldComponentUpdate from './updater'; //Fallback when RN version is < 0.44 const viewPropTypes = ViewPropTypes || View.propTypes; const EmptyArray = []; // horizontal calendar will be scrolled to (offset * viewport width) to keep selected date visible let horizontalScrollViewOffset = 0; // to throttle back-to-back triggering of onPressArrowRight in horizontal calendar let onPressArrowRightTriggered = false; // to throttle back-to-back triggering of onPressArrowLeft in horizontal calendar let onPressArrowLeftTriggered = false; const timezoneOffset = new Date().getTimezoneOffset() * 60 * 1000; class Calendar extends Component { static propTypes = { // Specify theme properties to override specific styles for calendar parts. Default = {} theme: PropTypes.object, // Collection of dates that have to be marked. Default = {} markedDates: PropTypes.object, // Specify style for calendar container element. Default = {} style: viewPropTypes.style, // Initially visible month. Default = Date() current: PropTypes.any, // Minimum date that can be selected, dates before minDate will be grayed out. Default = undefined minDate: PropTypes.any, // Maximum date that can be selected, dates after maxDate will be grayed out. Default = undefined maxDate: PropTypes.any, // If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday. firstDay: PropTypes.number, // Date marking style [simple/period/multi-dot/multi-period]. Default = 'simple' markingType: PropTypes.string, // Hide month navigation arrows. Default = false hideArrows: PropTypes.bool, // Display loading indicador. Default = false displayLoadingIndicator: PropTypes.bool, // Do not show days of other months in month page. Default = false hideExtraDays: PropTypes.bool, // Handler which gets executed on day press. Default = undefined onDayPress: PropTypes.func, // Handler which gets executed on day long press. Default = undefined onDayLongPress: PropTypes.func, // Handler which gets executed when visible month changes in calendar. Default = undefined onMonthChange: PropTypes.func, onVisibleMonthsChange: PropTypes.func, // Replace default arrows with custom ones (direction can be 'left' or 'right') renderArrow: PropTypes.func, // Provide custom day rendering component dayComponent: PropTypes.any, // Month format in calendar title. Formatting values: http://arshaw.com/xdate/#Formatting monthFormat: PropTypes.string, // Disables changing month when click on days of other months (when hideExtraDays is false). Default = false disableMonthChange: PropTypes.bool, // Hide day names. Default = false hideDayNames: PropTypes.bool, // Disable days by default. Default = false disabledByDefault: PropTypes.bool, // Show week numbers. Default = false showWeekNumbers: PropTypes.bool, // Handler which gets executed when press arrow icon left. It receive a callback can go back month onPressArrowLeft: PropTypes.func, // Handler which gets executed when press arrow icon left. It receive a callback can go next month onPressArrowRight: PropTypes.func, // Provide custom calendar header rendering component calendarHeaderComponent: PropTypes.any, // data which is passed to calendar header, useful only when implementing custom calendar header headerData: PropTypes.object, // Handler which gets executed when press list icon. It will set calendar to horizontal onPressListView: PropTypes.func, // Handler which gets executed when press grid icon. It will set calendar to grid onPressGridView: PropTypes.func, // to show horizontal calendar with scroll horizontal: PropTypes.bool, // to automatically scroll horizontal calendar to keep selected date in view autoHorizontalScroll: PropTypes.bool, // how many past days to be shown, if this is set - autoHorizontalScroll will not work showPastDatesInHorizontal: PropTypes.number, // offset to decide when to trigger onPressArrowRight in horizontal calendar, // 0 means when rightmost day is reached, undefined means no auto onPressArrowRight triggering horizontalEndReachedThreshold: PropTypes.number, // offset to decide when to trigger onPressArrowLeft in horizontal calendar, // 0 means when leftmost day is reached, undefined means no auto onPressArrowLeft triggering horizontalStartReachedThreshold: PropTypes.number, // to show a loader loading: PropTypes.bool, // provide a custom loader component LoaderComponent: PropTypes.any }; constructor(props) { super(props); this.style = styleConstructor(this.props.theme); let currentMonth; if (props.current) { currentMonth = parseDate(props.current); } else { currentMonth = XDate(); } this.state = { currentMonth, horizontal: props.horizontal }; this.updateMonth = this.updateMonth.bind(this); this.addMonth = this.addMonth.bind(this); this.pressDay = this.pressDay.bind(this); this.longPressDay = this.longPressDay.bind(this); this.shouldComponentUpdate = shouldComponentUpdate; this.onHorizontalCalendarScroll = this.onHorizontalCalendarScroll.bind(this); this.horizontalScrollViewRef = React.createRef(); } componentWillReceiveProps(nextProps) { const current= parseDate(nextProps.current); if (current && current.toString('yyyy MM') !== this.state.currentMonth.toString('yyyy MM')) { this.setState({ currentMonth: current.clone() }); } this.setState({ horizontal: nextProps.horizontal }); } // scroll the horizontal calendar so that selected date is visible componentDidUpdate() { const horizontalScrollView = this.horizontalScrollViewRef.current; if (horizontalScrollView && this.props.autoHorizontalScroll && (this.props.showPastDatesInHorizontal === undefined)) { const windowWidth = Dimensions.get('window').width; horizontalScrollView.scrollTo({ x: horizontalScrollViewOffset * windowWidth, animated: true }); } } updateMonth(day, doNotTriggerListeners) { if (day.toString('yyyy MM') === this.state.currentMonth.toString('yyyy MM')) { return; } this.setState({ currentMonth: day.clone() }, () => { if (!doNotTriggerListeners) { const currMont = this.state.currentMonth.clone(); if (this.props.onMonthChange) { this.props.onMonthChange(xdateToData(currMont)); } if (this.props.onVisibleMonthsChange) { this.props.onVisibleMonthsChange([xdateToData(currMont)]); } } }); } _handleDayInteraction(date, interaction) { const day = parseDate(date); const minDate = parseDate(this.props.minDate); const maxDate = parseDate(this.props.maxDate); if (!(minDate && !dateutils.isGTE(day, minDate)) && !(maxDate && !dateutils.isLTE(day, maxDate))) { const shouldUpdateMonth = this.props.disableMonthChange === undefined || !this.props.disableMonthChange; if (shouldUpdateMonth) { this.updateMonth(day); } if (interaction) { interaction(xdateToData(day)); } } } pressDay(date) { this._handleDayInteraction(date, this.props.onDayPress); } longPressDay(date) { this._handleDayInteraction(date, this.props.onDayLongPress); } addMonth(count) { this.updateMonth(this.state.currentMonth.clone().addMonths(count, true)); } renderDay(day, id) { const minDate = parseDate(this.props.minDate); const maxDate = parseDate(this.props.maxDate); let state = ''; if (this.props.disabledByDefault) { state = 'disabled'; } else if ((minDate && !dateutils.isGTE(day, minDate)) || (maxDate && !dateutils.isLTE(day, maxDate))) { state = 'disabled'; } else if (!dateutils.sameMonth(day, this.state.currentMonth)) { state = 'disabled'; } else if (dateutils.sameDate(day, XDate())) { state = 'today'; } if (!dateutils.sameMonth(day, this.state.currentMonth) && this.props.hideExtraDays) { return (<View key={id} style={{flex: 1}}/>); } const DayComp = this.getDayComponent(); const date = day.getDate(); return ( <View style={{flex: 1, alignItems: 'center'}} key={id}> <DayComp state={state} theme={this.props.theme} onPress={this.pressDay} onLongPress={this.longPressDay} date={xdateToData(day)} marking={this.getDateMarking(day)} horizontal={this.props.horizontal} current={this.props.current} showPastDatesInHorizontal={this.props.showPastDatesInHorizontal} > {date} </DayComp> </View> ); } onHorizontalCalendarScroll({ nativeEvent }) { const { layoutMeasurement, contentOffset, contentSize } = nativeEvent; const endReachedThreshold = this.props.horizontalEndReachedThreshold; const startReachedThreshold = this.props.horizontalStartReachedThreshold; const contentWidth = contentSize.width; const travelledWidth = layoutMeasurement.width + contentOffset.x; const horizontalScrollView = this.horizontalScrollViewRef.current; let calendarUpdated = false; // going left if (contentOffset.x === startReachedThreshold && !onPressArrowLeftTriggered) { if (this.props.showPastDatesInHorizontal !== undefined) { // don't auto select previous month when past dates are hidden const selectedMonthTime = this.state.currentMonth.getTime(); const oneDayTime = (24 * 3600 * 1000); const nextValidTime = new Date().setHours(0,0,0,0) + oneDayTime + timezoneOffset; if (selectedMonthTime > nextValidTime) { calendarUpdated = true; } } else { calendarUpdated = true; } if (calendarUpdated) { this.props.onPressArrowLeft(this.state.currentMonth, this.addMonth); onPressArrowLeftTriggered = true; onPressArrowRightTriggered = true; setTimeout(() => { onPressArrowLeftTriggered = false; onPressArrowRightTriggered = false; }, 500); } } // going right if (endReachedThreshold && (travelledWidth + endReachedThreshold) > contentWidth && !onPressArrowRightTriggered) { this.props.onPressArrowRight(this.state.currentMonth, this.addMonth); calendarUpdated = true; onPressArrowRightTriggered = true; setTimeout(() => { onPressArrowRightTriggered = false; }, 500); } if (calendarUpdated && horizontalScrollView) { horizontalScrollView.scrollTo({ x: 50, animated: false }); } } getDayComponent() { if (this.props.dayComponent) { return this.props.dayComponent; } switch (this.props.markingType) { case 'period': return UnitDay; case 'multi-dot': return MultiDotDay; case 'multi-period': return MultiPeriodDay; case 'custom': return SingleDay; default: return Day; } } getDateMarking(day) { if (!this.props.markedDates) { return false; } const dates = this.props.markedDates[day.toString('yyyy-MM-dd')] || EmptyArray; if (dates.length || dates) { return dates; } else { return false; } } renderWeekNumber (weekNumber) { return <Day key={`week-${weekNumber}`} theme={this.props.theme} marking={{disableTouchEvent: true}} state='disabled'>{weekNumber}</Day>; } renderWeek(days, id) { const week = []; let validTime = new Date().setHours(0,0,0,0); // ignoring hours, mins, secs, msecs if (this.props.showPastDatesInHorizontal !== undefined) { validTime -= this.props.showPastDatesInHorizontal * (24 * 60 * 60 * 1000); } days.forEach((day, id2) => { const dayTime = day.getTime(); // don't show past days in horizontal calendar if (this.state.horizontal && this.props.showPastDatesInHorizontal !== undefined) { if (dayTime >= validTime) { week.push(this.renderDay(day, id2)); } } else { week.push(this.renderDay(day, id2)); } // if day is selected (aka current) day then corresponding week row id will be offset if (dayTime === parseDate(this.props.current).getTime()) { horizontalScrollViewOffset = id; } }, this); if (this.props.showWeekNumbers) { week.unshift(this.renderWeekNumber(days[days.length - 1].getWeek())); } return (<View style={this.style.week} key={id}>{week}</View>); } getCalendarHeaderComponent() { if (this.props.calendarHeaderComponent) { return this.props.calendarHeaderComponent; } return CalendarHeader; } showLoader() { if (this.props.LoaderComponent) { return this.props.LoaderComponent; } else { return ( <View style={{ flex: 1, justifyContent: 'center' }}> <ActivityIndicator /> </View> ); } } showCalendar(weeks) { const windowWidth = Dimensions.get('window').width; if (this.state.horizontal) { return ( <ScrollView contentContainerStyle={Platform.OS === 'web' ? { flex: 1 } : {}} style={[this.style.monthView, { marginBottom: 10 }]} horizontal scrollEventThrottle={500} onScroll={this.onHorizontalCalendarScroll} ref={this.horizontalScrollViewRef} > {weeks} { this.props.loading ? <View style={[ this.style.loaderContainer, { width: windowWidth } ]}> { this.showLoader() } </View> : null } </ScrollView> ); } else { return ( <View style={this.style.monthView}> {weeks} { this.props.loading ? <View style={this.style.loaderContainer}> { this.showLoader() } </View> : null } </View> ); } } render() { const days = dateutils.page(this.state.currentMonth, this.props.firstDay); const weeks = []; while (days.length) { weeks.push(this.renderWeek(days.splice(0, 7), weeks.length)); } let indicator; const current = parseDate(this.props.current); if (current) { const lastMonthOfDay = current.clone().addMonths(1, true).setDate(1).addDays(-1).toString('yyyy-MM-dd'); if (this.props.displayLoadingIndicator && !(this.props.markedDates && this.props.markedDates[lastMonthOfDay])) { indicator = true; } } const CalendarHeader = this.getCalendarHeaderComponent(); return ( <View style={[this.style.container, this.props.style]}> <CalendarHeader theme={this.props.theme} hideArrows={this.props.hideArrows} month={this.state.currentMonth} addMonth={this.addMonth} showIndicator={indicator} firstDay={this.props.firstDay} renderArrow={this.props.renderArrow} monthFormat={this.props.monthFormat} hideDayNames={this.props.hideDayNames} weekNumbers={this.props.showWeekNumbers} onPressArrowLeft={this.props.onPressArrowLeft} onPressArrowRight={this.props.onPressArrowRight} headerData={this.props.headerData} horizontal={this.props.horizontal} onPressListView={this.props.onPressListView} onPressGridView={this.props.onPressGridView} /> { this.showCalendar(weeks) } </View>); } } export default Calendar;