UNPKG

react-native-slideable-calendar-strip

Version:
407 lines (384 loc) 12.1 kB
import React, { Component, PureComponent } from 'react'; import PropTypes from 'prop-types'; import { View, Text, FlatList, Platform, Dimensions, StyleSheet, PanResponder, TouchableOpacity, } from 'react-native'; import Weeks from './Weeks'; import { // getDay, format, addDays, subDays, // isToday, eachDay, isFuture, isSameDay, endOfWeek, getISOWeek, startOfWeek, differenceInDays, } from 'date-fns'; import ChineseLunar from 'chinese-lunar'; import ChineseLocale from 'date-fns/locale/zh_cn'; const width = Dimensions.get('window').width; const ITEM_LENGTH = width / 7; const _today = new Date(); const _year = _today.getFullYear(); const _month = _today.getMonth(); const _day = _today.getDate(); const TODAY = new Date(_year, _month, _day); // FORMAT: Wed May 16 2018 00:00:00 GMT+0800 (CST) class DateItem extends PureComponent { render() { const { item, marked, highlight, showLunar, onItemPress, } = this.props; const solar = format(item, 'D'); const _lunar = ChineseLunar.solarToLunar(item); const lunar = ChineseLunar.format(_lunar, 'd'); const highlightBgColor = '#6E99F1'; const normalBgColor = 'white'; const hightlightTextColor = '#fff'; const normalTextColor = 'rgba(0,0,0,0.9)'; return ( <View style={styles.itemContainer}> <TouchableOpacity underlayColor='#008b8b' style={styles.itemWrapButton} onPress={onItemPress} > <View style={[ styles.itemView, { paddingTop: showLunar ? 0 : 10 }, {backgroundColor: highlight ? highlightBgColor : normalBgColor} ]}> <Text style={[ styles.itemDateText, {color: highlight ? hightlightTextColor : normalTextColor} ]}>{solar}</Text> {showLunar && <Text style={[ styles.itemLunarText, {color: highlight ? hightlightTextColor : 'gray'} ]}>{lunar}</Text> } {marked && <View style={[ styles.itemBottomDot, {backgroundColor: highlight ? 'white' : '#6D88E6'} ]} /> } </View> </TouchableOpacity> </View> ); } } class CalendarStrip extends Component { constructor(props) { super(props); this.state = { datas: this.getInitialDates(props.weekStartsOn), isTodayVisible: true, pageOfToday: 2, // page of today in calendar, start from 0 currentPage: 2, // current page in calendar, start from 0 }; } componentWillMount() { const touchThreshold = 50; const speedThreshold = 0.2; this._panResponder = PanResponder.create({ onStartShouldSetPanResponder: () => false, onMoveShouldSetPanResponder: (evt, gestureState) => { const { dy, vy } = gestureState; if (dy > touchThreshold && vy > speedThreshold) { const { onSwipeDown } = this.props; onSwipeDown && onSwipeDown(); } return false; }, onPanResponderRelease: () => {}, }); } // componentDidMount() { // setTimeout(() => { // this.scrollToPage(2); // }, 100); // } componentWillReceiveProps(nextProps) { if (isSameDay(nextProps.selectedDate, this.props.selectedDate)) return; const nextSelectedDate = nextProps.selectedDate; if (!this.currentPageDatesIncludes(nextSelectedDate)) { const sameDay = (d) => isSameDay(d, nextSelectedDate); if (this.state.datas.find(sameDay)) { let selectedIndex = this.state.datas.findIndex(sameDay); if (selectedIndex === -1) selectedIndex = this.state.pageOfToday; // in case not find const selectedPage = ~~(selectedIndex / 7); this.scrollToPage(selectedPage); } else { // not born, then spawn these dates, then scroll to it. // past: born [startOfThatWeek, last] // future: born [first, endOfThatWeek] // momentumEnd() handle pageOfToday and currentPage if (isFuture(nextSelectedDate)) { const head = this.state.datas[0]; const tail = endOfWeek(nextSelectedDate, { weekStartsOn: nextProps.weekStartsOn }); const days = eachDay(head, tail); this.setState({ datas: days, isTodayVisible: false, }, () => { const page = ~~(days.length/7 - 1); // to last page this.scrollToPage(page); }); } else { const head = startOfWeek(nextSelectedDate, { weekStartsOn: nextProps.weekStartsOn }); const tail = this.state.datas[this.state.datas.length - 1]; const days = eachDay(head, tail); this.setState({ datas: days, isTodayVisible: false, }, () => { // to first page this.scrollToPage(0); }); } } } } scrollToPage = (page, animated=true) => { this._calendar.scrollToIndex({ animated, index: 7 * page }); } currentPageDatesIncludes = (date) => { const { currentPage } = this.state; const currentPageDates = this.state.datas.slice(7*currentPage, 7*(currentPage+1)); // dont use currentPageDates.includes(date); because can't compare Date in it return !!currentPageDates.find(d => isSameDay(d, date)); } getInitialDates(weekStartsOn=0) { // const todayInWeek = getDay(TODAY); const last2WeekOfToday = subDays(TODAY, 7 * 2); const next2WeekOfToday = addDays(TODAY, 7 * 2); const startLast2Week = startOfWeek(last2WeekOfToday, { weekStartsOn }); const endNext2Week = endOfWeek(next2WeekOfToday, { weekStartsOn }); const eachDays = eachDay(startLast2Week, endNext2Week); return eachDays; } loadNextTwoWeek(originalDates) { const originalFirstDate = originalDates[0]; const originalLastDate = originalDates[originalDates.length-1]; const lastDayOfNext2Week = addDays(originalLastDate, 7 * 2); const eachDays = eachDay(originalFirstDate, lastDayOfNext2Week); this.setState({ datas: eachDays }); } loadPreviousTwoWeek(originalDates) { const originalFirstDate = originalDates[0]; const originalLastDate = originalDates[originalDates.length-1]; const firstDayOfPrevious2Week = subDays(originalFirstDate, 7 * 2); const eachDays = eachDay(firstDayOfPrevious2Week, originalLastDate); this.setState(prevState => ({ datas: eachDays, currentPage: prevState.currentPage+2, pageOfToday: prevState.pageOfToday+2, }), () => { this.scrollToPage(2, false); }); } _renderHeader = () => { const { selectedDate, onPressGoToday, isChinese, showWeekNumber } = this.props; const dateFormatted_zh = format(selectedDate, 'YYYY/MM/DD [周]dd', {locale: ChineseLocale}); const dateFormatted_en = format(selectedDate, 'YYYY/MM/DD ddd'); const dateFormatted = isChinese ? dateFormatted_zh : dateFormatted_en; const weekNumber = getISOWeek(selectedDate); return ( <View style={styles.header}> <Text style={styles.headerDate}>{dateFormatted}</Text> {showWeekNumber && <Text style={styles.headerDateWeek}>{` W${weekNumber}`}</Text> } {!this.state.isTodayVisible && <TouchableOpacity style={styles.headerGoTodayButton} onPress={() => { const page = this.state.pageOfToday; onPressGoToday && onPressGoToday(TODAY); this.scrollToPage(page); }}> <Text style={styles.todayText}>今</Text> </TouchableOpacity> } </View> ); } _stringToDate = (dateString) => { // '2018-01-01' => Date const dateArr = dateString.split('-'); const [y, m, d] = dateArr.map(ds => parseInt(ds, 10)); // CAVEAT: Jan is 0 return new Date(y, m-1, d); } render() { const { isChinese, markedDate, onPressDate, selectedDate, weekStartsOn, showChineseLunar, } = this.props; const marked = markedDate.map(ds => this._stringToDate(ds)); return ( <View style={styles.container} {...this._panResponder.panHandlers}> {this._renderHeader()} <Weeks isChinese={isChinese} weekStartsOn={weekStartsOn} /> <FlatList ref={ref => this._calendar = ref} bounces={false} horizontal pagingEnabled initialScrollIndex={14} showsHorizontalScrollIndicator={false} onMomentumScrollEnd={this.momentumEnd} scrollEventThrottle={500} getItemLayout={(data, index) => ( {length: ITEM_LENGTH, offset: ITEM_LENGTH * index, index} )} onEndReached={() => { this.onEndReached(); } } onEndReachedThreshold={0.01} data={this.state.datas} extraData={this.state} keyExtractor={(item, index) => index.toString()} renderItem={({item}) => <DateItem item={item} showLunar={showChineseLunar} onItemPress={() => onPressDate && onPressDate(item)} highlight={isSameDay(selectedDate, item)} marked={marked.find(d => isSameDay(d, item))} />} /> </View> ); } momentumEnd = (event) => { /** { contentInset: { bottom: 0, top: 0, left: 0, right: 0 }, zoomScale: 1, contentOffset: { y: 0, x: 1875 }, layoutMeasurement: { height: 50, width: 375 }, contentSize: { height: 50, width: 2625 } } */ const firstDayInCalendar = this.state.datas ? this.state.datas[0] : new Date(); const daysBeforeToday = differenceInDays(firstDayInCalendar, new Date()); const pageOfToday = ~~(Math.abs(daysBeforeToday / 7)); const screenWidth = event.nativeEvent.layoutMeasurement.width; const currentPage = event.nativeEvent.contentOffset.x / screenWidth; this.setState({ pageOfToday, currentPage, isTodayVisible: currentPage === pageOfToday, }); // swipe to head ~ load 2 weeks if (event.nativeEvent.contentOffset.x < width) { this.loadPreviousTwoWeek(this.state.datas); } } onEndReached() { this.loadNextTwoWeek(this.state.datas); } } CalendarStrip.propTypes = { selectedDate: PropTypes.object.isRequired, onPressDate: PropTypes.func, onPressGoToday: PropTypes.func, markedDate: PropTypes.array, onSwipeDown: PropTypes.func, isChinese: PropTypes.bool, showWeekNumber: PropTypes.bool, showChineseLunar: PropTypes.bool, weekStartsOn: PropTypes.number, }; CalendarStrip.defaultProps = { isChinese: false, showWeekNumber: false, showChineseLunar: false, weekStartsOn: 0, }; const styles = StyleSheet.create({ container: { width, height: 30+30+50, }, header: { height: 30, flexDirection: 'row', justifyContent: 'center', alignItems: 'center', backgroundColor: 'white', }, headerDate: { color: 'gray', fontSize: 13, }, headerDateWeek: { color: '#3D6DCF', fontSize: 14, }, headerGoTodayButton: { borderRadius: 10, width: 20, height: 20, backgroundColor: '#3D6DCF', position: 'absolute', top: 5, right: 50, justifyContent: 'center', alignItems: 'center', }, todayText: { fontSize: 12, color: 'white', }, itemContainer: { width: width / 7, height: 50, borderBottomWidth: 1, borderBottomColor: 'rgba(100,100,100,0.1)', }, itemWrapButton: { flex: 1, justifyContent: 'center', alignItems: 'center', }, itemView: { justifyContent: 'flex-start', alignItems: 'center', paddingTop: 4, width: 44, height: 44, borderRadius: 22, }, itemDateText: { fontSize: 15, lineHeight: Platform.OS === 'ios' ? 19 : 15, }, itemLunarText: { fontSize: 10, }, itemBottomDot: { width: 4, left: 20, height: 4, bottom: 4, borderRadius: 2, position: 'absolute', } }); export default CalendarStrip;