react-native-calendars
Version:
React Native Calendar Components
204 lines (203 loc) • 9.56 kB
JavaScript
import XDate from 'xdate';
import React, { useCallback, useContext, useMemo, useRef, useState } from 'react';
import { FlatList, View } from 'react-native';
import { sameWeek, onSameDateRange, getWeekDates } from '../../dateutils';
import { toMarkingFormat } from '../../interface';
import styleConstructor from '../style';
import WeekDaysNames from '../../commons/WeekDaysNames';
import Week from '../week';
import { UpdateSources } from '../commons';
import constants from '../../commons/constants';
import { extractCalendarProps } from '../../componentUpdater';
import CalendarContext from '../Context';
import { useDidUpdate } from '../../hooks';
export const NUMBER_OF_PAGES = 6;
const NUM_OF_ITEMS = NUMBER_OF_PAGES * 2 + 1; // NUMBER_OF_PAGES before + NUMBER_OF_PAGES after + current
/**
* @description: Week calendar component
* @note: Should be wrapped with 'CalendarProvider'
* @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/expandableCalendar.js
*/
const WeekCalendar = (props) => {
const { calendarWidth, hideDayNames, current, theme, testID, markedDates } = props;
const context = useContext(CalendarContext);
const { allowShadow = true, ...calendarListProps } = props;
const { style: propsStyle, onDayPress, firstDay = 0, ...others } = extractCalendarProps(calendarListProps);
const { date, numberOfDays, updateSource, setDate, timelineLeftInset } = context;
const visibleWeek = useRef(date);
const style = useRef(styleConstructor(theme));
const items = useRef(getDatesArray(current ?? date, firstDay, numberOfDays));
const [listData, setListData] = useState(items.current);
const changedItems = useRef(constants.isRTL);
const list = useRef(null);
const currentIndex = useRef(NUMBER_OF_PAGES);
const shouldFixRTL = useMemo(() => !constants.isRN73() && constants.isAndroidRTL, []);
useDidUpdate(() => {
items.current = getDatesArray(date, firstDay, numberOfDays);
setListData(items.current);
visibleWeek.current = date;
list?.current?.scrollToIndex({ index: NUMBER_OF_PAGES, animated: false });
}, [numberOfDays]);
useDidUpdate(() => {
if (updateSource !== UpdateSources.WEEK_SCROLL) {
const pageIndex = items.current.findIndex(item => isCustomNumberOfDays(numberOfDays) ?
onSameDateRange({
firstDay: item,
secondDay: date,
numberOfDays: numberOfDays,
firstDateInRange: item
}) :
sameWeek(item, date, firstDay));
if (pageIndex !== currentIndex.current) {
const adjustedIndexFrScroll = shouldFixRTL ? NUM_OF_ITEMS - 1 - pageIndex : pageIndex;
if (pageIndex >= 0) {
visibleWeek.current = items.current[adjustedIndexFrScroll];
currentIndex.current = adjustedIndexFrScroll;
}
else {
visibleWeek.current = date;
currentIndex.current = NUMBER_OF_PAGES;
}
pageIndex <= 0 ? onEndReached() : list?.current?.scrollToIndex({ index: adjustedIndexFrScroll, animated: false });
}
}
}, [date, updateSource, shouldFixRTL]);
const containerWidth = useMemo(() => {
return calendarWidth ?? constants.screenWidth;
}, [calendarWidth]);
const _onDayPress = useCallback((value) => {
if (onDayPress) {
onDayPress(value);
}
else {
setDate?.(value.dateString, UpdateSources.DAY_PRESS);
}
}, [onDayPress]);
const getCurrentWeekMarkings = useCallback((date, markings) => {
if (!markings) {
return;
}
const dates = getWeekDates(date, firstDay);
return dates?.reduce((acc, date) => {
const dateString = toMarkingFormat(date);
return {
...acc,
...(markings[dateString] && { [dateString]: markings[dateString] })
};
}, {});
}, []);
const weekStyle = useMemo(() => {
return [{ width: containerWidth }, propsStyle];
}, [containerWidth, propsStyle]);
const renderItem = useCallback(({ item }) => {
const currentContext = sameWeek(date, item, firstDay) ? context : undefined;
const markings = getCurrentWeekMarkings(item, markedDates);
return (<Week {...others} markedDates={markings} current={item} firstDay={firstDay} style={weekStyle} context={currentContext} onDayPress={_onDayPress} numberOfDays={numberOfDays} timelineLeftInset={timelineLeftInset}/>);
}, [firstDay, _onDayPress, context, date, markedDates]);
const keyExtractor = useCallback((item, index) => `${item}-${index}`, []);
const renderWeekDaysNames = useMemo(() => {
return (<WeekDaysNames firstDay={firstDay} style={style.current.dayHeader}/>);
}, [firstDay]);
const weekCalendarStyle = useMemo(() => {
return [
allowShadow && style.current.containerShadow,
!hideDayNames && style.current.containerWrapper
];
}, [allowShadow, hideDayNames]);
const containerStyle = useMemo(() => {
return [style.current.week, style.current.weekCalendar];
}, []);
const getItemLayout = useCallback((_, index) => {
return {
length: containerWidth,
offset: containerWidth * index,
index
};
}, [containerWidth]);
const onEndReached = useCallback(() => {
changedItems.current = true;
items.current = (getDatesArray(visibleWeek.current, firstDay, numberOfDays));
setListData(items.current);
currentIndex.current = NUMBER_OF_PAGES;
list?.current?.scrollToIndex({ index: NUMBER_OF_PAGES, animated: false });
}, [firstDay, numberOfDays]);
const onViewableItemsChanged = useCallback(({ viewableItems }) => {
if (changedItems.current || viewableItems.length === 0) {
changedItems.current = false;
return;
}
const currItems = items.current;
const newDate = viewableItems[0]?.item;
if (newDate !== visibleWeek.current) {
if (shouldFixRTL) {
//in android RTL the item we see is the one in the opposite direction
const newDateOffset = -1 * (NUMBER_OF_PAGES - currItems.indexOf(newDate));
const adjustedNewDate = currItems[NUMBER_OF_PAGES - newDateOffset];
visibleWeek.current = adjustedNewDate;
currentIndex.current = currItems.indexOf(adjustedNewDate);
setDate(adjustedNewDate, UpdateSources.WEEK_SCROLL);
if (visibleWeek.current === currItems[currItems.length - 1]) {
onEndReached();
}
}
else {
currentIndex.current = currItems.indexOf(newDate);
visibleWeek.current = newDate;
setDate(newDate, UpdateSources.WEEK_SCROLL);
if (visibleWeek.current === currItems[0]) {
onEndReached();
}
}
}
}, [onEndReached, shouldFixRTL]);
const viewabilityConfigCallbackPairs = useRef([{
viewabilityConfig: {
itemVisiblePercentThreshold: 20
},
onViewableItemsChanged
}]);
return (<View testID={testID} style={weekCalendarStyle}>
{!hideDayNames && (<View style={containerStyle}>
{renderWeekDaysNames}
</View>)}
<View style={style.current.container}>
<FlatList testID={`${testID}.list`} ref={list} style={style.current.container} data={listData} horizontal showsHorizontalScrollIndicator={false} pagingEnabled scrollEnabled renderItem={renderItem} keyExtractor={keyExtractor} initialScrollIndex={NUMBER_OF_PAGES} getItemLayout={getItemLayout} viewabilityConfigCallbackPairs={viewabilityConfigCallbackPairs.current} onEndReached={onEndReached} onEndReachedThreshold={1 / NUM_OF_ITEMS}/>
</View>
</View>);
};
function getDateForDayRange(date, weekIndex, numberOfDays) {
const d = new XDate(date);
if (weekIndex !== 0) {
d.addDays(numberOfDays * weekIndex);
}
return toMarkingFormat(d);
}
function getDate(date, firstDay, weekIndex, numberOfDays) {
const d = new XDate(date);
// get the first day of the week as date (for the on scroll mark)
let dayOfTheWeek = d.getDay();
if (dayOfTheWeek < firstDay && firstDay > 0) {
dayOfTheWeek = 7 + dayOfTheWeek;
}
if (weekIndex !== 0) {
d.addDays(firstDay - dayOfTheWeek);
}
const newDate = numberOfDays && numberOfDays > 1 ? d.addDays(weekIndex * numberOfDays) : d.addWeeks(weekIndex);
const today = new XDate();
const offsetFromNow = newDate.diffDays(today);
const isSameWeek = offsetFromNow > 0 && offsetFromNow < (numberOfDays ?? 7);
return toMarkingFormat(isSameWeek ? today : newDate);
}
function getDatesArray(date, firstDay, numberOfDays) {
return [...Array(NUM_OF_ITEMS).keys()].map((index) => {
if (isCustomNumberOfDays(numberOfDays)) {
return getDateForDayRange(date, index - NUMBER_OF_PAGES, numberOfDays);
}
return getDate(date, firstDay, index - NUMBER_OF_PAGES);
});
}
function isCustomNumberOfDays(numberOfDays) {
return numberOfDays && numberOfDays > 1;
}
WeekCalendar.displayName = 'WeekCalendar';
export default WeekCalendar;