import React, { useCallback, useRef, useState, useMemo, useEffect, forwardRef, Ref, } from 'react'; import { FlatList, LayoutChangeEvent, Platform } from 'react-native'; import moment from 'moment'; import Month from './Month'; import useMonths from '../hooks/use-months'; import useRange from '../hooks/use-range'; import { getMonthIndex } from '../utils/date'; import { CalendarProps, ViewableItemsType } from '../types'; const NUMBER_OF_MONTHS = 12; const MONTH_HEIGHT = 370; const INITIAL_LIST_SIZE = 2; const VIEWABLE_RANGE_OFFSET = 5; const VIEWABILITY_CONFIG = { itemVisiblePercentThreshold: 10, minimumViewTime: 32, }; const Calendario = forwardRef((props: CalendarProps, ref: Ref) => { const { numberOfMonths = NUMBER_OF_MONTHS, startingMonth = moment().format('YYYY-MM-DD'), initialListSize = INITIAL_LIST_SIZE, showWeekdays = true, showMonthTitle = true, theme = {}, locale = 'en', disableRange = false, firstDayMonday = false, monthHeight = MONTH_HEIGHT, disableOffsetDays = false, viewableRangeOffset = VIEWABLE_RANGE_OFFSET, showsVerticalScrollIndicator, calculateMonthHeightDynamically, monthNames, onPress, dayNames, startDate, endDate, } = props; const [ dynamicLayoutScrollHasHappened, setDynamicLayoutScrollHasHappened, ] = useState(false); const [listRendered, setListRendered] = useState(false); const [firstViewableIndex, setFirstViewableIndex] = useState(0); const [lastViewableIndex, setLastViewableIndex] = useState( INITIAL_LIST_SIZE + viewableRangeOffset! ); const listReference = useRef(null); const { months, firstMonth: firstMonthToRender } = useMonths({ numberOfMonths, startingMonth, }); const { start: localStartDate, end: localEndDate } = useRange({ startDate, endDate, firstMonthToRender, numberOfMonths, }); const firstMonthIndex = useMemo(() => { if (localStartDate) { const monthIndex = getMonthIndex( firstMonthToRender, localStartDate, months, numberOfMonths ); if (monthIndex !== null) { return monthIndex; } } return 0; }, [firstMonthToRender, localStartDate, months, numberOfMonths]); useEffect(() => { if ( calculateMonthHeightDynamically && !dynamicLayoutScrollHasHappened && firstMonthIndex !== 0 && listRendered ) { setTimeout(() => { if (listReference.current !== null) { setDynamicLayoutScrollHasHappened(true); listReference.current.scrollToIndex({ index: firstMonthIndex, animated: true, }); } }, 0); } }, [ calculateMonthHeightDynamically, dynamicLayoutScrollHasHappened, firstMonthIndex, listRendered, ]); useEffect(() => { Calendar.layouts = []; }, [months]); const getItemLayout = useCallback( ( _data: any, index: number ): { length: number; offset: number; index: number } => { if (calculateMonthHeightDynamically) { if (index === -1 || !Calendar.layouts[index]) { return { index, length: 0, offset: 0 }; } const length = Calendar.layouts[index]; const offset = Calendar.layouts .slice(0, index) .reduce((a, c) => a + c, 0); return { length, offset, index }; } return { length: monthHeight, offset: monthHeight * index, index, }; }, [calculateMonthHeightDynamically, monthHeight] ); const keyExtractor = useCallback( (_item: any, index: number): string => String(index), [] ); const onViewableItemsChanged = useCallback( (info: ViewableItemsType) => { if (props.viewableItemsChanged) { props.viewableItemsChanged(info); } if (props.renderAllMonths) { return; } const { viewableItems } = info; if (viewableItems.length > 0) { const { 0: firstViewableItem, length: l, [l - 1]: lastViewableItem, } = viewableItems; if ( firstViewableIndex !== firstViewableItem.index || lastViewableIndex !== lastViewableItem.index ) { setFirstViewableIndex(firstViewableItem.index!); setLastViewableIndex(lastViewableItem.index! + VIEWABLE_RANGE_OFFSET); } } }, [firstViewableIndex, lastViewableIndex, props] ); const viewabilityConfigCallbackPairs = useRef([ { viewabilityConfig: VIEWABILITY_CONFIG, onViewableItemsChanged, }, ]); const handleMonthLayout = useCallback( (index, e: LayoutChangeEvent) => { if (calculateMonthHeightDynamically && !Calendar.layouts[index]) { Calendar.layouts[index] = e.nativeEvent.layout.height; if (index === firstMonthIndex) { setListRendered(true); } } }, [calculateMonthHeightDynamically, firstMonthIndex] ); const handlePress = useCallback( (date: Date) => { if (onPress) { onPress(date); } }, [onPress] ); const renderMonth = useCallback( ({ index }: { index: number }) => { const month = moment(firstMonthToRender).add(index, 'months'); return ( ); }, [ calculateMonthHeightDynamically, dayNames, disableOffsetDays, disableRange, firstDayMonday, firstMonthToRender, firstViewableIndex, handleMonthLayout, handlePress, lastViewableIndex, localEndDate, localStartDate, locale, monthHeight, monthNames, props.disabledDays, props.markedDays, props.maxDate, props.minDate, props.renderAllMonths, props.renderDayContent, showMonthTitle, showWeekdays, theme, viewableRangeOffset, ] ); const isWeb = Platform.OS === 'web'; const initialNumToRender = isWeb ? numberOfMonths : initialListSize; return ( ); }); Calendario.displayName = 'Calendar'; const Calendar = Object.assign(Calendario, { layouts: [] as number[], }); export { Calendario as Calendar };