UNPKG

react-native-ui-lib

Version:

[![SWUbanner](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner-direct.svg)](https://stand-with-ukraine.pp.ua)

186 lines (179 loc) • 6.92 kB
import React, { useCallback, useMemo, useRef, useState } from 'react'; import { useSharedValue, useAnimatedReaction, runOnJS } from 'react-native-reanimated'; import { FlashListPackage } from "../../optionalDependencies"; import { Constants } from "../../commons/new"; import { generateMonthItems } from "./helpers/CalendarProcessor"; import { addHeaders } from "./helpers/DataProcessor"; import { isSameMonth, /* addYears, */getDateObject } from "./helpers/DateUtils"; import { FirstDayOfWeek, UpdateSource } from "./types"; import CalendarContext from "./CalendarContext"; import CalendarItem from "./CalendarItem"; import Agenda from "./Agenda"; import TodayButton from "./TodayButton"; import Header from "./Header"; import { useDidUpdate } from "../../hooks"; const FlashList = FlashListPackage?.FlashList; const VIEWABILITY_CONFIG = { itemVisiblePercentThreshold: 95, minimumViewTime: 1000 }; const YEARS_RANGE = 5; const PAGE_RELOAD_THRESHOLD = 3; const NOW = new Date().setHours(0, 0, 0, 0); function Calendar(props) { const { data, children, initialDate = NOW, onChangeDate, firstDayOfWeek = FirstDayOfWeek.MONDAY, staticHeader = false, showExtraDays = true } = props; const [monthItems] = useState(() => generateMonthItems(initialDate, YEARS_RANGE, YEARS_RANGE)); const getItemIndex = useCallback(date => { 'worklet'; const dateObject = getDateObject(date); for (let i = 0; i < monthItems.length; i++) { if (monthItems[i].month === dateObject.month && monthItems[i].year === dateObject.year) { return i; } } return -1; }, [monthItems]); const flashListRef = useRef(); const current = useSharedValue(new Date(initialDate).setHours(0, 0, 0, 0)); const initialMonthIndex = useRef(getItemIndex(current.value)); const lastUpdateSource = useSharedValue(UpdateSource.INIT); const processedData = useMemo(() => addHeaders(data), [data]); const scrolledByUser = useSharedValue(false); const headerHeight = useSharedValue(0); const setDate = useCallback((date, updateSource) => { current.value = date; lastUpdateSource.value = updateSource; if (updateSource !== UpdateSource.PROP_UPDATE) { onChangeDate?.(date); } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const scrollToIndex = useCallback(index => { scrolledByUser.value = false; // @ts-expect-error flashListRef.current?.scrollToIndex({ index, animated: true }); // eslint-disable-next-line react-hooks/exhaustive-deps }, [getItemIndex]); useDidUpdate(() => { setDate(initialDate, UpdateSource.PROP_UPDATE); }, [initialDate]); useDidUpdate(() => { console.log('Update items'); const index = getItemIndex(current.value); scrollToIndex(index); }, [monthItems, getItemIndex]); const setHeaderHeight = useCallback(height => { headerHeight.value = height; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const contextValue = useMemo(() => { return { data: processedData, firstDayOfWeek, selectedDate: current, setDate, showWeeksNumbers: true, showExtraDays, updateSource: lastUpdateSource, staticHeader, setHeaderHeight, headerHeight, today: NOW }; }, [processedData, staticHeader, showExtraDays, firstDayOfWeek]); /** Pages reload */ // const mergeArrays = (prepend: boolean, array: DateObjectWithOptionalDay[], newArray: DateObjectWithOptionalDay[]) => { // const arr: DateObjectWithOptionalDay[] = array.slice(); // if (prepend) { // arr.unshift(...newArray); // } else { // arr.push(...newArray); // } // return arr; // }; const addPages = useCallback((/* index: number */ ) => { // const prepend = index < PAGE_RELOAD_THRESHOLD; // const append = index > items.length - PAGE_RELOAD_THRESHOLD; // const pastRange = prepend ? YEARS_RANGE : 0; // const futureRange = append ? YEARS_RANGE : 0; // const newDate = addYears(current.value, prepend ? -1 : 1); // const newItems = generateMonthItems(newDate, pastRange, futureRange); // const newArray = mergeArrays(prepend, items, newItems); // setMonthItems(newArray); // // eslint-disable-next-line react-hooks/exhaustive-deps }, [monthItems]); const shouldAddPages = useCallback(index => { 'worklet'; return index !== -1 && (index < PAGE_RELOAD_THRESHOLD || index > monthItems.length - PAGE_RELOAD_THRESHOLD); // eslint-disable-next-line react-hooks/exhaustive-deps }, [monthItems]); useAnimatedReaction(() => { return current.value; }, (selected, previous) => { const index = getItemIndex(selected); if (shouldAddPages(index)) { console.log('Add new pages: ', index, monthItems.length); runOnJS(addPages)(/* index */); } else if (lastUpdateSource.value !== UpdateSource.MONTH_SCROLL) { if (previous && !isSameMonth(selected, previous)) { runOnJS(scrollToIndex)(index); } } }, [getItemIndex]); /** Events */ // eslint-disable-next-line max-len const onViewableItemsChanged = useCallback(({ viewableItems }) => { const item = viewableItems?.[0]?.item; if (item && scrolledByUser.value) { if (!isSameMonth(item, current.value)) { const newDate = getDateObject({ year: item.year, month: item.month, day: 1 }).timestamp; setDate(newDate, UpdateSource.MONTH_SCROLL); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onMomentumScrollBegin = useCallback(() => { scrolledByUser.value = true; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const onScrollBeginDrag = useCallback(() => { scrolledByUser.value = true; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const renderCalendarItem = useCallback(({ item }) => { if (!staticHeader || headerHeight.value) { // item is rendered before static header height is calculated so it leaves extra space return <CalendarItem year={item.year} month={item.month} />; } }, []); return <CalendarContext.Provider value={contextValue}> {staticHeader && <Header />} <FlashList ref={flashListRef} estimatedItemSize={Constants.screenWidth} data={monthItems} initialScrollIndex={initialMonthIndex.current} estimatedFirstItemOffset={0} renderItem={renderCalendarItem} horizontal pagingEnabled showsHorizontalScrollIndicator={false} // TODO: Consider moving this shared logic with Agenda to a hook onViewableItemsChanged={onViewableItemsChanged} viewabilityConfig={VIEWABILITY_CONFIG} onMomentumScrollBegin={onMomentumScrollBegin} onScrollBeginDrag={onScrollBeginDrag} /> {children} <TodayButton /> </CalendarContext.Provider>; } Calendar.Agenda = Agenda; export default Calendar;