Skip to content

Commit 6682be0

Browse files
committed
Break main Timeline component to small components
1 parent 4d8f7b4 commit 6682be0

File tree

4 files changed

+179
-111
lines changed

4 files changed

+179
-111
lines changed

src/timeline/EventBlock.tsx

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React, {useCallback, useMemo} from 'react';
2+
import {View, Text, TextStyle, TouchableOpacity, ViewStyle} from 'react-native';
3+
import XDate from 'xdate';
4+
5+
export interface Event {
6+
start: string;
7+
end: string;
8+
title: string;
9+
summary: string;
10+
color?: string;
11+
}
12+
13+
export interface PackedEvent extends Event {
14+
left: number;
15+
top: number;
16+
width: number;
17+
height: number;
18+
}
19+
20+
export interface EventBlockProps {
21+
index: number;
22+
event: PackedEvent;
23+
onPress: (eventIndex: number) => void;
24+
renderEvent?: (event: PackedEvent) => JSX.Element;
25+
format24h?: boolean;
26+
styles: {[key: string]: ViewStyle | TextStyle};
27+
}
28+
29+
const TEXT_LINE_HEIGHT = 17;
30+
31+
const EventBlock = (props: EventBlockProps) => {
32+
const {index, event, renderEvent, onPress, format24h, styles} = props;
33+
34+
// Fixing the number of lines for the event title makes this calculation easier.
35+
// However it would make sense to overflow the title to a new line if needed
36+
const numberOfLines = Math.floor(event.height / TEXT_LINE_HEIGHT);
37+
const formatTime = format24h ? 'HH:mm' : 'hh:mm A';
38+
const eventStyle = useMemo(() => {
39+
return {
40+
left: event.left,
41+
height: event.height,
42+
width: event.width,
43+
top: event.top,
44+
backgroundColor: event.color ? event.color : '#add8e6'
45+
};
46+
}, [event]);
47+
48+
const _onPress = useCallback(() => {
49+
onPress(index);
50+
}, [index, onPress]);
51+
52+
return (
53+
<TouchableOpacity activeOpacity={0.9} onPress={_onPress} style={[styles.event, eventStyle]}>
54+
{renderEvent ? (
55+
renderEvent(event)
56+
) : (
57+
<View>
58+
<Text numberOfLines={1} style={styles.eventTitle}>
59+
{event.title || 'Event'}
60+
</Text>
61+
{numberOfLines > 1 ? (
62+
<Text numberOfLines={numberOfLines - 1} style={[styles.eventSummary]}>
63+
{event.summary || ' '}
64+
</Text>
65+
) : null}
66+
{numberOfLines > 2 ? (
67+
<Text style={styles.eventTimes} numberOfLines={1}>
68+
{new XDate(event.start).toString(formatTime)} - {new XDate(event.end).toString(formatTime)}
69+
</Text>
70+
) : null}
71+
</View>
72+
)}
73+
</TouchableOpacity>
74+
);
75+
};
76+
77+
export default EventBlock;

src/timeline/Packer.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
// @flow
22
import XDate from 'xdate';
3-
import {Event} from './Timeline';
3+
import {Event, PackedEvent} from './EventBlock';
44

5-
6-
const offset = 100;
5+
export const HALF_HOUR_BLOCK_HEIGHT = 100;
76

87
function buildEvent(column: any, left: number, width: number, dayStart: number) {
98
const startTime = new XDate(column.start);
109
const endTime = column.end ? new XDate(column.end) : new XDate(startTime).addHours(1);
1110

1211
const dayStartTime = new XDate(startTime).clearTime();
1312

14-
column.top = (dayStartTime.diffHours(startTime) - dayStart) * offset;
15-
column.height = startTime.diffHours(endTime) * offset;
13+
column.top = (dayStartTime.diffHours(startTime) - dayStart) * HALF_HOUR_BLOCK_HEIGHT;
14+
column.height = startTime.diffHours(endTime) * HALF_HOUR_BLOCK_HEIGHT;
1615
column.width = width;
1716
column.left = left;
1817
return column;
@@ -101,7 +100,7 @@ function populateEvents(events: Event[], screenWidth: number, dayStart: number)
101100
if (columns.length > 0) {
102101
pack(columns, screenWidth, calculatedEvents, dayStart);
103102
}
104-
return calculatedEvents;
103+
return calculatedEvents as PackedEvent[];
105104
}
106105

107106
export default populateEvents;

src/timeline/Timeline.tsx

Lines changed: 30 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,20 @@
22
import min from 'lodash/min';
33
import map from 'lodash/map';
44
import PropTypes from 'prop-types';
5-
import XDate from 'xdate';
65

76
import React, {Component} from 'react';
8-
import {View, Text, TouchableOpacity, Dimensions, ScrollView, TextStyle, ViewStyle} from 'react-native';
7+
import {View, Dimensions, ScrollView, TextStyle, ViewStyle} from 'react-native';
98

