Skip to content

Commit 885cd67

Browse files
committed
Support Timeline unavailableHours
1 parent 1ca84dd commit 885cd67

File tree

6 files changed

+182
-24
lines changed

6 files changed

+182
-24
lines changed

example/src/screens/timelineCalendar.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -217,13 +217,14 @@ export default class TimelineCalendarScreen extends Component {
217217
]);
218218
};
219219

220-
private timelineProps = {
220+
private timelineProps: Partial<TimelineProps> = {
221221
format24h: true,
222222
onBackgroundLongPress: this.createNewEvent,
223223
onBackgroundLongPressOut: this.approveNewEvent,
224224
// scrollToFirst: true,
225225
// start: 0,
226226
// end: 24,
227+
unavailableHours: [{start: 0, end: 6}, {start: 22, end: 24}],
227228
overlapEventsSpacing: 8,
228229
rightEdgeSpacing: 24,
229230
};

src/timeline/Packer.ts

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import XDate from 'xdate';
33
import constants from '../commons/constants';
44
import {Event, PackedEvent} from './EventBlock';
5+
import inRange from 'lodash/inRange';
56

67
type PartialPackedEvent = Event & {index: number};
78
interface PopulateOptions {
@@ -12,11 +13,27 @@ interface PopulateOptions {
1213
rightEdgeSpacing?: number;
1314
}
1415

16+
export interface UnavailableHours {
17+
start: number;
18+
end: number;
19+
}
20+
21+
interface UnavailableHoursOptions {
22+
hourBlockHeight?: number;
23+
dayStart: number;
24+
dayEnd: number;
25+
}
26+
1527
export const HOUR_BLOCK_HEIGHT = 100;
1628
const OVERLAP_EVENTS_SPACINGS = 10;
1729
const RIGHT_EDGE_SPACING = 10;
1830

19-
function buildEvent(event: Event & {index: number}, left: number, width: number, {dayStart = 0, hourBlockHeight = HOUR_BLOCK_HEIGHT}: PopulateOptions): PackedEvent {
31+
function buildEvent(
32+
event: Event & {index: number},
33+
left: number,
34+
width: number,
35+
{dayStart = 0, hourBlockHeight = HOUR_BLOCK_HEIGHT}: PopulateOptions
36+
): PackedEvent {
2037
const startTime = new XDate(event.start);
2138
const endTime = event.end ? new XDate(event.end) : new XDate(startTime).addHours(1);
2239

@@ -57,15 +74,19 @@ function packOverlappingEventGroup(
5774
calculatedEvents: PackedEvent[],
5875
populateOptions: PopulateOptions
5976
) {
60-
const {screenWidth = constants.screenWidth, rightEdgeSpacing = RIGHT_EDGE_SPACING, overlapEventsSpacing = OVERLAP_EVENTS_SPACINGS} = populateOptions;
77+
const {
78+
screenWidth = constants.screenWidth,
79+
rightEdgeSpacing = RIGHT_EDGE_SPACING,
80+
overlapEventsSpacing = OVERLAP_EVENTS_SPACINGS
81+
} = populateOptions;
6182
columns.forEach((column, columnIndex) => {
6283
column.forEach(event => {
6384
const totalWidth = screenWidth - rightEdgeSpacing;
6485
const columnSpan = calcColumnSpan(event, columnIndex, columns);
6586
const eventLeft = (columnIndex / columns.length) * totalWidth;
6687
let eventWidth = totalWidth * (columnSpan / columns.length);
6788

68-
if (columnIndex + columnSpan <= columns.length -1) {
89+
if (columnIndex + columnSpan <= columns.length - 1) {
6990
eventWidth -= overlapEventsSpacing;
7091
}
7192

@@ -74,7 +95,7 @@ function packOverlappingEventGroup(
7495
});
7596
}
7697

77-
function populateEvents(_events: Event[], populateOptions: PopulateOptions) {
98+
export function populateEvents(_events: Event[], populateOptions: PopulateOptions) {
7899
let lastEnd: string | null = null;
79100
let columns: PartialPackedEvent[][] = [];
80101
const calculatedEvents: PackedEvent[] = [];
@@ -125,4 +146,35 @@ function populateEvents(_events: Event[], populateOptions: PopulateOptions) {
125146
return calculatedEvents;
126147
}
127148

128-
export default populateEvents;
149+
export function buildUnavailableHoursBlocks(
150+
unavailableHours: UnavailableHours[] = [],
151+
options: UnavailableHoursOptions
152+
) {
153+
const {hourBlockHeight = HOUR_BLOCK_HEIGHT, dayStart = 0, dayEnd = 24} = options || {};
154+
const totalDayHours = dayEnd - dayStart;
155+
const totalDayHeight = (dayEnd - dayStart) * hourBlockHeight;
156+
return (
157+
unavailableHours
158+
.map(hours => {
159+
if (!inRange(hours.start, 0, 25) || !inRange(hours.end, 0, 25)) {
160+
console.error('Calendar Timeline unavailableHours is invalid. Hours should be between 0 and 24');
161+
return undefined;
162+
}
163+
164+
if (hours.start >= hours.end) {
165+
console.error('Calendar Timeline availableHours is invalid. start hour should be earlier than end hour');
166+
return undefined;
167+
}
168+
169+
const startFixed = Math.max(hours.start, dayStart);
170+
const endFixed = Math.min(hours.end, dayEnd);
171+
172+
return {
173+
top: ((startFixed - dayStart) / totalDayHours) * totalDayHeight,
174+
height: (endFixed - startFixed) * hourBlockHeight
175+
};
176+
})
177+
// Note: this filter falsy values (undefined blocks)
178+
.filter(Boolean)
179+
);
180+
}

src/timeline/Timeline.tsx

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import map from 'lodash/map';
66
import constants from '../commons/constants';
77
import {Theme} from '../types';
88
import styleConstructor, {HOURS_SIDEBAR_WIDTH} from './style';
9-
import populateEvents, {HOUR_BLOCK_HEIGHT} from './Packer';
9+
import {populateEvents, HOUR_BLOCK_HEIGHT, UnavailableHours} from './Packer';
1010
import {calcTimeOffset} from './helpers/presenter';
1111
import TimelineHours, {TimelineHoursProps} from './TimelineHours';
1212
import EventBlock, {Event, PackedEvent} from './EventBlock';
@@ -91,6 +91,14 @@ export interface TimelineProps {
9191
* Spacing to keep at the right edge (for background press)
9292
*/
9393
rightEdgeSpacing?: number;
94+
/**
95+
* Range of available hours
96+
*/
97+
unavailableHours?: UnavailableHours[];
98+
/**
99+
* Background color for unavailable hours
100+
*/
101+
unavailableHoursColor?: string;
94102
}
95103

96104
const Timeline = (props: TimelineProps) => {
@@ -113,6 +121,8 @@ const Timeline = (props: TimelineProps) => {
113121
onChangeOffset,
114122
overlapEventsSpacing,
115123
rightEdgeSpacing,
124+
unavailableHours,
125+
unavailableHoursColor,
116126
eventTapped
117127
} = props;
118128

@@ -195,6 +205,8 @@ const Timeline = (props: TimelineProps) => {
195205
date={date}
196206
format24h={format24h}
197207
styles={styles.current}
208+
unavailableHours={unavailableHours}
209+
unavailableHoursColor={unavailableHoursColor}
198210
onBackgroundLongPress={onBackgroundLongPress}
199211
onBackgroundLongPressOut={onBackgroundLongPressOut}
200212
/>

src/timeline/TimelineHours.tsx

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import React, {useCallback, useMemo, useRef} from 'react';
22
import {View, Text, TouchableWithoutFeedback, ViewStyle, TextStyle, StyleSheet} from 'react-native';
33
import range from 'lodash/range';
4-
import {HOUR_BLOCK_HEIGHT} from './Packer';
4+
import {buildUnavailableHoursBlocks, HOUR_BLOCK_HEIGHT, UnavailableHours} from './Packer';
55
import {buildTimeString, calcTimeByPosition} from './helpers/presenter';
66
import constants from '../commons/constants';
77

8-
const dimensionWidth = constants.screenWidth;
9-
108
interface NewEventTime {
119
hour: number;
1210
minutes: number;
@@ -20,16 +18,31 @@ export interface TimelineHoursProps {
2018
format24h?: boolean;
2119
onBackgroundLongPress?: (timeString: string, time: NewEventTime) => void;
2220
onBackgroundLongPressOut?: (timeString: string, time: NewEventTime) => void;
21+
unavailableHours?: UnavailableHours[];
22+
unavailableHoursColor?: string;
2323
styles: {[key: string]: ViewStyle | TextStyle};
2424
}
2525

26+
const dimensionWidth = constants.screenWidth;
27+
const EVENT_DIFF = 20;
28+
2629
const TimelineHours = (props: TimelineHoursProps) => {
27-
const {format24h, start = 0, end = 24, date, styles, onBackgroundLongPress, onBackgroundLongPressOut} = props;
30+
const {
31+
format24h,
32+
start = 0,
33+
end = 24,
34+
date,
35+
unavailableHours,
36+
unavailableHoursColor,
37+
styles,
38+
onBackgroundLongPress,
39+
onBackgroundLongPressOut
40+
} = props;
2841

2942
const lastLongPressEventTime = useRef<NewEventTime>();
3043
// const offset = this.calendarHeight / (end - start);
3144
const offset = HOUR_BLOCK_HEIGHT;
32-
const EVENT_DIFF = 20;
45+
const unavailableHoursBlocks = buildUnavailableHoursBlocks(unavailableHours, {dayStart: start, dayEnd: end});
3346

3447
const hours = useMemo(() => {
3548
return range(start, end + 1).map(i => {
@@ -77,6 +90,16 @@ const TimelineHours = (props: TimelineHoursProps) => {
7790
<TouchableWithoutFeedback onLongPress={handleBackgroundPress} onPressOut={handlePressOut}>
7891
<View style={StyleSheet.absoluteFillObject} />
7992
</TouchableWithoutFeedback>
93+
{unavailableHoursBlocks.map(block => (
94+
<View
95+
style={[
96+
styles.unavailableHoursBlock,
97+
block,
98+
unavailableHoursColor ? {backgroundColor: unavailableHoursColor} : undefined
99+
]}
100+
></View>
101+
))}
102+
80103
{hours.map(({timeText, time}, index) => {
81104
return (
82105
<React.Fragment key={time}>
@@ -98,7 +121,7 @@ const TimelineHours = (props: TimelineHoursProps) => {
98121
</React.Fragment>
99122
);
100123
})}
101-
<View style={styles.verticalLine}/>
124+
<View style={styles.verticalLine} />
102125
</>
103126
);
104127
};

src/timeline/__tests__/Packer.spec.js

Lines changed: 74 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import _ from 'lodash';
2-
import uut from '../Packer';
2+
import * as uut from '../Packer';
33

44
describe('Timeline Packer utils', () => {
55
const events = [
@@ -23,29 +23,34 @@ describe('Timeline Packer utils', () => {
2323
}
2424
];
2525
it('should sort events by start and end times', () => {
26-
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
26+
const packedEvents = uut.populateEvents(events, {screenWidth: 300, dayStart: 0});
2727
expect(packedEvents[0].id).toBe('event_1');
2828
expect(packedEvents[1].id).toBe('event_3');
2929
expect(packedEvents[2].id).toBe('event_2');
3030
});
3131

3232
describe('should set events block size', () => {
3333
it('should set event block height based on their duration', () => {
34-
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
34+
const packedEvents = uut.populateEvents(events, {screenWidth: 300, dayStart: 0});
3535
expect(packedEvents[0].height).toBe(100); // event 1
3636
expect(packedEvents[1].height).toBeCloseTo(108.333); // event 3
3737
expect(packedEvents[2].height).toBe(50); // event 2
3838
});
3939

4040
it('should set event block width based on overlaps of 3 events', () => {
41-
const packedEvents = uut(events, {screenWidth: 310, dayStart: 0, rightEdgeSpacing: 10, overlapEventsSpacing: 10});
41+
const packedEvents = uut.populateEvents(events, {
42+
screenWidth: 310,
43+
dayStart: 0,
44+
rightEdgeSpacing: 10,
45+
overlapEventsSpacing: 10
46+
});
4247
expect(packedEvents[0].width).toBe(90); // event 1
4348
expect(packedEvents[1].width).toBe(90); // event 3
4449
expect(packedEvents[2].width).toBe(100); // event 2
4550
});
4651

4752
it('should set event block width based on overlaps of 2 events', () => {
48-
const packedEvents = uut([events[0], events[1]], {
53+
const packedEvents = uut.populateEvents([events[0], events[1]], {
4954
screenWidth: 300,
5055
dayStart: 0,
5156
overlapEventsSpacing: 10,
@@ -56,7 +61,7 @@ describe('Timeline Packer utils', () => {
5661
});
5762

5863
it('should set event block width when there is not overlaps', () => {
59-
const packedEvents = uut([events[0]], {screenWidth: 300, dayStart: 0});
64+
const packedEvents = uut.populateEvents([events[0]], {screenWidth: 300, dayStart: 0});
6065
expect(packedEvents[0].width).toBe(290); // event 1
6166
});
6267

@@ -87,7 +92,11 @@ describe('Timeline Packer utils', () => {
8792
end: `2017-09-06 05:30:00`
8893
}
8994
];
90-
let packedEvents = uut(overlappingEvents, {screenWidth: 310, dayStart: 0, overlapEventsSpacing: 4});
95+
let packedEvents = uut.populateEvents(overlappingEvents, {
96+
screenWidth: 310,
97+
dayStart: 0,
98+
overlapEventsSpacing: 4
99+
});
91100
packedEvents = _.sortBy(packedEvents, 'index');
92101
expect(packedEvents[0].width).toBe(96);
93102
expect(packedEvents[1].width).toBe(96);
@@ -100,21 +109,26 @@ describe('Timeline Packer utils', () => {
100109

101110
describe('should set events block position', () => {
102111
it('should set top position base on the event start time', () => {
103-
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0});
112+
const packedEvents = uut.populateEvents(events, {screenWidth: 300, dayStart: 0});
104113
expect(packedEvents[0].top).toBe(150); // event 1
105114
expect(packedEvents[1].top).toBeCloseTo(191.666); // event 3
106115
expect(packedEvents[2].top).toBe(225); // event 2
107116
});
108117

109118
it('should set left position base when the 3 events overlap', () => {
110-
const packedEvents = uut(events, {screenWidth: 300, dayStart: 0, overlapEventsSpacing: 10, rightEdgeSpacing: 10});
119+
const packedEvents = uut.populateEvents(events, {
120+
screenWidth: 300,
121+
dayStart: 0,
122+
overlapEventsSpacing: 10,
123+
rightEdgeSpacing: 10
124+
});
111125
expect(packedEvents[0].left).toBe(0); // event 1
112126
expect(packedEvents[1].left).toBeCloseTo(96.666); // event 3
113127
expect(packedEvents[2].left).toBeCloseTo(193.333); // event 2
114128
});
115129

116130
it('should set left position base when the 2 events overlap', () => {
117-
const packedEvents = uut([events[0], events[1]], {
131+
const packedEvents = uut.populateEvents([events[0], events[1]], {
118132
screenWidth: 300,
119133
dayStart: 0,
120134
overlapEventsSpacing: 10,
@@ -124,4 +138,54 @@ describe('Timeline Packer utils', () => {
124138
expect(packedEvents[1].left).toBe(145); // event 3
125139
});
126140
});
141+
142+
describe('buildUnavailableHoursBlocks', () => {
143+
it('should build unavailable blocks with default options', () => {
144+
const blocks = uut.buildUnavailableHoursBlocks([
145+
{start: 0, end: 9},
146+
{start: 19, end: 24}
147+
]);
148+
expect(blocks[0]).toEqual({
149+
top: 0,
150+
height: 900
151+
});
152+
expect(blocks[1]).toEqual({
153+
top: 1900,
154+
height: 500
155+
});
156+
});
157+
158+
it('should not return blocks for invalid hours', () => {
159+
const blocks = uut.buildUnavailableHoursBlocks([
160+
{start: -2, end: 7},
161+
{start: 3, end: 7},
162+
{start: 22, end: 25}
163+
]);
164+
165+
expect(blocks.length).toBe(1);
166+
expect(blocks[0]).toEqual({
167+
top: 300,
168+
height: 400
169+
});
170+
});
171+
172+
it('should handle different start/end day hours', () => {
173+
const blocks = uut.buildUnavailableHoursBlocks(
174+
[
175+
{start: 0, end: 9},
176+
{start: 19, end: 24}
177+
],
178+
{dayStart: 8, dayEnd: 20}
179+
);
180+
181+
expect(blocks[0]).toEqual({
182+
top: 0,
183+
height: 100
184+
});
185+
expect(blocks[1]).toEqual({
186+
top: 1100,
187+
height: 100
188+
});
189+
});
190+
});
127191
});

0 commit comments

Comments
 (0)