UNPKG

react-native-schedule-week-view

Version:
370 lines (330 loc) 11.3 kB
import React, { PureComponent } from 'react'; import PropTypes from 'prop-types'; import { View, TouchableWithoutFeedback } from 'react-native'; import moment from 'moment'; import memoizeOne from 'memoize-one'; import NowLine from '../NowLine/NowLine'; import Event from '../Event/Event'; import { CONTAINER_HEIGHT, CONTAINER_WIDTH, calculateDaysArray, DATE_STR_FORMAT, availableNumberOfDays, minutesToYDimension, CONTENT_OFFSET, getTimeLabelHeight, } from '../utils'; import styles from './Events.styles'; const MINUTES_IN_HOUR = 60; const EVENT_HORIZONTAL_PADDING = 15; const EVENTS_CONTAINER_WIDTH = CONTAINER_WIDTH - EVENT_HORIZONTAL_PADDING; const MIN_ITEM_WIDTH = 4; const ALLOW_OVERLAP_SECONDS = 2; const areEventsOverlapped = (event1EndDate, event2StartDate) => { const endDate = moment(event1EndDate); endDate.subtract(ALLOW_OVERLAP_SECONDS, 'seconds'); return endDate.isSameOrAfter(event2StartDate); }; const getStyleForEvent = (event, regularItemWidth, hoursInDisplay) => { const startDate = moment(event.startDate); const startHours = startDate.hours(); const startMinutes = startDate.minutes(); const totalStartMinutes = startHours * MINUTES_IN_HOUR + startMinutes; const top = minutesToYDimension(hoursInDisplay, totalStartMinutes); const deltaMinutes = moment(event.endDate).diff(event.startDate, 'minutes'); const height = minutesToYDimension(hoursInDisplay, deltaMinutes); return { top: top + CONTENT_OFFSET, left: 0, height, width: regularItemWidth, }; }; const addOverlappedToArray = (baseArr, overlappedArr, itemWidth) => { // Given an array of overlapped events (with style), modifies their style to overlap them // and adds them to a (base) array of events. if (!overlappedArr) return; const nOverlapped = overlappedArr.length; if (nOverlapped === 0) { return; } if (nOverlapped === 1) { baseArr.push(overlappedArr[0]); return; } let nLanes; let horizontalPadding; let indexToLane; if (nOverlapped === 2) { nLanes = nOverlapped; horizontalPadding = 3; indexToLane = (index) => index; } else { // Distribute events in multiple lanes const maxLanes = nOverlapped; const latestByLane = {}; const laneByEvent = {}; overlappedArr.forEach((event, index) => { for (let lane = 0; lane < maxLanes; lane += 1) { const lastEvtInLaneIndex = latestByLane[lane]; const lastEvtInLane = (lastEvtInLaneIndex || lastEvtInLaneIndex === 0) && overlappedArr[lastEvtInLaneIndex]; if ( !lastEvtInLane || !areEventsOverlapped(lastEvtInLane.data.endDate, event.data.startDate) ) { // Place in this lane latestByLane[lane] = index; laneByEvent[index] = lane; break; } } }); nLanes = Object.keys(latestByLane).length; horizontalPadding = 2; indexToLane = (index) => laneByEvent[index]; } const dividedWidth = itemWidth / nLanes; const width = Math.max(dividedWidth - horizontalPadding, MIN_ITEM_WIDTH); overlappedArr.forEach((eventWithStyle, index) => { const { data, style } = eventWithStyle; baseArr.push({ data, style: { ...style, width, left: dividedWidth * indexToLane(index), }, }); }); }; const getEventsWithPosition = ( totalEvents, regularItemWidth, hoursInDisplay, ) => { return totalEvents.map((events) => { let overlappedSoFar = []; // Store events overlapped until now let lastDate = null; const eventsWithStyle = events.reduce((eventsAcc, event) => { const style = getStyleForEvent(event, regularItemWidth, hoursInDisplay); const eventWithStyle = { data: event, style, }; if (!lastDate || areEventsOverlapped(lastDate, event.startDate)) { overlappedSoFar.push(eventWithStyle); const endDate = moment(event.endDate); lastDate = lastDate ? moment.max(endDate, lastDate) : endDate; } else { addOverlappedToArray(eventsAcc, overlappedSoFar, regularItemWidth); overlappedSoFar = [eventWithStyle]; lastDate = moment(event.endDate); } return eventsAcc; }, []); addOverlappedToArray(eventsWithStyle, overlappedSoFar, regularItemWidth); return eventsWithStyle; }); }; class Events extends PureComponent { yToHour = (y) => { const { hoursInDisplay } = this.props; const hour = (y * hoursInDisplay) / CONTAINER_HEIGHT; return hour; }; getEventItemWidth = (padded = true) => { const { numberOfDays } = this.props; const fullWidth = padded ? EVENTS_CONTAINER_WIDTH : CONTAINER_WIDTH; return fullWidth / numberOfDays; }; processEvents = memoizeOne( (eventsByDate, initialDate, numberOfDays, hoursInDisplay, rightToLeft) => { // totalEvents stores events in each day of numberOfDays // example: [[event1, event2], [event3, event4], [event5]], each child array // is events for specific day in range const dates = calculateDaysArray(initialDate, numberOfDays, rightToLeft); const totalEvents = dates.map((date) => { const dateStr = date.format(DATE_STR_FORMAT); return eventsByDate[dateStr] || []; }); const regularItemWidth = this.getEventItemWidth(); const totalEventsWithPosition = getEventsWithPosition( totalEvents, regularItemWidth, hoursInDisplay, ); return totalEventsWithPosition; }, ); onGridTouch = (event, dayIndex, longPress, eventsInSection) => { const { initialDate, onGridClick, onGridLongPress } = this.props; const callback = longPress ? onGridLongPress : onGridClick; if (!callback) { return; } const { locationY } = event.nativeEvent; // WithDec === with decimals. // e.g. hours 10.5 === 10:30am const hoursWDec = this.yToHour(locationY - CONTENT_OFFSET); const minutesWDec = (hoursWDec - parseInt(hoursWDec, 10)) * 60; const seconds = Math.floor((minutesWDec - parseInt(minutesWDec, 10)) * 60); const hour = Math.floor(hoursWDec); const minutes = Math.floor(minutesWDec); const date = moment(initialDate) .add(dayIndex, 'day') .hours(hour) .minutes(minutes) .seconds(seconds) .toDate(); callback(event, hour, minutes, date, eventsInSection); }; onDragEvent = (event, newX, newY) => { const { onDragEvent } = this.props; if (!onDragEvent) { return; } const movedDays = Math.floor(newX / this.getEventItemWidth()); const startTime = event.startDate.getTime(); const newStartDate = new Date(startTime); newStartDate.setDate(newStartDate.getDate() + movedDays); let newMinutes = this.yToHour(newY - CONTENT_OFFSET) * 60; const newHour = Math.floor(newMinutes / 60); newMinutes %= 60; newStartDate.setHours(newHour, newMinutes); const newEndDate = new Date( newStartDate.getTime() + event.originalDuration, ); onDragEvent(event, newStartDate, newEndDate); }; isToday = (dayIndex) => { const { initialDate } = this.props; const today = moment(); return moment(initialDate).add(dayIndex, 'days').isSame(today, 'day'); }; // isGridClicked = (time,dayindex, timeSelected, dayIndexSelected) => { // return moment(initialDate).add(dayIndex, 'days').isSame(today, 'day'); // }; render() { const { eventsByDate, initialDate, numberOfDays, times, onEventPress, onEventLongPress, eventContainerStyle, gridRowStyle, gridColumnStyle, EventComponent, rightToLeft, hoursInDisplay, timeStep, showNowLine, nowLineColor, onDragEvent, } = this.props; const totalEvents = this.processEvents( eventsByDate, initialDate, numberOfDays, hoursInDisplay, rightToLeft, ); const timeSlotHeight = getTimeLabelHeight(hoursInDisplay, timeStep); return ( <View style={styles.container}> {times.map((time) => { return ( <View key={time} style={[styles.timeRow, { height: timeSlotHeight }, gridRowStyle]} /> ); })} <View style={styles.eventsContainer}> {totalEvents.map((eventsInSection, dayIndex) => { return ( <> {times.map((time) => { return ( <View key={time} style={[ styles.timeRow, { height: timeSlotHeight, backgroundColor: '#ff0' }, gridRowStyle, ]} /> ); })} <TouchableWithoutFeedback onPress={(e) => this.onGridTouch(e, dayIndex, false, eventsInSection) } onLongPress={(e) => this.onGridTouch(e, dayIndex, true, eventsInSection) } key={dayIndex} > <View style={[styles.eventsColumn, gridColumnStyle]}> {showNowLine && this.isToday(dayIndex) && ( <NowLine color={nowLineColor} hoursInDisplay={hoursInDisplay} width={this.getEventItemWidth(false)} /> )} {eventsInSection.map((item) => ( <Event key={item.data.id} event={item.data} position={item.style} onPress={onEventPress} onLongPress={onEventLongPress} EventComponent={EventComponent} containerStyle={eventContainerStyle} onDrag={onDragEvent && this.onDragEvent} /> ))} </View> </TouchableWithoutFeedback> </> ); })} </View> </View> ); } } const GridRowPropType = PropTypes.shape({ borderColor: PropTypes.string, borderTopWidth: PropTypes.number, }); const GridColumnPropType = PropTypes.shape({ borderColor: PropTypes.string, borderLeftWidth: PropTypes.number, }); Events.propTypes = { numberOfDays: PropTypes.oneOf(availableNumberOfDays).isRequired, eventsByDate: PropTypes.objectOf(PropTypes.arrayOf(Event.propTypes.event)) .isRequired, initialDate: PropTypes.string.isRequired, hoursInDisplay: PropTypes.number.isRequired, timeStep: PropTypes.number.isRequired, times: PropTypes.arrayOf(PropTypes.string).isRequired, onEventPress: PropTypes.func, onEventLongPress: PropTypes.func, onGridClick: PropTypes.func, onGridLongPress: PropTypes.func, eventContainerStyle: PropTypes.object, gridRowStyle: GridRowPropType, gridColumnStyle: GridColumnPropType, EventComponent: PropTypes.elementType, rightToLeft: PropTypes.bool, showNowLine: PropTypes.bool, nowLineColor: PropTypes.string, onDragEvent: PropTypes.func, }; export default Events;