109
import {Theme} from '../types';
1110
import styleConstructor from './style';
12-
import populateEvents from './Packer';
13-
11+
import populateEvents, {HALF_HOUR_BLOCK_HEIGHT} from './Packer';
12+
import TimelineHours from './TimelineHours';
13+
import EventBlock, {Event, PackedEvent} from './EventBlock';
1414

1515
const LEFT_MARGIN = 60 - 1;
16-
const TEXT_LINE_HEIGHT = 17;
17-
18-
function range(from: number, to: number) {
19-
return Array.from(Array(to), (_, i) => from + i);
20-
}
2116

2217
const {width: dimensionWidth} = Dimensions.get('window');
2318

24-
export type Event = {
25-
start: string;
26-
end: string;
27-
title: string;
28-
summary: string;
29-
color?: string;
30-
};
31-
3219
export interface TimelineProps {
3320
events: Event[];
3421
start?: number;
@@ -39,12 +26,12 @@ export interface TimelineProps {
3926
theme?: Theme;
4027
scrollToFirst?: boolean;
4128
format24h?: boolean;
42-
renderEvent?: (event: Event) => JSX.Element;
29+
renderEvent?: (event: PackedEvent) => JSX.Element;
4330
}
4431

4532
interface State {
4633
_scrollY: number;
47-
packedEvents: Event[];
34+
packedEvents: PackedEvent[];
4835
}
4936

5037
export default class Timeline extends Component<TimelineProps, State> {
@@ -72,21 +59,22 @@ export default class Timeline extends Component<TimelineProps, State> {
7259
format24h: true
7360
};
7461

75-
private scrollView: React.RefObject<any> = React.createRef();
62+
private scrollView = React.createRef<ScrollView>();
7663
style: {[key: string]: ViewStyle | TextStyle};
7764
calendarHeight: number;
7865

7966
constructor(props: TimelineProps) {
8067
super(props);
8168

8269
const {start = 0, end = 0} = this.props;
83-
this.calendarHeight = (end - start) * 100;
70+
this.calendarHeight = (end - start) * HALF_HOUR_BLOCK_HEIGHT;
8471

8572
this.style = styleConstructor(props.theme || props.styles, this.calendarHeight);
8673

8774
const width = dimensionWidth - LEFT_MARGIN;
8875
const packedEvents = populateEvents(props.events, width, start);
89-
const initPosition = min(map(packedEvents, 'top')) - this.calendarHeight / (end - start);
76+
const firstTop = min(map(packedEvents, 'top')) ?? 0;
77+
const initPosition = firstTop - this.calendarHeight / (end - start);
9078
const verifiedInitPosition = initPosition < 0 ? 0 : initPosition;
9179

9280
this.state = {
@@ -123,92 +111,31 @@ export default class Timeline extends Component<TimelineProps, State> {
123111
}, 1);
124112
}
125113

126-
_renderLines() {
127-
const {format24h, start = 0, end = 24} = this.props;
128-
const offset = this.calendarHeight / (end - start);
129-
const EVENT_DIFF = 20;
130-
131-
return range(start, end + 1).map((i, index) => {
132-
let timeText;
133-
134-
if (i === start) {
135-
timeText = '';
136-
} else if (i < 12) {
137-
timeText = !format24h ? `${i} AM` : `${i}:00`;
138-
} else if (i === 12) {
139-
timeText = !format24h ? `${i} PM` : `${i}:00`;
140-
} else if (i === 24) {
141-
timeText = !format24h ? '12 AM' : '23:59';
142-
} else {
143-
timeText = !format24h ? `${i - 12} PM` : `${i}:00`;
144-
}
145-
146-
return [
147-
<Text key={`timeLabel${i}`} style={[this.style.timeLabel, {top: offset * index - 6}]}>
148-
{timeText}
149-
</Text>,
150-
i === start ? null : (
151-
<View key={`line${i}`} style={[this.style.line, {top: offset * index, width: dimensionWidth - EVENT_DIFF}]} />
152-
),
153-
<View
154-
key={`lineHalf${i}`}
155-
style={[this.style.line, {top: offset * (index + 0.5), width: dimensionWidth - EVENT_DIFF}]}
156-
/>
157-
];
158-
});
159-
}
160-
161-
_onEventPress(event: Event) {
162-
if (this.props.eventTapped) { //TODO: remove after deprecation
114+
onEventPress = (eventIndex: number) => {
115+
const event = this.props.events[eventIndex];
116+
if (this.props.eventTapped) {
117+
//TODO: remove after deprecation
163118
this.props.eventTapped(event);
164119
} else {
165120
this.props.onEventPress?.(event);
166121
}
167-
}
122+
};
168123

169-
_renderEvents() {
124+
renderEvents() {
170125
const {packedEvents} = this.state;
171-
const events = packedEvents.map((event: any, i: number) => {
172-
const style = {
173-
left: event.left,
174-
height: event.height,
175-
width: event.width,
176-
top: event.top,
177-
backgroundColor: event.color ? event.color : '#add8e6'
178-
};
179-
180-
// Fixing the number of lines for the event title makes this calculation easier.
181-
// However it would make sense to overflow the title to a new line if needed
182-
const numberOfLines = Math.floor(event.height / TEXT_LINE_HEIGHT);
183-
const formatTime = this.props.format24h ? 'HH:mm' : 'hh:mm A';
126+
const {format24h, renderEvent} = this.props;
184127

128+
const events = packedEvents.map((event: PackedEvent, i: number) => {
185129
return (
186-
<TouchableOpacity
187-
activeOpacity={0.9}
188-
onPress={() => this._onEventPress(this.props.events[event.index])}
130+
<EventBlock
189131
key={i}
190-
style={[this.style.event, style]}
191-
>
192-
{this.props.renderEvent ? (
193-
this.props.renderEvent(event)
194-
) : (
195-
<View>
196-
<Text numberOfLines={1} style={this.style.eventTitle}>
197-
{event.title || 'Event'}
198-
</Text>
199-
{numberOfLines > 1 ? (
200-
<Text numberOfLines={numberOfLines - 1} style={[this.style.eventSummary]}>
201-
{event.summary || ' '}
202-
</Text>
203-
) : null}
204-
{numberOfLines > 2 ? (
205-
<Text style={this.style.eventTimes} numberOfLines={1}>
206-
{new XDate(event.start).toString(formatTime)} - {new XDate(event.end).toString(formatTime)}
207-
</Text>
208-
) : null}
209-
</View>
210-
)}
211-
</TouchableOpacity>
132+
index={i}
133+
event={event}
134+
styles={this.style}
135+
format24h={format24h}
136+
onPress={this.onEventPress}
137+
renderEvent={renderEvent}
138+
/>
212139
);
213140
});
214141

@@ -220,13 +147,11 @@ export default class Timeline extends Component<TimelineProps, State> {
220147
}
221148

222149
render() {
150+
const {format24h, start, end} = this.props;
223151
return (
224-
<ScrollView
225-
ref={this.scrollView}
226-
contentContainerStyle={[this.style.contentStyle, {width: dimensionWidth}]}
227-
>
228-
{this._renderLines()}
229-
{this._renderEvents()}
152+
<ScrollView ref={this.scrollView} contentContainerStyle={[this.style.contentStyle, {width: dimensionWidth}]}>
153+
<TimelineHours start={start} end={end} format24h={format24h} styles={this.style} />
154+
{this.renderEvents()}
230155
</ScrollView>
231156
);
232157
}

src/timeline/TimelineHours.tsx

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import React, {useMemo} from 'react';
2+
import {View, Text, ViewStyle, TextStyle, Dimensions} from 'react-native';
3+
import range from 'lodash/range';
4+
import {HALF_HOUR_BLOCK_HEIGHT} from './Packer';
5+
6+
const {width: dimensionWidth} = Dimensions.get('window');
7+
8+
interface TimelineHoursProps {
9+
start?: number;
10+
end?: number;
11+
format24h?: boolean;
12+
styles: {[key: string]: ViewStyle | TextStyle};
13+
}
14+
15+
const TimelineHours = (props: TimelineHoursProps) => {
16+
const {format24h, start = 0, end = 24, styles} = props;
17+
// const offset = this.calendarHeight / (end - start);
18+
const offset = HALF_HOUR_BLOCK_HEIGHT;
19+
const EVENT_DIFF = 20;
20+
21+
const hours = useMemo(() => {
22+
return range(start, end + 1).map(i => {
23+
let timeText;
24+
25+
if (i === start) {
26+
timeText = '';
27+
} else if (i < 12) {
28+
timeText = !format24h ? `${i} AM` : `${i}:00`;
29+
} else if (i === 12) {
30+
timeText = !format24h ? `${i} PM` : `${i}:00`;
31+
} else if (i === 24) {
32+
timeText = !format24h ? '12 AM' : '23:59';
33+
} else {
34+
timeText = !format24h ? `${i - 12} PM` : `${i}:00`;
35+
}
36+
return {timeText, time: i};
37+
});
38+
}, [start, end, format24h]);
39+
40+
return (
41+
<>
42+
{hours.map(({timeText, time}, index) => {
43+
return (
44+
<React.Fragment key={time}>
45+
<Text key={`timeLabel${time}`} style={[styles.timeLabel, {top: offset * index - 6}]}>
46+
{timeText}
47+
</Text>
48+
{time === start ? null : (
49+
<View
50+
key={`line${time}`}
51+
style={[styles.line, {top: offset * index, width: dimensionWidth - EVENT_DIFF}]}
52+
/>
53+
)}
54+
{
55+
<View
56+
key={`lineHalf${time}`}
57+
style={[styles.line, {top: offset * (index + 0.5), width: dimensionWidth - EVENT_DIFF}]}
58+
/>
59+
}
60+
</React.Fragment>
61+
);
62+
})}
63+
</>
64+
);
65+
};
66+
67+
export default React.memo(TimelineHours);

0 commit comments

Comments
 (0)