react-native-bigger-calendar 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -2
- package/dist/index.d.mts +289 -73
- package/dist/index.d.ts +289 -73
- package/dist/index.js +650 -144
- package/dist/index.mjs +649 -145
- package/package.json +1 -1
- package/src/components/Agenda.tsx +125 -0
- package/src/components/AllDayLane.tsx +83 -0
- package/src/components/Calendar.tsx +221 -11
- package/src/components/DefaultEvent.tsx +24 -5
- package/src/components/MonthPager.tsx +138 -26
- package/src/components/MonthView.tsx +82 -14
- package/src/components/TimeGrid.tsx +344 -56
- package/src/index.tsx +9 -2
- package/src/types.ts +23 -1
- package/src/utils/dates.ts +67 -2
- package/src/utils/layout.ts +18 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-native-bigger-calendar",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "A generic, themeable month/week/day calendar for React Native with pinch-to-zoom, virtualized paging, and a render-prop event API.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react-native",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { LegendList, type LegendListRenderItemProps } from '@legendapp/list/react-native';
|
|
2
|
+
import { format, isSameDay, type Locale, startOfDay } from 'date-fns';
|
|
3
|
+
import { type ComponentType, useCallback, useMemo } from 'react';
|
|
4
|
+
import { StyleSheet, Text, View } from 'react-native';
|
|
5
|
+
import { useCalendarTheme } from '../theme';
|
|
6
|
+
import type { CalendarEvent, EventKeyExtractor, RenderEvent } from '../types';
|
|
7
|
+
import { getIsToday } from '../utils/dates';
|
|
8
|
+
|
|
9
|
+
export type AgendaProps<T> = {
|
|
10
|
+
events: CalendarEvent<T>[];
|
|
11
|
+
locale?: Locale;
|
|
12
|
+
renderEvent: RenderEvent<T>;
|
|
13
|
+
keyExtractor: EventKeyExtractor<T>;
|
|
14
|
+
onPressEvent: (event: CalendarEvent<T>) => void;
|
|
15
|
+
onLongPressEvent?: (event: CalendarEvent<T>) => void;
|
|
16
|
+
onPressDay?: (date: Date) => void;
|
|
17
|
+
/** Highlight this date's header instead of the real "today". */
|
|
18
|
+
activeDate?: Date;
|
|
19
|
+
/** Drawn between rows of the agenda list. */
|
|
20
|
+
itemSeparatorComponent?: ComponentType<unknown> | null;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type Row<T> =
|
|
24
|
+
| { kind: 'header'; date: Date; key: string }
|
|
25
|
+
| { kind: 'event'; event: CalendarEvent<T>; index: number; key: string };
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A vertical, day-grouped list of events (no time grid). Events are sorted by
|
|
29
|
+
* start, grouped under a date header per day. The consumer controls which
|
|
30
|
+
* events (and therefore which date range) are shown.
|
|
31
|
+
*/
|
|
32
|
+
export function Agenda<T>({
|
|
33
|
+
events,
|
|
34
|
+
locale,
|
|
35
|
+
renderEvent,
|
|
36
|
+
keyExtractor,
|
|
37
|
+
onPressEvent,
|
|
38
|
+
onLongPressEvent,
|
|
39
|
+
onPressDay,
|
|
40
|
+
activeDate,
|
|
41
|
+
itemSeparatorComponent,
|
|
42
|
+
}: AgendaProps<T>) {
|
|
43
|
+
const theme = useCalendarTheme();
|
|
44
|
+
const RenderEventComponent = renderEvent;
|
|
45
|
+
|
|
46
|
+
const rows = useMemo<Row<T>[]>(() => {
|
|
47
|
+
const sorted = [...events].sort((a, b) => a.start.getTime() - b.start.getTime());
|
|
48
|
+
const out: Row<T>[] = [];
|
|
49
|
+
let currentDay: Date | null = null;
|
|
50
|
+
sorted.forEach((event, index) => {
|
|
51
|
+
if (!currentDay || !isSameDay(event.start, currentDay)) {
|
|
52
|
+
currentDay = startOfDay(event.start);
|
|
53
|
+
out.push({ kind: 'header', date: currentDay, key: `h-${currentDay.toISOString()}` });
|
|
54
|
+
}
|
|
55
|
+
out.push({ kind: 'event', event, index, key: `e-${keyExtractor(event, index)}` });
|
|
56
|
+
});
|
|
57
|
+
return out;
|
|
58
|
+
}, [events, keyExtractor]);
|
|
59
|
+
|
|
60
|
+
const keyExtractorRow = useCallback((row: Row<T>) => row.key, []);
|
|
61
|
+
const renderItem = useCallback(
|
|
62
|
+
({ item }: LegendListRenderItemProps<Row<T>>) => {
|
|
63
|
+
if (item.kind === 'header') {
|
|
64
|
+
const isHighlighted = activeDate
|
|
65
|
+
? isSameDay(item.date, activeDate)
|
|
66
|
+
: getIsToday(item.date);
|
|
67
|
+
return (
|
|
68
|
+
<Text
|
|
69
|
+
style={[
|
|
70
|
+
theme.text.weekday,
|
|
71
|
+
styles.header,
|
|
72
|
+
{ color: isHighlighted ? theme.colors.todayBackground : theme.colors.textMuted },
|
|
73
|
+
]}
|
|
74
|
+
onPress={onPressDay ? () => onPressDay(item.date) : undefined}
|
|
75
|
+
accessibilityRole={onPressDay ? 'button' : 'header'}
|
|
76
|
+
>
|
|
77
|
+
{format(item.date, 'EEEE, d LLLL', { locale })}
|
|
78
|
+
</Text>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return (
|
|
82
|
+
<View style={styles.eventRow}>
|
|
83
|
+
<RenderEventComponent
|
|
84
|
+
event={item.event}
|
|
85
|
+
mode="schedule"
|
|
86
|
+
onPress={() => onPressEvent(item.event)}
|
|
87
|
+
onLongPress={onLongPressEvent ? () => onLongPressEvent(item.event) : undefined}
|
|
88
|
+
/>
|
|
89
|
+
</View>
|
|
90
|
+
);
|
|
91
|
+
},
|
|
92
|
+
[theme, locale, activeDate, onPressDay, onPressEvent, onLongPressEvent, RenderEventComponent],
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<LegendList
|
|
97
|
+
style={styles.list}
|
|
98
|
+
data={rows}
|
|
99
|
+
keyExtractor={keyExtractorRow}
|
|
100
|
+
renderItem={renderItem}
|
|
101
|
+
// The public prop is data-agnostic; LegendList types the separator by row.
|
|
102
|
+
ItemSeparatorComponent={
|
|
103
|
+
(itemSeparatorComponent ?? undefined) as
|
|
104
|
+
| ComponentType<{ leadingItem: Row<T> }>
|
|
105
|
+
| undefined
|
|
106
|
+
}
|
|
107
|
+
recycleItems={false}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const styles = StyleSheet.create({
|
|
113
|
+
list: {
|
|
114
|
+
flex: 1,
|
|
115
|
+
},
|
|
116
|
+
header: {
|
|
117
|
+
paddingTop: 12,
|
|
118
|
+
paddingBottom: 4,
|
|
119
|
+
paddingHorizontal: 12,
|
|
120
|
+
},
|
|
121
|
+
eventRow: {
|
|
122
|
+
paddingHorizontal: 12,
|
|
123
|
+
paddingVertical: 2,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { addDays, startOfDay } from 'date-fns';
|
|
2
|
+
import { StyleSheet, View } from 'react-native';
|
|
3
|
+
import { useCalendarTheme } from '../theme';
|
|
4
|
+
import type { CalendarEvent, CalendarMode, EventKeyExtractor, RenderEvent } from '../types';
|
|
5
|
+
import { isAllDayEvent } from '../utils/layout';
|
|
6
|
+
|
|
7
|
+
type AllDayLaneProps<T> = {
|
|
8
|
+
days: Date[];
|
|
9
|
+
events: CalendarEvent<T>[];
|
|
10
|
+
mode: CalendarMode;
|
|
11
|
+
hourColumnWidth: number;
|
|
12
|
+
dayWidth: number;
|
|
13
|
+
renderEvent: RenderEvent<T>;
|
|
14
|
+
keyExtractor: EventKeyExtractor<T>;
|
|
15
|
+
onPressEvent: (event: CalendarEvent<T>) => void;
|
|
16
|
+
onLongPressEvent?: (event: CalendarEvent<T>) => void;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The all-day lane that sits above the scrolling time grid. All-day events are
|
|
21
|
+
* excluded from the timed columns (see `layoutDayEvents`) and shown here,
|
|
22
|
+
* stacked under their day(s). Renders nothing when no day has an all-day event,
|
|
23
|
+
* so timed-only calendars are unaffected.
|
|
24
|
+
*/
|
|
25
|
+
export function AllDayLane<T>({
|
|
26
|
+
days,
|
|
27
|
+
events,
|
|
28
|
+
mode,
|
|
29
|
+
hourColumnWidth,
|
|
30
|
+
dayWidth,
|
|
31
|
+
renderEvent,
|
|
32
|
+
keyExtractor,
|
|
33
|
+
onPressEvent,
|
|
34
|
+
onLongPressEvent,
|
|
35
|
+
}: AllDayLaneProps<T>) {
|
|
36
|
+
const theme = useCalendarTheme();
|
|
37
|
+
const RenderEventComponent = renderEvent;
|
|
38
|
+
|
|
39
|
+
const allDay = events.filter(isAllDayEvent);
|
|
40
|
+
const perDay = days.map((day) => {
|
|
41
|
+
const start = startOfDay(day);
|
|
42
|
+
const next = addDays(start, 1);
|
|
43
|
+
return allDay.filter((event) => event.start < next && event.end > start);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (perDay.every((list) => list.length === 0)) return null;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<View style={[styles.lane, { borderBottomColor: theme.colors.gridLine }]}>
|
|
50
|
+
<View style={{ width: hourColumnWidth }} />
|
|
51
|
+
{days.map((day, dayIndex) => (
|
|
52
|
+
<View key={day.toISOString()} style={[styles.column, { width: dayWidth }]}>
|
|
53
|
+
{perDay[dayIndex].map((event, index) => (
|
|
54
|
+
<View key={keyExtractor(event, index)} style={styles.chip}>
|
|
55
|
+
<RenderEventComponent
|
|
56
|
+
event={event}
|
|
57
|
+
mode={mode}
|
|
58
|
+
isAllDay
|
|
59
|
+
onPress={() => onPressEvent(event)}
|
|
60
|
+
onLongPress={onLongPressEvent ? () => onLongPressEvent(event) : undefined}
|
|
61
|
+
/>
|
|
62
|
+
</View>
|
|
63
|
+
))}
|
|
64
|
+
</View>
|
|
65
|
+
))}
|
|
66
|
+
</View>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const styles = StyleSheet.create({
|
|
71
|
+
lane: {
|
|
72
|
+
flexDirection: 'row',
|
|
73
|
+
borderBottomWidth: StyleSheet.hairlineWidth,
|
|
74
|
+
paddingBottom: 2,
|
|
75
|
+
},
|
|
76
|
+
column: {
|
|
77
|
+
paddingHorizontal: 1,
|
|
78
|
+
gap: 2,
|
|
79
|
+
},
|
|
80
|
+
chip: {
|
|
81
|
+
minHeight: 18,
|
|
82
|
+
},
|
|
83
|
+
});
|
|
@@ -1,4 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
endOfDay,
|
|
3
|
+
endOfMonth,
|
|
4
|
+
endOfWeek,
|
|
5
|
+
type Locale,
|
|
6
|
+
startOfDay,
|
|
7
|
+
startOfMonth,
|
|
8
|
+
startOfWeek,
|
|
9
|
+
} from 'date-fns';
|
|
10
|
+
import { useCallback, useMemo } from 'react';
|
|
11
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
2
12
|
import { useSharedValue } from 'react-native-reanimated';
|
|
3
13
|
import { CalendarThemeProvider, mergeTheme, type PartialCalendarTheme } from '../theme';
|
|
4
14
|
import type {
|
|
@@ -6,30 +16,65 @@ import type {
|
|
|
6
16
|
CalendarMode,
|
|
7
17
|
EventKeyExtractor,
|
|
8
18
|
RenderEvent,
|
|
19
|
+
RenderEventArgs,
|
|
9
20
|
WeekStartsOn,
|
|
10
21
|
} from '../types';
|
|
22
|
+
import { getViewDays } from '../utils/dates';
|
|
23
|
+
import { Agenda } from './Agenda';
|
|
11
24
|
import { DefaultEvent } from './DefaultEvent';
|
|
12
25
|
import { MonthPager } from './MonthPager';
|
|
13
|
-
import { DEFAULT_HOUR_HEIGHT, TimeGrid } from './TimeGrid';
|
|
26
|
+
import { DEFAULT_HOUR_HEIGHT, type HourRenderer, TimeGrid } from './TimeGrid';
|
|
14
27
|
|
|
15
28
|
export type CalendarProps<T> = {
|
|
16
29
|
events: CalendarEvent<T>[];
|
|
17
30
|
mode: CalendarMode;
|
|
18
31
|
date: Date;
|
|
19
32
|
onChangeDate: (date: Date) => void;
|
|
33
|
+
/** Fired alongside `onChangeDate` with the `[start, end]` of the newly-visible range. */
|
|
34
|
+
onChangeDateRange?: (range: [Date, Date]) => void;
|
|
20
35
|
onPressEvent: (event: CalendarEvent<T>) => void;
|
|
36
|
+
/** Long-press an event (month/week/day). */
|
|
37
|
+
onLongPressEvent?: (event: CalendarEvent<T>) => void;
|
|
21
38
|
/** Tap a day cell (month mode) — e.g. drill into the day view. */
|
|
22
39
|
onPressDay?: (date: Date) => void;
|
|
40
|
+
/** Long-press a day cell (month mode). */
|
|
41
|
+
onLongPressDay?: (date: Date) => void;
|
|
23
42
|
/** Tap the "+N more" overflow label in a month cell. */
|
|
24
43
|
onPressMore?: (events: CalendarEvent<T>[], date: Date) => void;
|
|
25
44
|
/** Tap empty space on the week/day grid; receives the date+time pressed. */
|
|
26
45
|
onPressCell?: (date: Date) => void;
|
|
46
|
+
/** After an empty-cell press, snap the pager back to the active page. Default false. */
|
|
47
|
+
resetPageOnPressCell?: boolean;
|
|
48
|
+
/** Long-press empty space on the week/day grid; receives the date+time. */
|
|
49
|
+
onLongPressCell?: (date: Date) => void;
|
|
50
|
+
/** Tap a day's column header on the week/day grid (default header only). */
|
|
51
|
+
onPressDateHeader?: (date: Date) => void;
|
|
27
52
|
/** Max events shown per month cell before they collapse into "+N more". */
|
|
28
53
|
maxVisibleEventCount?: number;
|
|
54
|
+
/** Sort each month day's events by start. Default true. */
|
|
55
|
+
sortedMonthView?: boolean;
|
|
56
|
+
/** Month overflow label template; `{moreCount}` is replaced. Default "{moreCount} More". */
|
|
57
|
+
moreLabel?: string;
|
|
58
|
+
/** Show dimmed adjacent-month days in the month grid. Default true. */
|
|
59
|
+
showAdjacentMonths?: boolean;
|
|
60
|
+
/** Ignore taps on month-cell events (day taps still fire). Default false. */
|
|
61
|
+
disableMonthEventCellPress?: boolean;
|
|
29
62
|
/** First day of the week. Sunday = 0 (default) … Saturday = 6. */
|
|
30
63
|
weekStartsOn?: WeekStartsOn;
|
|
64
|
+
/** Number of day columns when `mode="custom"`. Ignored by other modes. Default 1. */
|
|
65
|
+
numberOfDays?: number;
|
|
66
|
+
/**
|
|
67
|
+
* Last weekday of a `custom` partial-week view (0–6). When set, `custom` shows
|
|
68
|
+
* `weekStartsOn`…`weekEndsOn` of the visible week and pages by week, taking
|
|
69
|
+
* precedence over `numberOfDays`. Ignored by other modes.
|
|
70
|
+
*/
|
|
71
|
+
weekEndsOn?: WeekStartsOn;
|
|
31
72
|
/** Replace the built-in event box. Return a `flex: 1` element. */
|
|
32
73
|
renderEvent?: RenderEvent<T>;
|
|
74
|
+
/** Per-event style merged onto the built-in event box (static or a function of the event). */
|
|
75
|
+
eventCellStyle?: StyleProp<ViewStyle> | ((event: CalendarEvent<T>) => StyleProp<ViewStyle>);
|
|
76
|
+
/** Per-date style for month cells and week/day columns (e.g. shade specific dates). */
|
|
77
|
+
calendarCellStyle?: (date: Date) => StyleProp<ViewStyle>;
|
|
33
78
|
/** Stable key per event. Defaults to start-time + index. */
|
|
34
79
|
keyExtractor?: EventKeyExtractor<T>;
|
|
35
80
|
/** Partial theme merged over the defaults. */
|
|
@@ -41,18 +86,49 @@ export type CalendarProps<T> = {
|
|
|
41
86
|
minHourHeight?: number;
|
|
42
87
|
maxHourHeight?: number;
|
|
43
88
|
hourColumnWidth?: number;
|
|
89
|
+
/** Hide the left hour-axis column on the week/day grid. Default false. */
|
|
90
|
+
hideHours?: boolean;
|
|
91
|
+
/** Sub-hour divider lines per hour on the week/day grid (e.g. 2 = half-hours). Default 1. */
|
|
92
|
+
timeslots?: number;
|
|
93
|
+
/** Show the ISO week number in the week/day header gutter. Default false. */
|
|
94
|
+
showWeekNumber?: boolean;
|
|
95
|
+
/** Prefix for the week-number label (e.g. "W"). Default "W". */
|
|
96
|
+
weekNumberPrefix?: string;
|
|
97
|
+
/** Replace the hour-axis label on the week/day grid. Receives the hour (0–23) and `ampm`. */
|
|
98
|
+
hourComponent?: HourRenderer;
|
|
99
|
+
/** Always render six week rows in month view, for a fixed-height grid. Default false. */
|
|
100
|
+
showSixWeeks?: boolean;
|
|
101
|
+
/** Allow swiping between pages (all modes). Default true. */
|
|
102
|
+
swipeEnabled?: boolean;
|
|
103
|
+
/** Show the vertical scroll indicator on the week/day grid. Default true. */
|
|
104
|
+
showVerticalScrollIndicator?: boolean;
|
|
105
|
+
/** Allow vertical scrolling of the week/day grid. Default true. */
|
|
106
|
+
verticalScrollEnabled?: boolean;
|
|
107
|
+
/** Element rendered between the day header and the week/day grid. */
|
|
108
|
+
headerComponent?: React.ReactNode;
|
|
44
109
|
/** First hour shown on the week/day grid (0–23). Default 0. */
|
|
45
110
|
minHour?: number;
|
|
46
111
|
/** Last hour shown on the week/day grid, exclusive (1–24). Default 24. */
|
|
47
112
|
maxHour?: number;
|
|
48
|
-
/** Show hour labels in 12-hour AM/PM form. Default false (24h). */
|
|
113
|
+
/** Show hour labels (and built-in event times) in 12-hour AM/PM form. Default false (24h). */
|
|
49
114
|
ampm?: boolean;
|
|
115
|
+
/** Show the time range in the built-in event renderer (day/week/schedule). Default true. */
|
|
116
|
+
showTime?: boolean;
|
|
50
117
|
/** Initial vertical scroll, in minutes from midnight (week/day). */
|
|
51
118
|
scrollOffsetMinutes?: number;
|
|
52
119
|
/** Show the current-time line on the week/day grid. Default true. */
|
|
53
120
|
showNowIndicator?: boolean;
|
|
54
|
-
/**
|
|
55
|
-
locale?:
|
|
121
|
+
/** A date-fns `Locale` for weekday/date labels. Defaults to English. */
|
|
122
|
+
locale?: Locale;
|
|
123
|
+
/** Highlight this date (header/cell/agenda) instead of the real "today". */
|
|
124
|
+
activeDate?: Date;
|
|
125
|
+
/**
|
|
126
|
+
* Lay the day columns out right-to-left (month, week/day grid and all-day lane).
|
|
127
|
+
* Cosmetic only: the hour gutter stays on the left and paging still advances
|
|
128
|
+
* with the system scroll direction. Default false. For full RTL (including
|
|
129
|
+
* scroll direction), also enable React Native's `I18nManager`.
|
|
130
|
+
*/
|
|
131
|
+
isRTL?: boolean;
|
|
56
132
|
/**
|
|
57
133
|
* Allow a fling to carry across several pages before snapping. Default false:
|
|
58
134
|
* one day/week/month per swipe.
|
|
@@ -60,6 +136,10 @@ export type CalendarProps<T> = {
|
|
|
60
136
|
freeSwipe?: boolean;
|
|
61
137
|
/** Custom header above the week/day grid. Receives the visible days. */
|
|
62
138
|
renderTimeGridHeader?: (days: Date[]) => React.ReactNode;
|
|
139
|
+
/** Replace the weekday-label header above the month grid. Return `null` to hide it. */
|
|
140
|
+
renderHeaderForMonthView?: (weekDays: Date[]) => React.ReactNode;
|
|
141
|
+
/** Drawn between rows of the `schedule` (agenda) list. */
|
|
142
|
+
itemSeparatorComponent?: React.ComponentType<unknown> | null;
|
|
63
143
|
};
|
|
64
144
|
|
|
65
145
|
// Derive a key purely from event data so identity is stable across reorders and
|
|
@@ -68,18 +148,51 @@ export type CalendarProps<T> = {
|
|
|
68
148
|
const defaultKeyExtractor: EventKeyExtractor<unknown> = (event) =>
|
|
69
149
|
`${event.start.toISOString()}|${event.end.toISOString()}|${event.title ?? ''}`;
|
|
70
150
|
|
|
151
|
+
// The [start, end] of the dates a given mode shows around `date`. Month spans the
|
|
152
|
+
// padded grid (whole weeks); time-grid modes span their day columns.
|
|
153
|
+
function visibleRange(
|
|
154
|
+
mode: CalendarMode,
|
|
155
|
+
date: Date,
|
|
156
|
+
weekStartsOn: WeekStartsOn,
|
|
157
|
+
numberOfDays: number,
|
|
158
|
+
weekEndsOn?: WeekStartsOn,
|
|
159
|
+
): [Date, Date] {
|
|
160
|
+
if (mode === 'month') {
|
|
161
|
+
return [
|
|
162
|
+
startOfWeek(startOfMonth(date), { weekStartsOn }),
|
|
163
|
+
endOfWeek(endOfMonth(date), { weekStartsOn }),
|
|
164
|
+
];
|
|
165
|
+
}
|
|
166
|
+
const days = getViewDays(mode, date, weekStartsOn, numberOfDays, false, weekEndsOn);
|
|
167
|
+
return [startOfDay(days[0]), endOfDay(days[days.length - 1])];
|
|
168
|
+
}
|
|
169
|
+
|
|
71
170
|
export function Calendar<T>({
|
|
72
171
|
events,
|
|
73
172
|
mode,
|
|
74
173
|
date,
|
|
75
174
|
onChangeDate,
|
|
175
|
+
onChangeDateRange,
|
|
76
176
|
onPressEvent,
|
|
177
|
+
onLongPressEvent,
|
|
77
178
|
onPressDay,
|
|
179
|
+
onLongPressDay,
|
|
78
180
|
onPressMore,
|
|
79
181
|
onPressCell,
|
|
182
|
+
resetPageOnPressCell,
|
|
183
|
+
onLongPressCell,
|
|
184
|
+
onPressDateHeader,
|
|
80
185
|
maxVisibleEventCount = 2,
|
|
186
|
+
sortedMonthView,
|
|
187
|
+
moreLabel,
|
|
188
|
+
showAdjacentMonths,
|
|
189
|
+
disableMonthEventCellPress,
|
|
81
190
|
weekStartsOn = 0,
|
|
191
|
+
numberOfDays,
|
|
192
|
+
weekEndsOn,
|
|
82
193
|
renderEvent = DefaultEvent,
|
|
194
|
+
eventCellStyle,
|
|
195
|
+
calendarCellStyle,
|
|
83
196
|
keyExtractor = defaultKeyExtractor as EventKeyExtractor<T>,
|
|
84
197
|
theme,
|
|
85
198
|
cellHeight: cellHeightProp,
|
|
@@ -87,19 +200,73 @@ export function Calendar<T>({
|
|
|
87
200
|
minHourHeight,
|
|
88
201
|
maxHourHeight,
|
|
89
202
|
hourColumnWidth,
|
|
203
|
+
hideHours,
|
|
204
|
+
timeslots,
|
|
205
|
+
showWeekNumber,
|
|
206
|
+
weekNumberPrefix,
|
|
207
|
+
hourComponent,
|
|
208
|
+
showSixWeeks,
|
|
209
|
+
swipeEnabled,
|
|
210
|
+
showVerticalScrollIndicator,
|
|
211
|
+
verticalScrollEnabled,
|
|
212
|
+
headerComponent,
|
|
90
213
|
minHour,
|
|
91
214
|
maxHour,
|
|
92
215
|
ampm,
|
|
216
|
+
showTime,
|
|
93
217
|
scrollOffsetMinutes,
|
|
94
218
|
showNowIndicator,
|
|
95
219
|
locale,
|
|
220
|
+
activeDate,
|
|
221
|
+
isRTL,
|
|
96
222
|
freeSwipe,
|
|
97
223
|
renderTimeGridHeader,
|
|
224
|
+
renderHeaderForMonthView,
|
|
225
|
+
itemSeparatorComponent,
|
|
98
226
|
}: CalendarProps<T>) {
|
|
99
227
|
const mergedTheme = useMemo(() => mergeTheme(theme), [theme]);
|
|
100
228
|
const internalCellHeight = useSharedValue(hourHeight);
|
|
101
229
|
const cellHeight = cellHeightProp ?? internalCellHeight;
|
|
102
230
|
|
|
231
|
+
// Swallow presses on disabled events once, so every view inherits the guard.
|
|
232
|
+
const handlePressEvent = useCallback(
|
|
233
|
+
(event: CalendarEvent<T>) => {
|
|
234
|
+
if (!event.disabled) onPressEvent(event);
|
|
235
|
+
},
|
|
236
|
+
[onPressEvent],
|
|
237
|
+
);
|
|
238
|
+
const handleLongPressEvent = useMemo(
|
|
239
|
+
() =>
|
|
240
|
+
onLongPressEvent
|
|
241
|
+
? (event: CalendarEvent<T>) => {
|
|
242
|
+
if (!event.disabled) onLongPressEvent(event);
|
|
243
|
+
}
|
|
244
|
+
: undefined,
|
|
245
|
+
[onLongPressEvent],
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
// Echo every date change and, when asked, the full visible range derived from it.
|
|
249
|
+
const handleChangeDate = useCallback(
|
|
250
|
+
(next: Date) => {
|
|
251
|
+
onChangeDate(next);
|
|
252
|
+
onChangeDateRange?.(visibleRange(mode, next, weekStartsOn, numberOfDays ?? 1, weekEndsOn));
|
|
253
|
+
},
|
|
254
|
+
[onChangeDate, onChangeDateRange, mode, weekStartsOn, numberOfDays, weekEndsOn],
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
// Inject `eventCellStyle`, `ampm` and `showTime` into the renderer once, so
|
|
258
|
+
// every view gets them for free without threading the props through each
|
|
259
|
+
// component. Skip the wrapper entirely when none are set.
|
|
260
|
+
const resolvedRenderEvent = useMemo<RenderEvent<T>>(() => {
|
|
261
|
+
if (eventCellStyle == null && ampm == null && showTime == null) return renderEvent;
|
|
262
|
+
const Base = renderEvent;
|
|
263
|
+
return function StyledEvent(props: RenderEventArgs<T>) {
|
|
264
|
+
const cellStyle =
|
|
265
|
+
typeof eventCellStyle === 'function' ? eventCellStyle(props.event) : eventCellStyle;
|
|
266
|
+
return <Base {...props} cellStyle={cellStyle} ampm={ampm} showTime={showTime} />;
|
|
267
|
+
};
|
|
268
|
+
}, [renderEvent, eventCellStyle, ampm, showTime]);
|
|
269
|
+
|
|
103
270
|
return (
|
|
104
271
|
<CalendarThemeProvider value={mergedTheme}>
|
|
105
272
|
{mode === 'month' ? (
|
|
@@ -108,26 +275,62 @@ export function Calendar<T>({
|
|
|
108
275
|
events={events}
|
|
109
276
|
maxVisibleEventCount={maxVisibleEventCount}
|
|
110
277
|
weekStartsOn={weekStartsOn}
|
|
111
|
-
|
|
278
|
+
locale={locale}
|
|
279
|
+
sortedMonthView={sortedMonthView}
|
|
280
|
+
moreLabel={moreLabel}
|
|
281
|
+
showAdjacentMonths={showAdjacentMonths}
|
|
282
|
+
disableMonthEventCellPress={disableMonthEventCellPress}
|
|
283
|
+
isRTL={isRTL}
|
|
284
|
+
showSixWeeks={showSixWeeks}
|
|
285
|
+
activeDate={activeDate}
|
|
286
|
+
renderHeaderForMonthView={renderHeaderForMonthView}
|
|
287
|
+
calendarCellStyle={calendarCellStyle}
|
|
288
|
+
renderEvent={resolvedRenderEvent}
|
|
112
289
|
keyExtractor={keyExtractor}
|
|
113
290
|
onPressDay={onPressDay}
|
|
114
|
-
|
|
291
|
+
onLongPressDay={onLongPressDay}
|
|
292
|
+
onPressEvent={handlePressEvent}
|
|
293
|
+
onLongPressEvent={handleLongPressEvent}
|
|
115
294
|
onPressMore={onPressMore}
|
|
116
|
-
onChangeDate={
|
|
295
|
+
onChangeDate={handleChangeDate}
|
|
117
296
|
freeSwipe={freeSwipe}
|
|
297
|
+
swipeEnabled={swipeEnabled}
|
|
298
|
+
/>
|
|
299
|
+
) : mode === 'schedule' ? (
|
|
300
|
+
<Agenda
|
|
301
|
+
events={events}
|
|
302
|
+
locale={locale}
|
|
303
|
+
renderEvent={resolvedRenderEvent}
|
|
304
|
+
keyExtractor={keyExtractor}
|
|
305
|
+
onPressEvent={handlePressEvent}
|
|
306
|
+
onLongPressEvent={handleLongPressEvent}
|
|
307
|
+
onPressDay={onPressDay}
|
|
308
|
+
activeDate={activeDate}
|
|
309
|
+
itemSeparatorComponent={itemSeparatorComponent}
|
|
118
310
|
/>
|
|
119
311
|
) : (
|
|
120
312
|
<TimeGrid
|
|
121
313
|
mode={mode}
|
|
314
|
+
numberOfDays={numberOfDays}
|
|
315
|
+
weekEndsOn={weekEndsOn}
|
|
122
316
|
date={date}
|
|
123
317
|
events={events}
|
|
124
318
|
cellHeight={cellHeight}
|
|
125
319
|
hourHeight={hourHeight}
|
|
126
320
|
weekStartsOn={weekStartsOn}
|
|
127
|
-
renderEvent={
|
|
321
|
+
renderEvent={resolvedRenderEvent}
|
|
128
322
|
keyExtractor={keyExtractor}
|
|
129
323
|
scrollOffsetMinutes={scrollOffsetMinutes}
|
|
130
324
|
hourColumnWidth={hourColumnWidth}
|
|
325
|
+
hideHours={hideHours}
|
|
326
|
+
timeslots={timeslots}
|
|
327
|
+
calendarCellStyle={calendarCellStyle}
|
|
328
|
+
showWeekNumber={showWeekNumber}
|
|
329
|
+
weekNumberPrefix={weekNumberPrefix}
|
|
330
|
+
hourComponent={hourComponent}
|
|
331
|
+
showVerticalScrollIndicator={showVerticalScrollIndicator}
|
|
332
|
+
verticalScrollEnabled={verticalScrollEnabled}
|
|
333
|
+
headerComponent={headerComponent}
|
|
131
334
|
minHour={minHour}
|
|
132
335
|
maxHour={maxHour}
|
|
133
336
|
ampm={ampm}
|
|
@@ -135,10 +338,17 @@ export function Calendar<T>({
|
|
|
135
338
|
maxHourHeight={maxHourHeight}
|
|
136
339
|
showNowIndicator={showNowIndicator}
|
|
137
340
|
locale={locale}
|
|
341
|
+
activeDate={activeDate}
|
|
342
|
+
isRTL={isRTL}
|
|
138
343
|
freeSwipe={freeSwipe}
|
|
139
|
-
|
|
344
|
+
swipeEnabled={swipeEnabled}
|
|
345
|
+
onPressEvent={handlePressEvent}
|
|
346
|
+
onLongPressEvent={handleLongPressEvent}
|
|
140
347
|
onPressCell={onPressCell}
|
|
141
|
-
|
|
348
|
+
resetPageOnPressCell={resetPageOnPressCell}
|
|
349
|
+
onLongPressCell={onLongPressCell}
|
|
350
|
+
onPressDateHeader={onPressDateHeader}
|
|
351
|
+
onChangeDate={handleChangeDate}
|
|
142
352
|
renderHeader={renderTimeGridHeader}
|
|
143
353
|
/>
|
|
144
354
|
)}
|
|
@@ -8,14 +8,30 @@ import type { RenderEventArgs } from '../types';
|
|
|
8
8
|
* and (on the day/week grid) its time range. Pass your own `renderEvent` to
|
|
9
9
|
* `<Calendar>` to replace it entirely.
|
|
10
10
|
*/
|
|
11
|
-
export function DefaultEvent<T>({
|
|
11
|
+
export function DefaultEvent<T>({
|
|
12
|
+
event,
|
|
13
|
+
mode,
|
|
14
|
+
isAllDay,
|
|
15
|
+
ampm = false,
|
|
16
|
+
showTime = true,
|
|
17
|
+
cellStyle,
|
|
18
|
+
onPress,
|
|
19
|
+
onLongPress,
|
|
20
|
+
}: RenderEventArgs<T>) {
|
|
12
21
|
const theme = useCalendarTheme();
|
|
13
|
-
const
|
|
22
|
+
const timeFormat = ampm ? 'h:mm a' : 'HH:mm';
|
|
23
|
+
const shouldShowTime = mode !== 'month' && !isAllDay && showTime;
|
|
14
24
|
|
|
15
25
|
return (
|
|
16
26
|
<TouchableOpacity
|
|
17
|
-
style={[
|
|
27
|
+
style={[
|
|
28
|
+
styles.box,
|
|
29
|
+
{ backgroundColor: theme.colors.eventBackground },
|
|
30
|
+
event.disabled && styles.disabled,
|
|
31
|
+
cellStyle,
|
|
32
|
+
]}
|
|
18
33
|
onPress={onPress}
|
|
34
|
+
onLongPress={onLongPress}
|
|
19
35
|
activeOpacity={0.7}
|
|
20
36
|
accessibilityRole="button"
|
|
21
37
|
accessibilityLabel={event.title}
|
|
@@ -30,13 +46,13 @@ export function DefaultEvent<T>({ event, mode, onPress }: RenderEventArgs<T>) {
|
|
|
30
46
|
{event.title}
|
|
31
47
|
</Text>
|
|
32
48
|
) : null}
|
|
33
|
-
{
|
|
49
|
+
{shouldShowTime ? (
|
|
34
50
|
<Text
|
|
35
51
|
style={[styles.time, { color: theme.colors.eventText }]}
|
|
36
52
|
numberOfLines={1}
|
|
37
53
|
allowFontScaling={false}
|
|
38
54
|
>
|
|
39
|
-
{`${format(event.start,
|
|
55
|
+
{`${format(event.start, timeFormat)} - ${format(event.end, timeFormat)}`}
|
|
40
56
|
</Text>
|
|
41
57
|
) : null}
|
|
42
58
|
</TouchableOpacity>
|
|
@@ -54,4 +70,7 @@ const styles = StyleSheet.create({
|
|
|
54
70
|
time: {
|
|
55
71
|
fontSize: 11,
|
|
56
72
|
},
|
|
73
|
+
disabled: {
|
|
74
|
+
opacity: 0.5,
|
|
75
|
+
},
|
|
57
76
|
});
|