import PropTypes from 'prop-types'; import XDate from 'xdate'; import memoize from 'memoize-one'; import React, { Component } from 'react'; import { View, ViewStyle, StyleProp, TextStyle, ImageStyle } from 'react-native'; // @ts-expect-error import GestureRecognizer, { swipeDirections } from 'react-native-swipe-gestures'; import { page, isGTE, isLTE, sameMonth } from '../dateutils'; import { xdateToData, parseDate, toMarkingFormat } from '../interface'; import { getState } from '../day-state-manager'; import { extractComponentProps } from '../componentUpdater'; // @ts-expect-error import { WEEK_NUMBER } from '../testIDs'; import { DateData, Theme } from '../types'; import styleConstructor from './style'; import CalendarHeader, { CalendarHeaderProps } from './header'; import Day, { DayProps } from './day/index'; import BasicDay from './day/basic'; import { MarkingProps } from './day/marking'; type MarkedDatesType = { [key: string]: MarkingProps; }; export interface CalendarProps extends CalendarHeaderProps, DayProps { /** Specify theme properties to override specific styles for calendar parts */ theme?: Theme; /** If firstDay=1 week starts from Monday. Note that dayNames and dayNamesShort should still start from Sunday */ firstDay?: number; /** Display loading indicator */ displayLoadingIndicator?: boolean; /** Show week numbers */ showWeekNumbers?: boolean; /** Specify style for calendar container element */ style?: StyleProp; /** Initially visible month */ current?: string; // TODO: migrate to 'initialDate' /** Initially visible month. If changed will initialize the calendar to this value */ initialDate?: string; /** Minimum date that can be selected, dates before minDate will be grayed out */ minDate?: string; /** Maximum date that can be selected, dates after maxDate will be grayed out */ maxDate?: string; /** Collection of dates that have to be marked */ markedDates?: MarkedDatesType; /** Do not show days of other months in month page */ hideExtraDays?: boolean; /** Always show six weeks on each month (only when hideExtraDays = false) */ showSixWeeks?: boolean; /** Handler which gets executed on day press */ onDayPress?: (date: DateData) => void; /** Handler which gets executed on day long press */ onDayLongPress?: (date: DateData) => void; /** Handler which gets executed when month changes in calendar */ onMonthChange?: (date: DateData) => void; /** Handler which gets executed when visible month changes in calendar */ onVisibleMonthsChange?: (months: DateData[]) => void; /** Disables changing month when click on days of other months (when hideExtraDays is false) */ disableMonthChange?: boolean; /** Enable the option to swipe between months */ enableSwipeMonths?: boolean; /** Disable days by default */ disabledByDefault?: boolean; /** Style passed to the header */ headerStyle?: ViewStyle; /** Allow rendering a totally custom header */ customHeader?: any; /** Allow selection of dates before minDate or after maxDate */ allowSelectionOutOfRange?: boolean; /**List weekday close */ weekdayClose?: any; /**List weekday close */ dayClose?: any /**Title header calendar */ titleHeaderCalendar?: any /**From date select */ fromDate?: string /**To date select */ toDate?: string /**Type swipe month */ onChangeMonth?: (type: string) => void /**stype icon */ icStyle?: ImageStyle /**stype content */ contentStyle?: ViewStyle /**text stype */ txtStyle?: TextStyle } interface State { prevInitialDate?: string; currentMonth: any; } /** * @description: Calendar component * @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/calendars.js * @gif: https://github.com/wix/react-native-calendars/blob/master/demo/assets/calendar.gif */ class Calendar extends Component { static displayName = 'Calendar'; static propTypes = { ...CalendarHeader.propTypes, ...Day.propTypes, theme: PropTypes.object, firstDay: PropTypes.number, displayLoadingIndicator: PropTypes.bool, showWeekNumbers: PropTypes.bool, style: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.number]), current: PropTypes.string, initialDate: PropTypes.string, minDate: PropTypes.string, maxDate: PropTypes.string, markedDates: PropTypes.object, hideExtraDays: PropTypes.bool, showSixWeeks: PropTypes.bool, onDayPress: PropTypes.func, onDayLongPress: PropTypes.func, onMonthChange: PropTypes.func, onVisibleMonthsChange: PropTypes.func, disableMonthChange: PropTypes.bool, enableSwipeMonths: PropTypes.bool, disabledByDefault: PropTypes.bool, headerStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), customHeader: PropTypes.any, allowSelectionOutOfRange: PropTypes.bool, weekdayClose: PropTypes.any, dayClose: PropTypes.any, titleHeaderCalendar: PropTypes.any, fromDate: PropTypes.any, toDate: PropTypes.any, onChangeMonth: PropTypes.any, icStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), contentStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), txtStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), }; static defaultProps = { enableSwipeMonths: false }; state = { prevInitialDate: this.props.initialDate, currentMonth: this.props.current || this.props.initialDate ? parseDate(this.props.current || this.props.initialDate) : new XDate() }; style = styleConstructor(this.props.theme); header: React.RefObject = React.createRef(); static getDerivedStateFromProps(nextProps: CalendarProps, prevState: State) { if (nextProps?.initialDate && nextProps?.initialDate !== prevState.prevInitialDate) { return { prevInitialDate: nextProps.initialDate, currentMonth: parseDate(nextProps.initialDate) }; } return null; } addMonth = (count: number) => { this.updateMonth(this.state.currentMonth.clone().addMonths(count, true)); }; updateMonth = (day: any) => { if (sameMonth(day, this.state.currentMonth)) { return; } this.setState({ currentMonth: day.clone() }, () => { const currMont = this.state.currentMonth.clone(); this.props.onMonthChange?.(xdateToData(currMont)); this.props.onVisibleMonthsChange?.([xdateToData(currMont)]); }); }; handleDayInteraction(date: DateData, interaction?: (date: DateData) => void) { const { disableMonthChange, allowSelectionOutOfRange } = this.props; const day = parseDate(date); const min = parseDate(this.props.minDate); const max = parseDate(this.props.maxDate); if (allowSelectionOutOfRange || !(min && !isGTE(day, min)) && !(max && !isLTE(day, max))) { const shouldUpdateMonth = disableMonthChange === undefined || !disableMonthChange; if (shouldUpdateMonth) { this.updateMonth(day); } if (interaction) { interaction(date); } } } pressDay = (date?: DateData) => { if (date) this.handleDayInteraction(date, this.props.onDayPress); }; longPressDay = (date?: DateData) => { if (date) this.handleDayInteraction(date, this.props.onDayLongPress); }; swipeProps = { onSwipe: (direction: string) => this.onSwipe(direction) }; onSwipe = (gestureName: string) => { const { SWIPE_UP, SWIPE_DOWN, SWIPE_LEFT, SWIPE_RIGHT } = swipeDirections; switch (gestureName) { case SWIPE_UP: case SWIPE_DOWN: break; case SWIPE_LEFT: this.onSwipeLeft(); break; case SWIPE_RIGHT: this.onSwipeRight(); break; } }; onSwipeLeft = () => { this.header?.current?.onPressRight(); }; onSwipeRight = () => { this.header?.current?.onPressLeft(); }; renderWeekNumber = memoize(weekNumber => { return ( {weekNumber} ); }); renderDay(day: XDate, id: number) { const { hideExtraDays, markedDates, fromDate, toDate, txtStyle } = this.props; const dayProps = extractComponentProps(Day, this.props); if (!sameMonth(day, this.state.currentMonth) && hideExtraDays) { return ; } return ( { day && } ); } renderWeek(days: XDate[], id: number) { const week = []; days.forEach((day: XDate, id2: number) => { week.push(this.renderDay(day, id2)); }, this); if (this.props.showWeekNumbers) { week.unshift(this.renderWeekNumber(days[days.length - 1].getWeek())); } return ( {week} ); } renderMonth() { const { currentMonth } = this.state; const { showSixWeeks, hideExtraDays, contentStyle } = this.props; const shouldShowSixWeeks = showSixWeeks && !hideExtraDays; const days = page(currentMonth, 0, shouldShowSixWeeks); const weeks = []; while (days.length) { weeks.push(this.renderWeek(days.splice(0, 7), weeks.length)); } return {weeks}; } renderHeader() { const { customHeader, headerStyle, displayLoadingIndicator, markedDates, testID, titleHeaderCalendar, icStyle, txtStyle, onChangeMonth } = this.props; let indicator; if (this.state.currentMonth) { const lastMonthOfDay = toMarkingFormat(this.state.currentMonth.clone().addMonths(1, true).setDate(1).addDays(-1)); if (displayLoadingIndicator && !markedDates?.[lastMonthOfDay]) { indicator = true; } } const headerProps = extractComponentProps(CalendarHeader, this.props); const CustomHeader = customHeader; const HeaderComponent = customHeader ? CustomHeader : CalendarHeader; const ref = customHeader ? undefined : this.header; return ( ); } render() { const { enableSwipeMonths, style } = this.props; const GestureComponent = enableSwipeMonths ? GestureRecognizer : View; const gestureProps = enableSwipeMonths ? this.swipeProps : undefined; return ( {this.renderHeader()} {this.renderMonth()} ); } } export default Calendar;