react-native-calendars-monthly-view
Version:
React Native Calendar Components
319 lines (278 loc) • 10.9 kB
JavaScript
import _ from 'lodash';
import PropTypes from 'prop-types';
import XDate from 'xdate';
import React, {Component} from 'react';
import {FlatList, Platform, Dimensions, View} from 'react-native';
import {extractComponentProps} from '../component-updater';
import {xdateToData, parseDate} from '../interface';
import dateutils from '../dateutils';
import {STATIC_HEADER} from '../testIDs';
import styleConstructor from './style';
import Calendar from '../calendar';
import CalendarListItem from './item';
import CalendarHeader from '../calendar/header/index';
const {width} = Dimensions.get('window');
/**
* @description: Calendar List component for both vertical and horizontal calendars
* @extends: Calendar
* @extendslink: docs/Calendar
* @example: https://github.com/wix/react-native-calendars/blob/master/example/src/screens/calendarsList.js
* @gif: https://github.com/wix/react-native-calendars/blob/master/demo/calendar-list.gif
*/
class CalendarList extends Component {
static displayName = 'CalendarList';
static propTypes = {
...Calendar.propTypes,
/** Max amount of months allowed to scroll to the past. Default = 50 */
pastScrollRange: PropTypes.number,
/** Max amount of months allowed to scroll to the future. Default = 50 */
futureScrollRange: PropTypes.number,
/** Used when calendar scroll is horizontal, default is device width, pagination should be disabled */
calendarWidth: PropTypes.number,
/** Dynamic calendar height */
calendarHeight: PropTypes.number,
/** Style for the List item (the calendar) */
calendarStyle: PropTypes.oneOfType([PropTypes.object, PropTypes.number, PropTypes.array]),
/** Whether to use static header that will not scroll with the list (horizontal only) */
staticHeader: PropTypes.bool,
/** Enable or disable vertical / horizontal scroll indicator. Default = false */
showScrollIndicator: PropTypes.bool,
/** Enable or disable scrolling of calendar list */
scrollEnabled: PropTypes.bool,
/** When true, the calendar list scrolls to top when the status bar is tapped. Default = true */
scrollsToTop: PropTypes.bool,
/** Enable or disable paging on scroll */
pagingEnabled: PropTypes.bool,
/** Whether the scroll is horizontal */
horizontal: PropTypes.bool,
/** Should Keyboard persist taps */
keyboardShouldPersistTaps: PropTypes.oneOf(['never', 'always', 'handled']),
/** A custom key extractor for the generated calendar months */
keyExtractor: PropTypes.func,
/** How far from the end to trigger the onEndReached callback */
onEndReachedThreshold: PropTypes.number,
/** Called once when the scroll position gets within onEndReachedThreshold */
onEndReached: PropTypes.func
};
static defaultProps = {
calendarWidth: width,
calendarHeight: 360,
pastScrollRange: 50,
futureScrollRange: 50,
showScrollIndicator: false,
horizontal: false,
scrollsToTop: false,
scrollEnabled: true,
removeClippedSubviews: Platform.OS === 'android',
keyExtractor: (item, index) => String(index)
};
constructor(props) {
super(props);
this.style = styleConstructor(props.theme);
this.viewabilityConfig = {
itemVisiblePercentThreshold: 20
};
const rows = [];
const texts = [];
const date = parseDate(props.current) || XDate();
for (let i = 0; i <= props.pastScrollRange + props.futureScrollRange; i++) {
const rangeDate = date.clone().addMonths(i - props.pastScrollRange, true);
const rangeDateStr = rangeDate.toString('MMM yyyy');
texts.push(rangeDateStr);
/*
* This selects range around current shown month [-0, +2] or [-1, +1] month for detail calendar rendering.
* If `this.pastScrollRange` is `undefined` it's equal to `false` or 0 in next condition.
*/
if (
(props.pastScrollRange - 1 <= i && i <= props.pastScrollRange + 1) ||
(!props.pastScrollRange && i <= props.pastScrollRange + 2)
) {
rows.push(rangeDate);
} else {
rows.push(rangeDateStr);
}
}
this.state = {
rows,
texts,
openDate: date,
currentMonth: parseDate(props.current)
};
}
componentDidUpdate(prevProps) {
const prevCurrent = parseDate(prevProps.current);
const current = parseDate(this.props.current);
if (current && prevCurrent && current.getTime() !== prevCurrent.getTime()) {
this.scrollToMonth(current);
}
}
static getDerivedStateFromProps(nextProps, prevState) {
const rowclone = prevState.rows;
const newrows = [];
for (let i = 0; i < rowclone.length; i++) {
let val = prevState.texts[i];
if (rowclone[i].getTime) {
val = rowclone[i].clone();
val.propbump = rowclone[i].propbump ? rowclone[i].propbump + 1 : 1;
}
newrows.push(val);
}
return {rows: newrows};
}
scrollToDay(d, offset, animated) {
const {horizontal, calendarHeight, calendarWidth, pastScrollRange, firstDay} = this.props;
const day = parseDate(d);
const diffMonths = Math.round(this.state.openDate.clone().setDate(1).diffMonths(day.clone().setDate(1)));
const size = horizontal ? calendarWidth : calendarHeight;
let scrollAmount = size * pastScrollRange + diffMonths * size + (offset || 0);
if (!horizontal) {
let week = 0;
const days = dateutils.page(day, firstDay);
for (let i = 0; i < days.length; i++) {
week = Math.floor(i / 7);
if (dateutils.sameDate(days[i], day)) {
scrollAmount += 46 * week;
break;
}
}
}
this.listView.scrollToOffset({offset: scrollAmount, animated});
}
scrollToMonth = m => {
const {horizontal, calendarHeight, calendarWidth, pastScrollRange} = this.props;
const month = parseDate(m);
const scrollTo = month || this.state.openDate;
let diffMonths = Math.round(this.state.openDate.clone().setDate(1).diffMonths(scrollTo.clone().setDate(1)));
const size = horizontal ? calendarWidth : calendarHeight;
const scrollAmount = size * pastScrollRange + diffMonths * size;
this.listView.scrollToOffset({offset: scrollAmount, animated: false});
};
getItemLayout = (data, index) => {
const {horizontal, calendarHeight, calendarWidth} = this.props;
return {
length: horizontal ? calendarWidth : calendarHeight,
offset: (horizontal ? calendarWidth : calendarHeight) * index,
index
};
};
getMonthIndex(month) {
let diffMonths = this.state.openDate.diffMonths(month) + this.props.pastScrollRange;
return diffMonths;
}
addMonth = count => {
this.updateMonth(this.state.currentMonth.clone().addMonths(count, true));
};
updateMonth(day, doNotTriggerListeners) {
if (day.toString('yyyy MM') === this.state.currentMonth.toString('yyyy MM')) {
return;
}
this.setState(
{
currentMonth: day.clone()
},
() => {
this.scrollToMonth(this.state.currentMonth);
if (!doNotTriggerListeners) {
const currMont = this.state.currentMonth.clone();
_.invoke(this.props, 'onMonthChange', xdateToData(currMont));
_.invoke(this.props, 'onVisibleMonthsChange', [xdateToData(currMont)]);
}
}
);
}
onViewableItemsChanged = ({viewableItems}) => {
function rowIsCloseToViewable(index, distance) {
for (let i = 0; i < viewableItems.length; i++) {
if (Math.abs(index - parseInt(viewableItems[i].index)) <= distance) {
return true;
}
}
return false;
}
const rowclone = this.state.rows;
const newrows = [];
const visibleMonths = [];
for (let i = 0; i < rowclone.length; i++) {
let val = rowclone[i];
const rowShouldBeRendered = rowIsCloseToViewable(i, 1);
if (rowShouldBeRendered && !rowclone[i].getTime) {
val = this.state.openDate.clone().addMonths(i - this.props.pastScrollRange, true);
} else if (!rowShouldBeRendered) {
val = this.state.texts[i];
}
newrows.push(val);
if (rowIsCloseToViewable(i, 0)) {
visibleMonths.push(xdateToData(val));
}
}
_.invoke(this.props, 'onVisibleMonthsChange', visibleMonths);
this.setState({
rows: newrows,
currentMonth: parseDate(visibleMonths[0])
});
};
renderItem = ({item}) => {
const {calendarStyle, horizontal, calendarWidth, testID, ...others} = this.props;
return (
<CalendarListItem
{...others}
item={item}
testID={`${testID}_${item}`}
style={calendarStyle}
calendarWidth={horizontal ? calendarWidth : undefined}
scrollToMonth={this.scrollToMonth}
/>
);
};
renderStaticHeader() {
const {staticHeader, horizontal, headerStyle} = this.props;
const useStaticHeader = staticHeader && horizontal;
const headerProps = extractComponentProps(CalendarHeader, this.props);
if (useStaticHeader) {
return (
<CalendarHeader
{...headerProps}
testID={STATIC_HEADER}
style={[this.style.staticHeader, headerStyle]}
month={this.state.currentMonth}
addMonth={this.addMonth}
accessibilityElementsHidden={true} // iOS
importantForAccessibility={'no-hide-descendants'} // Android
/>
);
}
}
render() {
const {style, pastScrollRange, futureScrollRange, horizontal, showScrollIndicator, testID} = this.props;
return (
<View>
<FlatList
ref={c => (this.listView = c)}
style={[this.style.container, style]}
initialListSize={pastScrollRange + futureScrollRange + 1} // ListView deprecated
data={this.state.rows}
renderItem={this.renderItem}
getItemLayout={this.getItemLayout}
onViewableItemsChanged={this.onViewableItemsChanged}
viewabilityConfig={this.viewabilityConfig}
initialScrollIndex={this.state.openDate ? this.getMonthIndex(this.state.openDate) : false}
showsVerticalScrollIndicator={showScrollIndicator}
showsHorizontalScrollIndicator={horizontal && showScrollIndicator}
testID={testID}
onLayout={this.props.onLayout}
removeClippedSubviews={this.props.removeClippedSubviews}
pagingEnabled={this.props.pagingEnabled}
scrollEnabled={this.props.scrollEnabled}
scrollsToTop={this.props.scrollsToTop}
horizontal={this.props.horizontal}
keyboardShouldPersistTaps={this.props.keyboardShouldPersistTaps}
keyExtractor={this.props.keyExtractor}
onEndReachedThreshold={this.props.onEndReachedThreshold}
onEndReached={this.props.onEndReached}
/>
{this.renderStaticHeader()}
</View>
);
}
}
export default CalendarList;