Skip to content

Commit 310f1dc

Browse files
authored
Merge pull request #1747 from wix/feat/TimelineList
Feat/timeline list
2 parents 60f674e + efb3a60 commit 310f1dc

File tree

11 files changed

+435
-39
lines changed

11 files changed

+435
-39
lines changed

example/src/screens/timelineCalendar.tsx

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1-
import XDate from 'xdate';
21
import React, {Component} from 'react';
32
import {Alert} from 'react-native';
4-
import {ExpandableCalendar, Timeline, CalendarProvider, TimelineProps} from 'react-native-calendars';
5-
import {sameDate} from '../../../src/dateutils';
3+
import {
4+
ExpandableCalendar,
5+
TimelineEventProps,
6+
TimelineList,
7+
CalendarProvider,
8+
TimelineProps,
9+
CalendarUtils
10+
} from 'react-native-calendars';
11+
import _ from 'lodash';
612

7-
const EVENTS = [
13+
const EVENTS: TimelineEventProps[] = [
814
{
915
start: '2017-09-06 01:30:00',
1016
end: '2017-09-06 02:30:00',
@@ -82,7 +88,9 @@ export default class TimelineCalendarScreen extends Component {
8288
state = {
8389
currentDate: '2017-09-10',
8490
events: EVENTS,
85-
newEvent: undefined
91+
eventsByDate: _.groupBy(EVENTS, e => CalendarUtils.getCalendarDateString(e.start)) as {
92+
[key: string]: TimelineEventProps[];
93+
}
8694
};
8795

8896
marked = {
@@ -103,45 +111,78 @@ export default class TimelineCalendarScreen extends Component {
103111
};
104112

105113
createNewEvent: TimelineProps['onBackgroundLongPress'] = (timeString, timeObject) => {
114+
const {eventsByDate} = this.state;
106115
const hourString = `${(timeObject.hour + 1).toString().padStart(2, '0')}`;
107116
const minutesString = `${timeObject.minutes.toString().padStart(2, '0')}`;
108117

109118
const newEvent = {
119+
id: 'draft',
110120
start: `${timeString}`,
111121
end: `${timeObject.date} ${hourString}:${minutesString}:00`,
112122
title: 'New Event',
113123
color: '#ffffff'
114124
};
115125

116-
this.setState({newEvent});
126+
if (timeObject.date) {
127+
if (eventsByDate[timeObject.date]) {
128+
eventsByDate[timeObject.date] = [...eventsByDate[timeObject.date], newEvent];
129+
this.setState({eventsByDate});
130+
} else {
131+
eventsByDate[timeObject.date] = [newEvent];
132+
this.setState({eventsByDate: {...eventsByDate}});
133+
}
134+
}
117135
};
118136

119-
approveNewEvent = () => {
137+
approveNewEvent: TimelineProps['onBackgroundLongPressOut'] = (_timeString, timeObject) => {
138+
const {eventsByDate} = this.state;
139+
120140
Alert.prompt('New Event', 'Enter event title', [
121141
{
122142
text: 'Cancel',
123143
onPress: () => {
124-
this.setState({
125-
newEvent: undefined
126-
});
144+
if (timeObject.date) {
145+
eventsByDate[timeObject.date] = _.filter(eventsByDate[timeObject.date], e => e.id !== 'draft');
146+
147+
this.setState({
148+
eventsByDate
149+
});
150+
}
127151
}
128152
},
129153
{
130154
text: 'Create',
131155
onPress: eventTitle => {
132-
const {newEvent = {}, events} = this.state;
133-
this.setState({
134-
newEvent: undefined,
135-
events: [...events, {...newEvent, title: eventTitle ?? 'New Event', color: '#d8ade6'}]
136-
});
156+
if (timeObject.date) {
157+
const draftEvent = _.find(eventsByDate[timeObject.date], {id: 'draft'});
158+
if (draftEvent) {
159+
draftEvent.id = undefined;
160+
draftEvent.title = eventTitle ?? 'New Event';
161+
draftEvent.color = '#d8ade6';
162+
eventsByDate[timeObject.date] = [...eventsByDate[timeObject.date]];
163+
164+
this.setState({
165+
eventsByDate
166+
});
167+
}
168+
}
137169
}
138170
}
139171
]);
140172
};
141173

174+
private timelineProps = {
175+
format24h: true,
176+
onBackgroundLongPress: this.createNewEvent,
177+
onBackgroundLongPressOut: this.approveNewEvent
178+
// scrollToFirst: true,
179+
// start: 0,
180+
// end: 24
181+
};
182+
142183
render() {
143-
const {currentDate, events, newEvent} = this.state;
144-
const timelineEvents = newEvent ? [...events, newEvent] : events;
184+
const {currentDate, eventsByDate} = this.state;
185+
145186
return (
146187
<CalendarProvider
147188
date={currentDate}
@@ -156,18 +197,7 @@ export default class TimelineCalendarScreen extends Component {
156197
rightArrowImageSource={require('../img/next.png')}
157198
markedDates={this.marked}
158199
/>
159-
<Timeline
160-
date={currentDate}
161-
format24h={true}
162-
eventTapped={e => e}
163-
events={timelineEvents.filter(event => sameDate(new XDate(event.start), new XDate(this.state.currentDate)))}
164-
scrollToFirst
165-
onBackgroundLongPress={this.createNewEvent}
166-
onBackgroundLongPressOut={this.approveNewEvent}
167-
showNowIndicator
168-
// start={0}
169-
// end={24}
170-
/>
200+
<TimelineList events={eventsByDate} timelineProps={this.timelineProps} />
171201
</CalendarProvider>
172202
);
173203
}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"memoize-one": "^5.2.1",
3737
"prop-types": "^15.5.10",
3838
"react-native-swipe-gestures": "^1.0.5",
39+
"recyclerlistview": "^3.0.5",
3940
"xdate": "^0.8.0"
4041
},
4142
"devDependencies": {

src/commons/constants.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
import {Dimensions} from 'react-native';
2+
3+
const {width: screenWidth, height: screenHeight} = Dimensions.get('screen');
4+
5+
export default {
6+
screenWidth,
7+
screenHeight
8+
};

src/dateutils.spec.js

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import XDate from 'xdate';
2-
import {sameMonth, sameWeek, isLTE, isGTE, month, page} from './dateutils';
2+
import {sameMonth, sameWeek, isLTE, isGTE, month, page, generateDay} from './dateutils';
33

44
describe('dateutils', function () {
55
describe('sameMonth()', function () {
@@ -162,4 +162,21 @@ describe('dateutils', function () {
162162
}
163163
});
164164
});
165+
166+
describe('generateDay', () => {
167+
it('should generate a day in string format with an offset', () => {
168+
expect(generateDay('2017-09-22', 2)).toBe('2017-09-24');
169+
expect(generateDay('2017-09-22', -2)).toBe('2017-09-20');
170+
});
171+
172+
it('should generate the same day when offset was not sent', () => {
173+
expect(generateDay('2017-09-22')).toBe('2017-09-22');
174+
});
175+
176+
it('should handle month and year changes', () => {
177+
expect(generateDay('2017-10-22', 10)).toBe('2017-11-01');
178+
expect(generateDay('2017-12-26', 10)).toBe('2018-01-05');
179+
expect(generateDay('2018-01-01', -3)).toBe('2017-12-29');
180+
});
181+
});
165182
});

src/dateutils.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,14 @@
11
const XDate = require('xdate');
2-
const {parseDate} = require('./interface');
2+
const {parseDate, toMarkingFormat} = require('./interface');
33

44
const latinNumbersPattern = /[0-9]/g;
55

66
export function sameMonth(a: XDate, b: XDate) {
7-
return a.getFullYear() === b.getFullYear() &&
8-
a.getMonth() === b.getMonth();
7+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth();
98
}
109

1110
export function sameDate(a: XDate, b: XDate) {
12-
return a.getFullYear() === b.getFullYear() &&
13-
a.getMonth() === b.getMonth() &&
14-
a.getDate() === b.getDate();
11+
return a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate();
1512
}
1613

1714
export function sameWeek(a: XDate, b: XDate, firstDayOfWeek: number) {
@@ -144,3 +141,8 @@ export function getWeekDates(date: XDate, firstDay = 0, format?: string) {
144141
return daysArray;
145142
}
146143
}
144+
145+
export function generateDay(originDate: string, daysOffset = 0) {
146+
const baseDate = new XDate(originDate);
147+
return toMarkingFormat(baseDate.clone().addDays(daysOffset));
148+
}

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ export type {CalendarContextProviderProps} from './expandableCalendar/Context/Pr
1616
export {default as asCalendarConsumer} from './expandableCalendar/asCalendarConsumer';
1717
export {default as Timeline} from './timeline/Timeline';
1818
export type {TimelineProps, TimelineEventProps, TimelinePackedEventProps} from './timeline/Timeline';
19+
export {default as TimelineList} from './timeline-list';
1920
export {default as CalendarUtils} from './services';
2021
export {DateData, AgendaEntry, AgendaSchedule} from './types';
2122
export {default as LocaleConfig} from 'xdate';
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
// TODO: Make this a common component for all horizontal lists in this lib
2+
import React, {forwardRef, useCallback, useMemo, useRef} from 'react';
3+
import {DataProvider, LayoutProvider, RecyclerListView, RecyclerListViewProps} from 'recyclerlistview';
4+
import constants from '../commons/constants';
5+
6+
const layoutProvider = new LayoutProvider(
7+
() => 'page',
8+
(_type, dim) => {
9+
dim.width = constants.screenWidth;
10+
dim.height = constants.screenHeight;
11+
}
12+
);
13+
14+
const dataProviderMaker = (items: string[]) =>
15+
new DataProvider((item1, item2) => item1.value !== item2.value || item1.label !== item2.label).cloneWithRows(items);
16+
17+
export interface HorizontalListProps
18+
extends Omit<RecyclerListViewProps, 'dataProvider' | 'layoutProvider' | 'rowRenderer'> {
19+
data: any[];
20+
renderItem: RecyclerListViewProps['rowRenderer'];
21+
pageWidth?: number;
22+
onPageChange?: (pageIndex: number, prevPageIndex: number) => void;
23+
initialPageIndex?: number;
24+
}
25+
26+
const HorizontalList = (props: HorizontalListProps, ref: any) => {
27+
const {
28+
renderItem,
29+
data,
30+
pageWidth = constants.screenWidth,
31+
onPageChange,
32+
initialPageIndex = 0,
33+
extendedState,
34+
scrollViewProps
35+
} = props;
36+
const dataProvider = useMemo(() => {
37+
return dataProviderMaker(data);
38+
}, [data]);
39+
40+
const pageIndex = useRef<number>();
41+
42+
const onScroll = useCallback(
43+
(event, offsetX, offsetY) => {
44+
const currPageIndex = Math.round(event.nativeEvent.contentOffset.x / pageWidth);
45+
46+
if (pageIndex.current !== currPageIndex) {
47+
if (pageIndex.current !== undefined) {
48+
onPageChange?.(currPageIndex, pageIndex.current);
49+
}
50+
pageIndex.current = currPageIndex;
51+
}
52+
53+
props.onScroll?.(event, offsetX, offsetY);
54+
},
55+
[props.onScroll]
56+
);
57+
58+
return (
59+
<RecyclerListView
60+
ref={ref}
61+
isHorizontal
62+
rowRenderer={renderItem}
63+
dataProvider={dataProvider}
64+
layoutProvider={layoutProvider}
65+
extendedState={extendedState}
66+
initialRenderIndex={initialPageIndex}
67+
renderAheadOffset={5 * pageWidth}
68+
onScroll={onScroll}
69+
scrollViewProps={{
70+
pagingEnabled: true,
71+
bounces: false,
72+
...scrollViewProps
73+
}}
74+
/>
75+
);
76+
};
77+
78+
export default forwardRef(HorizontalList);

0 commit comments

Comments
 (0)