UNPKG

react-native-schedule-week-view

Version:
601 lines (542 loc) 18.2 kB
import React, { Component } from 'react'; import PropTypes from 'prop-types'; import { View, ScrollView, Animated, VirtualizedList, InteractionManager, ActivityIndicator, } 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, CONTAINER_WIDTH, } from '../utils'; const MINUTES_IN_DAY = 60 * 24; const calculateTimesArray = ( minutesStep, formatTimeLabel, beginAt = 0, endAt = MINUTES_IN_DAY, ) => { const times = []; const startOfDay = moment().startOf('day'); for ( let timer = beginAt >= 0 && beginAt < MINUTES_IN_DAY ? beginAt : 0; timer < endAt && timer < MINUTES_IN_DAY; timer += minutesStep ) { const time = startOfDay.clone().minutes(timer); times.push(time.format(formatTimeLabel)); } return times; }; 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, }; 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); } if (this.props.numberOfDays !== prevProps.numberOfDays) { const initialDates = this.calculatePagesDates( this.state.currentMoment, this.props.numberOfDays, this.props.prependMostRecent, this.props.fixedHorizontally, ); this.currentPageIndex = this.pageOffset; this.setState( { currentMoment: moment(initialDates[this.currentPageIndex]).toDate(), initialDates, }, () => { this.eventsGrid.scrollToIndex({ index: this.pageOffset, animated: false, }); }, ); } } componentWillUnmount() { this.eventsGridScrollX.removeAllListeners(); } calculateTimes = memoizeOne(calculateTimesArray); 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 (!fixedHorizontally) { centralDate.subtract( // Ensure centralDate is before currentMoment (centralDate.day() + numberOfDays - weekStartsOn) % numberOfDays, '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) => { // in milliseconds const originalDuration = event.endDate.getTime() - event.startDate.getTime(); 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(), originalDuration, }); } }); // 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: CONTAINER_WIDTH, offset: CONTAINER_WIDTH * index, index, }); render() { const { showTitle, numberOfDays, headerStyle, headerTextStyle, hourTextStyle, gridRowStyle, gridColumnStyle, eventContainerStyle, TodayHeaderComponent, formatDateHeader, onEventPress, onEventLongPress, events, hoursInDisplay, timeStep, beginAgendaAt, endAgendaAt, formatTimeLabel, onGridClick, onGridLongPress, EventComponent, prependMostRecent, rightToLeft, fixedHorizontally, showNowLine, nowLineColor, onDragEvent, isRefreshing, RefreshComponent, today } = this.props; const { currentMoment, initialDates } = this.state; const times = this.calculateTimes( timeStep, formatTimeLabel, beginAgendaAt, endAgendaAt, ); const eventsByDate = this.sortEventsByDate(events); const horizontalInverted = (prependMostRecent && !rightToLeft) || (!prependMostRecent && rightToLeft); 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} TodayComponent={TodayHeaderComponent} formatDate={formatDateHeader} initialDate={item} rightToLeft={rightToLeft} today={today} /> </View> ); }} /> </View> {isRefreshing && RefreshComponent && ( <RefreshComponent style={styles.loadingSpinner} /> )} <ScrollView onStartShouldSetResponderCapture={() => false} onMoveShouldSetResponderCapture={() => false} onResponderTerminationRequest={() => false} ref={this.verticalAgendaRef} > <View style={styles.scrollViewContent}> <Times times={times} textStyle={hourTextStyle} hoursInDisplay={hoursInDisplay} timeStep={timeStep} /> <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} onStartShouldSetResponderCapture={() => false} onMoveShouldSetResponderCapture={() => false} onResponderTerminationRequest={() => false} 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} gridRowStyle={gridRowStyle} gridColumnStyle={gridColumnStyle} rightToLeft={rightToLeft} showNowLine={showNowLine} nowLineColor={nowLineColor} onDragEvent={onDragEvent} /> ); }} 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, hourTextStyle: PropTypes.object, eventContainerStyle: PropTypes.object, gridRowStyle: Events.propTypes.gridRowStyle, gridColumnStyle: Events.propTypes.gridColumnStyle, selectedDate: PropTypes.instanceOf(Date).isRequired, today: PropTypes.string, locale: PropTypes.string, hoursInDisplay: PropTypes.number, timeStep: PropTypes.number, beginAgendaAt: PropTypes.number, endAgendaAt: PropTypes.number, formatTimeLabel: PropTypes.string, startHour: PropTypes.number, initialHour: PropTypes.number, finalHour: 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, onDragEvent: PropTypes.func, isRefreshing: PropTypes.bool, RefreshComponent: PropTypes.elementType, }; WeekView.defaultProps = { events: [], locale: 'en', hoursInDisplay: 6, weekStartsOn: 1, timeStep: 60, beginAgendaAt: 0, endAgendaAt: MINUTES_IN_DAY, formatTimeLabel: 'H:mm', startHour: 8, initialHour: 0, finalHour: 24, showTitle: true, rightToLeft: false, prependMostRecent: false, RefreshComponent: ActivityIndicator, };