UNPKG

@anatolyk/react-native-week-view

Version:
550 lines (493 loc) 16.8 kB
import React, { Component } from 'react' import PropTypes from 'prop-types' import { View, ScrollView, Animated, VirtualizedList, InteractionManager, } from 'react-native' import moment from 'moment' import memoizeOne from 'memoize-one' import Event from '../Event/Event' import Events from '../Events/Events' import Header from '../Header/Header' import Title from '../Title/Title' import Times from '../Times/Times' import styles from './WeekView.styles' import { CONTAINER_HEIGHT, DATE_STR_FORMAT, availableNumberOfDays, setLocale, WIDTH, } from '../utils' const MINUTES_IN_DAY = 60 * 24 export default class WeekView extends Component { constructor(props) { super(props) this.eventsGrid = null this.verticalAgenda = null this.header = null this.pageOffset = 2 this.currentPageIndex = this.pageOffset this.eventsGridScrollX = new Animated.Value(0) const initialDates = this.calculatePagesDates( props.selectedDate, props.numberOfDays, props.weekStartsOn, props.prependMostRecent, props.fixedHorizontally, ) this.state = { // currentMoment should always be the first date of the current page currentMoment: moment(initialDates[this.currentPageIndex]).toDate(), initialDates, scrollEnabled: true, topSelectedIndex: -1, bottomSelectedIndex: -1, } setLocale(props.locale) } componentDidMount() { requestAnimationFrame(() => { this.scrollToVerticalStart() }) this.eventsGridScrollX.addListener((position) => { this.header.scrollToOffset({ offset: position.value, animated: false }) }) } componentDidUpdate(prevprops) { if (this.props.locale !== prevprops.locale) { setLocale(this.props.locale) } } componentWillUnmount() { this.eventsGridScrollX.removeAllListeners() } calculateTimes = memoizeOne((minutesStep, formatTimeLabel) => { const times = [] const startOfDay = moment().startOf('day') for (let timer = 0; timer < MINUTES_IN_DAY; timer += minutesStep) { const time = startOfDay.clone().minutes(timer) times.push(time.format(formatTimeLabel)) } return times }) scrollToVerticalStart = () => { if (this.verticalAgenda) { const { startHour, hoursInDisplay } = this.props const startHeight = (startHour * CONTAINER_HEIGHT) / hoursInDisplay this.verticalAgenda.scrollTo({ y: startHeight, x: 0, animated: false }) } } getSignToTheFuture = () => { const { prependMostRecent } = this.props const daySignToTheFuture = prependMostRecent ? -1 : 1 return daySignToTheFuture } prependPagesInPlace = (initialDates, nPages) => { const { numberOfDays } = this.props const daySignToTheFuture = this.getSignToTheFuture() const first = initialDates[0] const daySignToThePast = daySignToTheFuture * -1 const addDays = numberOfDays * daySignToThePast for (let i = 1; i <= nPages; i += 1) { const initialDate = moment(first).add(addDays * i, 'd') initialDates.unshift(initialDate.format(DATE_STR_FORMAT)) } } appendPagesInPlace = (initialDates, nPages) => { const { numberOfDays } = this.props const daySignToTheFuture = this.getSignToTheFuture() const latest = initialDates[initialDates.length - 1] const addDays = numberOfDays * daySignToTheFuture for (let i = 1; i <= nPages; i += 1) { const initialDate = moment(latest).add(addDays * i, 'd') initialDates.push(initialDate.format(DATE_STR_FORMAT)) } } goToDate = (targetDate, animated = true) => { const { initialDates } = this.state const { numberOfDays } = this.props const currentDate = moment(initialDates[this.currentPageIndex]).startOf( 'day', ) const deltaDay = moment(targetDate).startOf('day').diff(currentDate, 'day') const deltaIndex = Math.floor(deltaDay / numberOfDays) const signToTheFuture = this.getSignToTheFuture() const targetIndex = this.currentPageIndex + deltaIndex * signToTheFuture this.goToPageIndex(targetIndex, animated) } goToNextPage = (animated = true) => { const signToTheFuture = this.getSignToTheFuture() this.goToPageIndex(this.currentPageIndex + 1 * signToTheFuture, animated) } goToPrevPage = (animated = true) => { const signToTheFuture = this.getSignToTheFuture() this.goToPageIndex(this.currentPageIndex - 1 * signToTheFuture, animated) } goToPageIndex = (target, animated = true) => { if (target === this.currentPageIndex) { return } const { initialDates } = this.state const scrollTo = (moveToIndex) => { this.eventsGrid.scrollToIndex({ index: moveToIndex, animated, }) this.currentPageIndex = moveToIndex } const newState = {} let newStateCallback = () => {} // The final target may change, if pages are added let targetIndex = target const lastViewablePage = initialDates.length - this.pageOffset if (targetIndex < this.pageOffset) { const nPages = this.pageOffset - targetIndex this.prependPagesInPlace(initialDates, nPages) targetIndex = this.pageOffset newState.initialDates = [...initialDates] newStateCallback = () => setTimeout(() => scrollTo(targetIndex), 0) } else if (targetIndex > lastViewablePage) { const nPages = targetIndex - lastViewablePage this.appendPagesInPlace(initialDates, nPages) targetIndex = initialDates.length - this.pageOffset newState.initialDates = [...initialDates] newStateCallback = () => setTimeout(() => scrollTo(targetIndex), 0) } else { scrollTo(targetIndex) } newState.currentMoment = moment(initialDates[targetIndex]).toDate() this.setState(newState, newStateCallback) } scrollBegun = () => { this.isScrollingHorizontal = true } scrollEnded = (event) => { if (!this.isScrollingHorizontal) { // Ensure the callback is called only once return } this.isScrollingHorizontal = false const { nativeEvent: { contentOffset, contentSize }, } = event const { x: position } = contentOffset const { width: innerWidth } = contentSize const { onSwipePrev, onSwipeNext } = this.props const { initialDates } = this.state const newPage = Math.round((position / innerWidth) * initialDates.length) const movedPages = newPage - this.currentPageIndex this.currentPageIndex = newPage if (movedPages === 0) { return } InteractionManager.runAfterInteractions(() => { const newMoment = moment(initialDates[this.currentPageIndex]).toDate() const newState = { currentMoment: newMoment, } let newStateCallback = () => {} if (movedPages < 0 && newPage < this.pageOffset) { this.prependPagesInPlace(initialDates, 1) this.currentPageIndex += 1 newState.initialDates = [...initialDates] const scrollToCurrentIndex = () => this.eventsGrid.scrollToIndex({ index: this.currentPageIndex, animated: false, }) newStateCallback = () => setTimeout(scrollToCurrentIndex, 0) } else if ( movedPages > 0 && newPage >= this.state.initialDates.length - this.pageOffset ) { this.appendPagesInPlace(initialDates, 1) newState.initialDates = [...initialDates] } this.setState(newState, newStateCallback) if (movedPages < 0) { onSwipePrev && onSwipePrev(newMoment) } else { onSwipeNext && onSwipeNext(newMoment) } }) } eventsGridRef = (ref) => { this.eventsGrid = ref } verticalAgendaRef = (ref) => { this.verticalAgenda = ref } headerRef = (ref) => { this.header = ref } calculatePagesDates = ( currentMoment, numberOfDays, weekStartsOn, prependMostRecent, fixedHorizontally, ) => { const initialDates = [] const centralDate = moment(currentMoment) if (numberOfDays === 7 || fixedHorizontally) { centralDate.subtract( // Ensure centralDate is before currentMoment (centralDate.day() + 7 - weekStartsOn) % 7, 'days', ) } for (let i = -this.pageOffset; i <= this.pageOffset; i += 1) { const initialDate = moment(centralDate).add(numberOfDays * i, 'd') initialDates.push(initialDate.format(DATE_STR_FORMAT)) } return prependMostRecent ? initialDates.reverse() : initialDates } sortEventsByDate = memoizeOne((events) => { // Stores the events hashed by their date // For example: { "2020-02-03": [event1, event2, ...] } // If an event spans through multiple days, adds the event multiple times const sortedEvents = {} events.forEach((event) => { const startDate = moment(event.startDate) const endDate = moment(event.endDate) for ( let date = moment(startDate); date.isSameOrBefore(endDate, 'days'); date.add(1, 'days') ) { // Calculate actual start and end dates const startOfDay = moment(date).startOf('day') const endOfDay = moment(date).endOf('day') const actualStartDate = moment.max(startDate, startOfDay) const actualEndDate = moment.min(endDate, endOfDay) // Add to object const dateStr = date.format(DATE_STR_FORMAT) if (!sortedEvents[dateStr]) { sortedEvents[dateStr] = [] } sortedEvents[dateStr].push({ ...event, startDate: actualStartDate.toDate(), endDate: actualEndDate.toDate(), }) } }) // For each day, sort the events by the minute (in-place) Object.keys(sortedEvents).forEach((date) => { sortedEvents[date].sort((a, b) => { return moment(a.startDate).diff(b.startDate, 'minutes') }) }) return sortedEvents }) getListItemLayout = (index) => ({ length: (WIDTH * 7) / 8, offset: ((WIDTH * 7) / 8) * index, index, }) render() { const { showTitle, numberOfDays, headerStyle, headerTextStyle, headerTextDateStyle, hourTextStyle, eventContainerStyle, TodayHeaderComponent, formatDateHeader, onEventPress, onEventLongPress, events, hoursInDisplay, timeStep, formatTimeLabel, onGridClick, onGridLongPress, EventComponent, prependMostRecent, rightToLeft, fixedHorizontally, showNowLine, nowLineColor, onIntervalSelected, } = this.props const { currentMoment, initialDates, scrollEnabled } = this.state const times = this.calculateTimes(timeStep, formatTimeLabel) const eventsByDate = this.sortEventsByDate(events) const horizontalInverted = (prependMostRecent && !rightToLeft) || (!prependMostRecent && rightToLeft) const handleIntervalChange = (startTime, endTime) => { this.setState({ topSelectedIndex: startTime, bottomSelectedIndex: endTime, }) } const handleScrollEnabled = () => { this.setState((state) => ({ ...state, scrollEnabled: !state.scrollEnabled, })) } return ( <View style={styles.container}> <View style={styles.headerContainer}> <Title showTitle={showTitle} style={headerStyle} textStyle={headerTextStyle} numberOfDays={numberOfDays} selectedDate={currentMoment} /> <VirtualizedList horizontal pagingEnabled inverted={horizontalInverted} showsHorizontalScrollIndicator={false} scrollEnabled={false} ref={this.headerRef} data={initialDates} getItem={(data, index) => data[index]} getItemCount={(data) => data.length} getItemLayout={(_, index) => this.getListItemLayout(index)} keyExtractor={(item) => item} initialScrollIndex={this.pageOffset} renderItem={({ item }) => { return ( <View key={item} style={styles.header}> <Header style={headerStyle} textStyle={headerTextStyle} textDateStyle={headerTextDateStyle} TodayComponent={TodayHeaderComponent} formatDate={formatDateHeader} initialDate={item} numberOfDays={numberOfDays} rightToLeft={rightToLeft} /> </View> ) }} /> </View> <ScrollView ref={this.verticalAgendaRef} scrollEventThrottle={100} scrollEnabled={scrollEnabled}> <View style={styles.scrollViewContent}> <Times times={times} textStyle={hourTextStyle} hoursInDisplay={hoursInDisplay} timeStep={timeStep} interval={{ start: this.state.topSelectedIndex, end: this.state.bottomSelectedIndex, }} /> <VirtualizedList data={initialDates} getItem={(data, index) => data[index]} getItemCount={(data) => data.length} getItemLayout={(_, index) => this.getListItemLayout(index)} keyExtractor={(item) => item} initialScrollIndex={this.pageOffset} scrollEnabled={!fixedHorizontally && scrollEnabled} renderItem={({ item }) => { return ( <Events times={times} eventsByDate={eventsByDate} initialDate={item} numberOfDays={numberOfDays} onEventPress={onEventPress} onEventLongPress={onEventLongPress} onGridClick={onGridClick} onGridLongPress={onGridLongPress} hoursInDisplay={hoursInDisplay} timeStep={timeStep} EventComponent={EventComponent} eventContainerStyle={eventContainerStyle} rightToLeft={rightToLeft} showNowLine={showNowLine} nowLineColor={nowLineColor} onTimeIntervalChanged={handleIntervalChange} onIntervalSelected={onIntervalSelected} setScrollEnabled={handleScrollEnabled} /> ) }} horizontal pagingEnabled inverted={horizontalInverted} onMomentumScrollBegin={this.scrollBegun} onMomentumScrollEnd={this.scrollEnded} scrollEventThrottle={32} onScroll={Animated.event( [ { nativeEvent: { contentOffset: { x: this.eventsGridScrollX, }, }, }, ], { useNativeDriver: false }, )} ref={this.eventsGridRef} /> </View> </ScrollView> </View> ) } } WeekView.propTypes = { events: PropTypes.arrayOf(Event.propTypes.event), formatDateHeader: PropTypes.string, numberOfDays: PropTypes.oneOf(availableNumberOfDays).isRequired, weekStartsOn: PropTypes.number, onSwipeNext: PropTypes.func, onSwipePrev: PropTypes.func, onEventPress: PropTypes.func, onEventLongPress: PropTypes.func, onGridClick: PropTypes.func, onGridLongPress: PropTypes.func, headerStyle: PropTypes.object, headerTextStyle: PropTypes.object, headerTextDateStyle: PropTypes.object, hourTextStyle: PropTypes.object, eventContainerStyle: PropTypes.object, selectedDate: PropTypes.instanceOf(Date).isRequired, locale: PropTypes.string, hoursInDisplay: PropTypes.number, timeStep: PropTypes.number, formatTimeLabel: PropTypes.string, startHour: PropTypes.number, EventComponent: PropTypes.elementType, TodayHeaderComponent: PropTypes.elementType, showTitle: PropTypes.bool, rightToLeft: PropTypes.bool, fixedHorizontally: PropTypes.bool, prependMostRecent: PropTypes.bool, showNowLine: PropTypes.bool, nowLineColor: PropTypes.string, onIntervalSelected: PropTypes.func, } WeekView.defaultProps = { events: [], locale: 'en', hoursInDisplay: 6, weekStartsOn: 1, timeStep: 60, formatTimeLabel: 'H:mm', startHour: 0, showTitle: true, rightToLeft: false, prependMostRecent: false, }