UNPKG

react-native-calendars

Version:
193 lines (192 loc) • 8.49 kB
import PropTypes from 'prop-types'; import get from 'lodash/get'; import map from 'lodash/map'; import isFunction from 'lodash/isFunction'; import isUndefined from 'lodash/isUndefined'; import debounce from 'lodash/debounce'; import XDate from 'xdate'; import React, { forwardRef, useCallback, useContext, useEffect, useMemo, useRef } from 'react'; import { SectionList } from 'react-native'; import { useDidUpdate, useCombinedRefs } from '../../hooks'; import { getMoment } from '../../momentResolver'; import { isToday, isGTE, sameDate } from '../../dateutils'; import { parseDate } from '../../interface'; import { getDefaultLocale } from '../../services'; import { UpdateSources, todayString } from '../commons'; import constants from '../../commons/constants'; import styleConstructor from '../style'; import Context from '../Context'; import InfiniteAgendaList from './infiniteAgendaList'; import { AgendaSectionHeader } from './commons'; const viewabilityConfig = { itemVisiblePercentThreshold: 20 // 50 means if 50% of the item is visible }; /** * @description: AgendaList component * @note: Should be wrapped with 'CalendarProvider' * @extends: SectionList * @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/expandableCalendar.js */ const AgendaList = forwardRef((props, ref) => { const { theme, sections, scrollToNextEvent, viewOffset = 0, avoidDateUpdates, onScroll, onMomentumScrollBegin, onMomentumScrollEnd, onScrollToIndexFailed, renderSectionHeader, sectionStyle, keyExtractor, dayFormatter, dayFormat = 'dddd, MMM d', useMoment, markToday = true, onViewableItemsChanged } = props; const { date, updateSource, setDate, setDisabled } = useContext(Context); const style = useRef(styleConstructor(theme)); const list = useCombinedRefs(ref); const _topSection = useRef(sections[0]?.title); const didScroll = useRef(false); const sectionScroll = useRef(false); const sectionHeight = useRef(0); useEffect(() => { if (date !== _topSection.current) { setTimeout(() => { scrollToSection(date); }, 500); } }, []); useDidUpdate(() => { // NOTE: on first init data should set first section to the current date!!! if (updateSource !== UpdateSources.LIST_DRAG && updateSource !== UpdateSources.CALENDAR_INIT) { scrollToSection(date); } }, [date]); const getSectionIndex = (date) => { let i; map(sections, (section, index) => { // NOTE: sections titles should match current date format!!! if (section.title === date) { i = index; } }); return i; }; const getNextSectionIndex = (date) => { let i = 0; for (let j = 1; j < sections.length; j++) { const prev = parseDate(sections[j - 1]?.title); const next = parseDate(sections[j]?.title); const cur = new XDate(date); if (isGTE(cur, prev) && isGTE(next, cur)) { i = sameDate(prev, cur) ? j - 1 : j; break; } else if (isGTE(cur, next)) { i = j; } } return i; }; const getSectionTitle = useCallback((title) => { if (!title) return; let sectionTitle = title; if (dayFormatter) { sectionTitle = dayFormatter(title); } else if (dayFormat) { if (useMoment) { const moment = getMoment(); sectionTitle = moment(title).format(dayFormat); } else { sectionTitle = new XDate(title).toString(dayFormat); } } if (markToday) { const string = getDefaultLocale().today || todayString; const today = isToday(title); sectionTitle = today ? `${string}, ${sectionTitle}` : sectionTitle; } return sectionTitle; }, []); const scrollToSection = useCallback(debounce((d) => { const sectionIndex = scrollToNextEvent ? getNextSectionIndex(d) : getSectionIndex(d); if (isUndefined(sectionIndex)) { return; } if (list?.current && sectionIndex !== undefined) { sectionScroll.current = true; // to avoid setDate() in onViewableItemsChanged _topSection.current = sections[sectionIndex]?.title; // @ts-expect-error should be fixed when we fix the typings of the ref. list?.current.scrollToLocation({ animated: true, sectionIndex: sectionIndex, itemIndex: 1, viewPosition: 0, viewOffset: (constants.isAndroid ? sectionHeight.current : 0) + viewOffset }); } }, 1000, { leading: true, trailing: true }), [viewOffset, sections]); const _onViewableItemsChanged = useCallback((info) => { if (info?.viewableItems && !sectionScroll.current) { const topSection = get(info?.viewableItems[0], 'section.title'); if (topSection && topSection !== _topSection.current) { _topSection.current = topSection; if (didScroll.current && !avoidDateUpdates) { // to avoid setDate() on first load (while setting the initial context.date value) setDate?.(_topSection.current, UpdateSources.LIST_DRAG); } } } onViewableItemsChanged?.(info); }, [avoidDateUpdates, setDate, onViewableItemsChanged]); const _onScroll = useCallback((event) => { if (!didScroll.current) { didScroll.current = true; scrollToSection.cancel(); } onScroll?.(event); }, [onScroll]); const _onMomentumScrollBegin = useCallback((event) => { setDisabled?.(true); onMomentumScrollBegin?.(event); }, [onMomentumScrollBegin, setDisabled]); const _onMomentumScrollEnd = useCallback((event) => { // when list momentum ends AND when scrollToSection scroll ends sectionScroll.current = false; setDisabled?.(false); onMomentumScrollEnd?.(event); }, [onMomentumScrollEnd, setDisabled]); const headerTextStyle = useMemo(() => [style.current.sectionText, sectionStyle], [sectionStyle]); const _onScrollToIndexFailed = useCallback((info) => { if (onScrollToIndexFailed) { onScrollToIndexFailed(info); } else { console.log('onScrollToIndexFailed info: ', info); } }, [onScrollToIndexFailed]); const onHeaderLayout = useCallback((event) => { sectionHeight.current = event.nativeEvent.layout.height; }, []); const _renderSectionHeader = useCallback((info) => { const title = info?.section?.title; if (renderSectionHeader) { return renderSectionHeader(title); } const headerTitle = getSectionTitle(title); return <AgendaSectionHeader title={headerTitle} style={headerTextStyle} onLayout={onHeaderLayout}/>; }, [headerTextStyle]); const _keyExtractor = useCallback((item, index) => { return isFunction(keyExtractor) ? keyExtractor(item, index) : String(index); }, [keyExtractor]); if (props.infiniteListProps) { return <InfiniteAgendaList {...props}/>; } return (<SectionList stickySectionHeadersEnabled {...props} // @ts-expect-error ref={list} keyExtractor={_keyExtractor} showsVerticalScrollIndicator={false} onViewableItemsChanged={_onViewableItemsChanged} viewabilityConfig={viewabilityConfig} renderSectionHeader={_renderSectionHeader} onScroll={_onScroll} onMomentumScrollBegin={_onMomentumScrollBegin} onMomentumScrollEnd={_onMomentumScrollEnd} onScrollToIndexFailed={_onScrollToIndexFailed}/>); // _getItemLayout = (data, index) => { // return {length: constants.screenWidth, offset: constants.screenWidth * index, index}; // } }); export default AgendaList; AgendaList.displayName = 'AgendaList'; AgendaList.propTypes = { dayFormat: PropTypes.string, dayFormatter: PropTypes.func, useMoment: PropTypes.bool, markToday: PropTypes.bool, // @ts-expect-error TODO Figure out why forwardRef causes error about the number type sectionStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]), avoidDateUpdates: PropTypes.bool };