@super-calendar/native 2.0.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/LICENSE +21 -0
- package/dist/DefaultMonthEvent-86XCNCge.d.ts +326 -0
- package/dist/DefaultMonthEvent-CpRoOloh.d.mts +326 -0
- package/dist/MonthList-BCfN-UGz.js +1089 -0
- package/dist/MonthList-CyrEJ_U6.mjs +1030 -0
- package/dist/index.d.mts +386 -0
- package/dist/index.d.ts +386 -0
- package/dist/index.js +1700 -0
- package/dist/index.mjs +1543 -0
- package/dist/picker.d.mts +3 -0
- package/dist/picker.d.ts +3 -0
- package/dist/picker.js +96 -0
- package/dist/picker.mjs +3 -0
- package/package.json +92 -0
- package/src/components/Agenda.tsx +133 -0
- package/src/components/AllDayLane.tsx +98 -0
- package/src/components/Calendar.tsx +456 -0
- package/src/components/DefaultEvent.tsx +223 -0
- package/src/components/DefaultMonthEvent.tsx +105 -0
- package/src/components/MonthList.tsx +570 -0
- package/src/components/MonthPager.tsx +377 -0
- package/src/components/MonthView.tsx +518 -0
- package/src/components/TimeGrid.tsx +1943 -0
- package/src/index.tsx +70 -0
- package/src/picker.tsx +55 -0
- package/src/theme.ts +86 -0
- package/src/types.ts +62 -0
- package/src/utils/useWebGridZoom.ts +59 -0
- package/src/utils/useWebPagerKeys.ts +56 -0
|
@@ -0,0 +1,456 @@
|
|
|
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";
|
|
12
|
+
import { useSharedValue } from "react-native-reanimated";
|
|
13
|
+
import { CalendarThemeProvider, mergeTheme, type PartialCalendarTheme } from "../theme";
|
|
14
|
+
import type {
|
|
15
|
+
BusinessHours,
|
|
16
|
+
CalendarEvent,
|
|
17
|
+
CalendarMode,
|
|
18
|
+
EventKeyExtractor,
|
|
19
|
+
RenderEvent,
|
|
20
|
+
RenderEventArgs,
|
|
21
|
+
WeekStartsOn,
|
|
22
|
+
} from "../types";
|
|
23
|
+
import { getViewDays } from "@super-calendar/core";
|
|
24
|
+
import { Agenda } from "./Agenda";
|
|
25
|
+
import { DefaultEvent } from "./DefaultEvent";
|
|
26
|
+
import { MonthPager } from "./MonthPager";
|
|
27
|
+
import {
|
|
28
|
+
DEFAULT_HOUR_HEIGHT,
|
|
29
|
+
type EventDragHandler,
|
|
30
|
+
type EventDragStartHandler,
|
|
31
|
+
type HourRenderer,
|
|
32
|
+
TimeGrid,
|
|
33
|
+
} from "./TimeGrid";
|
|
34
|
+
|
|
35
|
+
export type CalendarProps<T> = {
|
|
36
|
+
events: CalendarEvent<T>[];
|
|
37
|
+
mode: CalendarMode;
|
|
38
|
+
date: Date;
|
|
39
|
+
onChangeDate: (date: Date) => void;
|
|
40
|
+
/** Fired alongside `onChangeDate` with the `[start, end]` of the newly-visible range. */
|
|
41
|
+
onChangeDateRange?: (range: [Date, Date]) => void;
|
|
42
|
+
onPressEvent: (event: CalendarEvent<T>) => void;
|
|
43
|
+
/** Long-press an event (month/week/day). */
|
|
44
|
+
onLongPressEvent?: (event: CalendarEvent<T>) => void;
|
|
45
|
+
/**
|
|
46
|
+
* Enable drag-to-move and drag-to-resize on the week/day grid. Called with the
|
|
47
|
+
* dragged event and its new start/end (snapped to `dragStepMinutes`); update
|
|
48
|
+
* your own event state in response. Long-press an event to move it (drag
|
|
49
|
+
* horizontally to change the day too); drag its bottom grip to resize. Return
|
|
50
|
+
* `false` to reject the drop (e.g. an overlap) and snap the event back.
|
|
51
|
+
*/
|
|
52
|
+
onDragEvent?: EventDragHandler<T>;
|
|
53
|
+
/**
|
|
54
|
+
* Fired when a move or resize gesture begins on the week/day grid, before any
|
|
55
|
+
* change is committed: on grab for a move (after the long-press), and when a
|
|
56
|
+
* resize drag starts. Use it to trigger haptic feedback, e.g.
|
|
57
|
+
* `Haptics.impactAsync()` from `expo-haptics`. Native-only and inert unless
|
|
58
|
+
* `onDragEvent` is also set.
|
|
59
|
+
*/
|
|
60
|
+
onDragStart?: EventDragStartHandler<T>;
|
|
61
|
+
/** Minutes a drag-to-move/resize snaps to. Default 15. */
|
|
62
|
+
dragStepMinutes?: number;
|
|
63
|
+
/**
|
|
64
|
+
* Show the resize grip on draggable events. Default true. Set false to keep
|
|
65
|
+
* drag-to-move and drag-to-resize working while hiding the visible indicator.
|
|
66
|
+
*/
|
|
67
|
+
showDragHandle?: boolean;
|
|
68
|
+
/** Tap a day cell (month mode) — e.g. drill into the day view. */
|
|
69
|
+
onPressDay?: (date: Date) => void;
|
|
70
|
+
/** Long-press a day cell (month mode). */
|
|
71
|
+
onLongPressDay?: (date: Date) => void;
|
|
72
|
+
/** Tap the "+N more" overflow label in a month cell. */
|
|
73
|
+
onPressMore?: (events: CalendarEvent<T>[], date: Date) => void;
|
|
74
|
+
/** Tap empty space on the week/day grid; receives the date+time pressed. */
|
|
75
|
+
onPressCell?: (date: Date) => void;
|
|
76
|
+
/** After an empty-cell press, snap the pager back to the active page. Default false. */
|
|
77
|
+
resetPageOnPressCell?: boolean;
|
|
78
|
+
/** Long-press empty space on the week/day grid; receives the date+time. */
|
|
79
|
+
onLongPressCell?: (date: Date) => void;
|
|
80
|
+
/**
|
|
81
|
+
* Enable drag-to-create on the week/day grid: long-press empty space and drag
|
|
82
|
+
* to sweep out a new event's time range. Called on release with the snapped
|
|
83
|
+
* `start`/`end` (to `dragStepMinutes`); create your own event in response. A
|
|
84
|
+
* stationary press yields a one-step range. Native-only; supersedes
|
|
85
|
+
* `onLongPressCell` on empty space when set.
|
|
86
|
+
*/
|
|
87
|
+
onCreateEvent?: (start: Date, end: Date) => void;
|
|
88
|
+
/** Tap a day's column header on the week/day grid (default header only). */
|
|
89
|
+
onPressDateHeader?: (date: Date) => void;
|
|
90
|
+
/**
|
|
91
|
+
* Max event chips per month cell before they collapse into "+N more". Omit to
|
|
92
|
+
* auto-fit as many as the cell height allows (default); set a number for a
|
|
93
|
+
* fixed cap. Pass an explicit value when using a custom `renderEvent`.
|
|
94
|
+
*/
|
|
95
|
+
maxVisibleEventCount?: number;
|
|
96
|
+
/** Sort each month day's events by start. Default true. */
|
|
97
|
+
sortedMonthView?: boolean;
|
|
98
|
+
/** Month overflow label template; `{moreCount}` is replaced. Default "{moreCount} More". */
|
|
99
|
+
moreLabel?: string;
|
|
100
|
+
/** Show dimmed adjacent-month days in the month grid. Default true. */
|
|
101
|
+
showAdjacentMonths?: boolean;
|
|
102
|
+
/** Ignore taps on month-cell events (day taps still fire). Default false. */
|
|
103
|
+
disableMonthEventCellPress?: boolean;
|
|
104
|
+
/** First day of the week. Sunday = 0 (default) … Saturday = 6. */
|
|
105
|
+
weekStartsOn?: WeekStartsOn;
|
|
106
|
+
/** Number of day columns when `mode="custom"`. Ignored by other modes. Default 1. */
|
|
107
|
+
numberOfDays?: number;
|
|
108
|
+
/**
|
|
109
|
+
* Last weekday of a `custom` partial-week view (0–6). When set, `custom` shows
|
|
110
|
+
* `weekStartsOn`…`weekEndsOn` of the visible week and pages by week, taking
|
|
111
|
+
* precedence over `numberOfDays`. Ignored by other modes.
|
|
112
|
+
*/
|
|
113
|
+
weekEndsOn?: WeekStartsOn;
|
|
114
|
+
/** Replace the built-in event box. Return a `flex: 1` element. */
|
|
115
|
+
renderEvent?: RenderEvent<T>;
|
|
116
|
+
/** Per-event style merged onto the built-in event box (static or a function of the event). */
|
|
117
|
+
eventCellStyle?: StyleProp<ViewStyle> | ((event: CalendarEvent<T>) => StyleProp<ViewStyle>);
|
|
118
|
+
/** Per-date style for month cells and week/day columns (e.g. shade specific dates). */
|
|
119
|
+
calendarCellStyle?: (date: Date) => StyleProp<ViewStyle>;
|
|
120
|
+
/**
|
|
121
|
+
* Week/day grid only: open hours per day for business-hours shading. Hours
|
|
122
|
+
* outside the returned `{ start, end }` are tinted; return `null` to shade the
|
|
123
|
+
* whole day (closed). Omit for no shading.
|
|
124
|
+
*/
|
|
125
|
+
businessHours?: BusinessHours;
|
|
126
|
+
/** Stable key per event. Defaults to start-time + index. */
|
|
127
|
+
keyExtractor?: EventKeyExtractor<T>;
|
|
128
|
+
/** Partial theme merged over the defaults. */
|
|
129
|
+
theme?: PartialCalendarTheme;
|
|
130
|
+
/** Externally-owned per-hour row height (week/day). Created internally if omitted. */
|
|
131
|
+
cellHeight?: ReturnType<typeof useSharedValue<number>>;
|
|
132
|
+
/** Initial per-hour row height in px (week/day). Default 48. */
|
|
133
|
+
hourHeight?: number;
|
|
134
|
+
minHourHeight?: number;
|
|
135
|
+
maxHourHeight?: number;
|
|
136
|
+
hourColumnWidth?: number;
|
|
137
|
+
/** Hide the left hour-axis column on the week/day grid. Default false. */
|
|
138
|
+
hideHours?: boolean;
|
|
139
|
+
/** Sub-hour divider lines per hour on the week/day grid (e.g. 2 = half-hours). Default 1. */
|
|
140
|
+
timeslots?: number;
|
|
141
|
+
/**
|
|
142
|
+
* Show the all-day lane above the week/day grid. Default true. When false the
|
|
143
|
+
* lane is never rendered, so all-day events (see `ICalendarEvent.allDay` and
|
|
144
|
+
* whole-day spans) are not shown on the time grid.
|
|
145
|
+
*/
|
|
146
|
+
showAllDayEventCell?: boolean;
|
|
147
|
+
/** Show the ISO week number in the week/day header gutter. Default false. */
|
|
148
|
+
showWeekNumber?: boolean;
|
|
149
|
+
/** Prefix for the week-number label (e.g. "W"). Default "W". */
|
|
150
|
+
weekNumberPrefix?: string;
|
|
151
|
+
/** Replace the hour-axis label on the week/day grid. Receives the hour (0–23) and `ampm`. */
|
|
152
|
+
hourComponent?: HourRenderer;
|
|
153
|
+
/** Always render six week rows in month view, for a fixed-height grid. Default false. */
|
|
154
|
+
showSixWeeks?: boolean;
|
|
155
|
+
/** Allow swiping between pages (all modes). Default true. */
|
|
156
|
+
swipeEnabled?: boolean;
|
|
157
|
+
/** Show the vertical scroll indicator on the week/day grid. Default true. */
|
|
158
|
+
showVerticalScrollIndicator?: boolean;
|
|
159
|
+
/** Allow vertical scrolling of the week/day grid. Default true. */
|
|
160
|
+
verticalScrollEnabled?: boolean;
|
|
161
|
+
/** Element rendered between the day header and the week/day grid. */
|
|
162
|
+
headerComponent?: React.ReactNode;
|
|
163
|
+
/** First hour shown on the week/day grid (0–23). Default 0. */
|
|
164
|
+
minHour?: number;
|
|
165
|
+
/** Last hour shown on the week/day grid, exclusive (1–24). Default 24. */
|
|
166
|
+
maxHour?: number;
|
|
167
|
+
/** Show hour labels (and built-in event times) in 12-hour AM/PM form. Default false (24h). */
|
|
168
|
+
ampm?: boolean;
|
|
169
|
+
/** Show the time range in the built-in event renderer (day/week/schedule). Default true. */
|
|
170
|
+
showTime?: boolean;
|
|
171
|
+
/** Add a trailing ellipsis (…) when an event title overflows in the built-in renderer; otherwise the text is clipped. Default false. */
|
|
172
|
+
ellipsizeTitle?: boolean;
|
|
173
|
+
/** Label the built-in renderer shows for an all-day event in the schedule (and its screen-reader text). Default "All day". */
|
|
174
|
+
allDayLabel?: string;
|
|
175
|
+
/** Initial vertical scroll, in minutes from midnight (week/day). */
|
|
176
|
+
scrollOffsetMinutes?: number;
|
|
177
|
+
/** Show the current-time line on the week/day grid. Default true. */
|
|
178
|
+
showNowIndicator?: boolean;
|
|
179
|
+
/** A date-fns `Locale` for weekday/date labels. Defaults to English. */
|
|
180
|
+
locale?: Locale;
|
|
181
|
+
/** Highlight this date (header/cell/agenda) instead of the real "today". */
|
|
182
|
+
activeDate?: Date;
|
|
183
|
+
/**
|
|
184
|
+
* Lay the day columns out right-to-left (month, week/day grid and all-day lane).
|
|
185
|
+
* Cosmetic only: the hour gutter stays on the left and paging still advances
|
|
186
|
+
* with the system scroll direction. Default false. For full RTL (including
|
|
187
|
+
* scroll direction), also enable React Native's `I18nManager`.
|
|
188
|
+
*/
|
|
189
|
+
isRTL?: boolean;
|
|
190
|
+
/**
|
|
191
|
+
* Allow a fling to carry across several pages before snapping. Default false:
|
|
192
|
+
* one day/week/month per swipe.
|
|
193
|
+
*/
|
|
194
|
+
freeSwipe?: boolean;
|
|
195
|
+
/** Custom header above the week/day grid. Receives the visible days. */
|
|
196
|
+
renderTimeGridHeader?: (days: Date[]) => React.ReactNode;
|
|
197
|
+
/** Replace the weekday-label header above the month grid. Return `null` to hide it. */
|
|
198
|
+
renderHeaderForMonthView?: (weekDays: Date[]) => React.ReactNode;
|
|
199
|
+
/**
|
|
200
|
+
* Month mode only: replace the default date badge in each day cell. Receives
|
|
201
|
+
* the day; return your own date label (e.g. a number with a dot or badge).
|
|
202
|
+
* Event chips and the "+N more" label still render below it.
|
|
203
|
+
*/
|
|
204
|
+
renderCustomDateForMonth?: (date: Date) => React.ReactNode;
|
|
205
|
+
/** Drawn between rows of the `schedule` (agenda) list. */
|
|
206
|
+
itemSeparatorComponent?: React.ComponentType<unknown> | null;
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
// Derive a key purely from event data so identity is stable across reorders and
|
|
210
|
+
// list mutations. Supply your own `keyExtractor` returning a real id when events
|
|
211
|
+
// can share an identical start/end/title.
|
|
212
|
+
const defaultKeyExtractor: EventKeyExtractor<unknown> = (event) =>
|
|
213
|
+
`${event.start.toISOString()}|${event.end.toISOString()}|${event.title ?? ""}`;
|
|
214
|
+
|
|
215
|
+
// The [start, end] of the dates a given mode shows around `date`. Month spans the
|
|
216
|
+
// padded grid (whole weeks); time-grid modes span their day columns.
|
|
217
|
+
function visibleRange(
|
|
218
|
+
mode: CalendarMode,
|
|
219
|
+
date: Date,
|
|
220
|
+
weekStartsOn: WeekStartsOn,
|
|
221
|
+
numberOfDays: number,
|
|
222
|
+
weekEndsOn?: WeekStartsOn,
|
|
223
|
+
): [Date, Date] {
|
|
224
|
+
if (mode === "month") {
|
|
225
|
+
return [
|
|
226
|
+
startOfWeek(startOfMonth(date), { weekStartsOn }),
|
|
227
|
+
endOfWeek(endOfMonth(date), { weekStartsOn }),
|
|
228
|
+
];
|
|
229
|
+
}
|
|
230
|
+
const days = getViewDays(mode, date, weekStartsOn, numberOfDays, false, weekEndsOn);
|
|
231
|
+
return [startOfDay(days[0]), endOfDay(days[days.length - 1])];
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export function Calendar<T>({
|
|
235
|
+
events,
|
|
236
|
+
mode,
|
|
237
|
+
date,
|
|
238
|
+
onChangeDate,
|
|
239
|
+
onChangeDateRange,
|
|
240
|
+
onPressEvent,
|
|
241
|
+
onLongPressEvent,
|
|
242
|
+
onDragEvent,
|
|
243
|
+
onDragStart,
|
|
244
|
+
dragStepMinutes,
|
|
245
|
+
showDragHandle,
|
|
246
|
+
onPressDay,
|
|
247
|
+
onLongPressDay,
|
|
248
|
+
onPressMore,
|
|
249
|
+
onPressCell,
|
|
250
|
+
resetPageOnPressCell,
|
|
251
|
+
onLongPressCell,
|
|
252
|
+
onCreateEvent,
|
|
253
|
+
onPressDateHeader,
|
|
254
|
+
maxVisibleEventCount,
|
|
255
|
+
sortedMonthView,
|
|
256
|
+
moreLabel,
|
|
257
|
+
showAdjacentMonths,
|
|
258
|
+
disableMonthEventCellPress,
|
|
259
|
+
weekStartsOn = 0,
|
|
260
|
+
numberOfDays,
|
|
261
|
+
weekEndsOn,
|
|
262
|
+
renderEvent = DefaultEvent,
|
|
263
|
+
eventCellStyle,
|
|
264
|
+
calendarCellStyle,
|
|
265
|
+
businessHours,
|
|
266
|
+
keyExtractor = defaultKeyExtractor as EventKeyExtractor<T>,
|
|
267
|
+
theme,
|
|
268
|
+
cellHeight: cellHeightProp,
|
|
269
|
+
hourHeight = DEFAULT_HOUR_HEIGHT,
|
|
270
|
+
minHourHeight,
|
|
271
|
+
maxHourHeight,
|
|
272
|
+
hourColumnWidth,
|
|
273
|
+
hideHours,
|
|
274
|
+
timeslots,
|
|
275
|
+
showAllDayEventCell,
|
|
276
|
+
showWeekNumber,
|
|
277
|
+
weekNumberPrefix,
|
|
278
|
+
hourComponent,
|
|
279
|
+
showSixWeeks,
|
|
280
|
+
swipeEnabled,
|
|
281
|
+
showVerticalScrollIndicator,
|
|
282
|
+
verticalScrollEnabled,
|
|
283
|
+
headerComponent,
|
|
284
|
+
minHour,
|
|
285
|
+
maxHour,
|
|
286
|
+
ampm,
|
|
287
|
+
showTime,
|
|
288
|
+
ellipsizeTitle,
|
|
289
|
+
allDayLabel,
|
|
290
|
+
scrollOffsetMinutes,
|
|
291
|
+
showNowIndicator,
|
|
292
|
+
locale,
|
|
293
|
+
activeDate,
|
|
294
|
+
isRTL,
|
|
295
|
+
freeSwipe,
|
|
296
|
+
renderTimeGridHeader,
|
|
297
|
+
renderHeaderForMonthView,
|
|
298
|
+
renderCustomDateForMonth,
|
|
299
|
+
itemSeparatorComponent,
|
|
300
|
+
}: CalendarProps<T>) {
|
|
301
|
+
const mergedTheme = useMemo(() => mergeTheme(theme), [theme]);
|
|
302
|
+
const internalCellHeight = useSharedValue(hourHeight);
|
|
303
|
+
const cellHeight = cellHeightProp ?? internalCellHeight;
|
|
304
|
+
|
|
305
|
+
// Swallow presses on disabled events once, so every view inherits the guard.
|
|
306
|
+
const handlePressEvent = useCallback(
|
|
307
|
+
(event: CalendarEvent<T>) => {
|
|
308
|
+
if (!event.disabled) onPressEvent(event);
|
|
309
|
+
},
|
|
310
|
+
[onPressEvent],
|
|
311
|
+
);
|
|
312
|
+
const handleLongPressEvent = useMemo(
|
|
313
|
+
() =>
|
|
314
|
+
onLongPressEvent
|
|
315
|
+
? (event: CalendarEvent<T>) => {
|
|
316
|
+
if (!event.disabled) onLongPressEvent(event);
|
|
317
|
+
}
|
|
318
|
+
: undefined,
|
|
319
|
+
[onLongPressEvent],
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
// Echo every date change and, when asked, the full visible range derived from it.
|
|
323
|
+
const handleChangeDate = useCallback(
|
|
324
|
+
(next: Date) => {
|
|
325
|
+
onChangeDate(next);
|
|
326
|
+
onChangeDateRange?.(visibleRange(mode, next, weekStartsOn, numberOfDays ?? 1, weekEndsOn));
|
|
327
|
+
},
|
|
328
|
+
[onChangeDate, onChangeDateRange, mode, weekStartsOn, numberOfDays, weekEndsOn],
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Inject `eventCellStyle`, `ampm`, `showTime`, `ellipsizeTitle` and
|
|
332
|
+
// `allDayLabel` into the renderer once, so every view gets them for free
|
|
333
|
+
// without threading the props through each component. Skip the wrapper
|
|
334
|
+
// entirely when none are set.
|
|
335
|
+
const resolvedRenderEvent = useMemo<RenderEvent<T>>(() => {
|
|
336
|
+
if (
|
|
337
|
+
eventCellStyle == null &&
|
|
338
|
+
ampm == null &&
|
|
339
|
+
showTime == null &&
|
|
340
|
+
ellipsizeTitle == null &&
|
|
341
|
+
allDayLabel == null
|
|
342
|
+
)
|
|
343
|
+
return renderEvent;
|
|
344
|
+
const Base = renderEvent;
|
|
345
|
+
return function StyledEvent(props: RenderEventArgs<T>) {
|
|
346
|
+
const cellStyle =
|
|
347
|
+
typeof eventCellStyle === "function" ? eventCellStyle(props.event) : eventCellStyle;
|
|
348
|
+
return (
|
|
349
|
+
<Base
|
|
350
|
+
{...props}
|
|
351
|
+
cellStyle={cellStyle}
|
|
352
|
+
ampm={ampm}
|
|
353
|
+
showTime={showTime}
|
|
354
|
+
ellipsizeTitle={ellipsizeTitle}
|
|
355
|
+
allDayLabel={allDayLabel}
|
|
356
|
+
/>
|
|
357
|
+
);
|
|
358
|
+
};
|
|
359
|
+
}, [renderEvent, eventCellStyle, ampm, showTime, ellipsizeTitle, allDayLabel]);
|
|
360
|
+
|
|
361
|
+
return (
|
|
362
|
+
<CalendarThemeProvider value={mergedTheme}>
|
|
363
|
+
{mode === "month" ? (
|
|
364
|
+
<MonthPager
|
|
365
|
+
date={date}
|
|
366
|
+
events={events}
|
|
367
|
+
maxVisibleEventCount={maxVisibleEventCount}
|
|
368
|
+
weekStartsOn={weekStartsOn}
|
|
369
|
+
locale={locale}
|
|
370
|
+
sortedMonthView={sortedMonthView}
|
|
371
|
+
moreLabel={moreLabel}
|
|
372
|
+
showAdjacentMonths={showAdjacentMonths}
|
|
373
|
+
disableMonthEventCellPress={disableMonthEventCellPress}
|
|
374
|
+
isRTL={isRTL}
|
|
375
|
+
showSixWeeks={showSixWeeks}
|
|
376
|
+
activeDate={activeDate}
|
|
377
|
+
renderHeaderForMonthView={renderHeaderForMonthView}
|
|
378
|
+
renderCustomDateForMonth={renderCustomDateForMonth}
|
|
379
|
+
calendarCellStyle={calendarCellStyle}
|
|
380
|
+
renderEvent={resolvedRenderEvent}
|
|
381
|
+
keyExtractor={keyExtractor}
|
|
382
|
+
onPressDay={onPressDay}
|
|
383
|
+
onLongPressDay={onLongPressDay}
|
|
384
|
+
onPressEvent={handlePressEvent}
|
|
385
|
+
onLongPressEvent={handleLongPressEvent}
|
|
386
|
+
onPressMore={onPressMore}
|
|
387
|
+
onChangeDate={handleChangeDate}
|
|
388
|
+
freeSwipe={freeSwipe}
|
|
389
|
+
swipeEnabled={swipeEnabled}
|
|
390
|
+
/>
|
|
391
|
+
) : mode === "schedule" ? (
|
|
392
|
+
<Agenda
|
|
393
|
+
events={events}
|
|
394
|
+
locale={locale}
|
|
395
|
+
renderEvent={resolvedRenderEvent}
|
|
396
|
+
keyExtractor={keyExtractor}
|
|
397
|
+
onPressEvent={handlePressEvent}
|
|
398
|
+
onLongPressEvent={handleLongPressEvent}
|
|
399
|
+
onPressDay={onPressDay}
|
|
400
|
+
activeDate={activeDate}
|
|
401
|
+
itemSeparatorComponent={itemSeparatorComponent}
|
|
402
|
+
/>
|
|
403
|
+
) : (
|
|
404
|
+
<TimeGrid
|
|
405
|
+
mode={mode}
|
|
406
|
+
numberOfDays={numberOfDays}
|
|
407
|
+
weekEndsOn={weekEndsOn}
|
|
408
|
+
date={date}
|
|
409
|
+
events={events}
|
|
410
|
+
cellHeight={cellHeight}
|
|
411
|
+
hourHeight={hourHeight}
|
|
412
|
+
weekStartsOn={weekStartsOn}
|
|
413
|
+
renderEvent={resolvedRenderEvent}
|
|
414
|
+
keyExtractor={keyExtractor}
|
|
415
|
+
scrollOffsetMinutes={scrollOffsetMinutes}
|
|
416
|
+
hourColumnWidth={hourColumnWidth}
|
|
417
|
+
hideHours={hideHours}
|
|
418
|
+
timeslots={timeslots}
|
|
419
|
+
showAllDayEventCell={showAllDayEventCell}
|
|
420
|
+
calendarCellStyle={calendarCellStyle}
|
|
421
|
+
businessHours={businessHours}
|
|
422
|
+
showWeekNumber={showWeekNumber}
|
|
423
|
+
weekNumberPrefix={weekNumberPrefix}
|
|
424
|
+
hourComponent={hourComponent}
|
|
425
|
+
showVerticalScrollIndicator={showVerticalScrollIndicator}
|
|
426
|
+
verticalScrollEnabled={verticalScrollEnabled}
|
|
427
|
+
headerComponent={headerComponent}
|
|
428
|
+
minHour={minHour}
|
|
429
|
+
maxHour={maxHour}
|
|
430
|
+
ampm={ampm}
|
|
431
|
+
minHourHeight={minHourHeight}
|
|
432
|
+
maxHourHeight={maxHourHeight}
|
|
433
|
+
showNowIndicator={showNowIndicator}
|
|
434
|
+
locale={locale}
|
|
435
|
+
activeDate={activeDate}
|
|
436
|
+
isRTL={isRTL}
|
|
437
|
+
freeSwipe={freeSwipe}
|
|
438
|
+
swipeEnabled={swipeEnabled}
|
|
439
|
+
onPressEvent={handlePressEvent}
|
|
440
|
+
onLongPressEvent={handleLongPressEvent}
|
|
441
|
+
onDragEvent={onDragEvent}
|
|
442
|
+
showDragHandle={showDragHandle}
|
|
443
|
+
onDragStart={onDragStart}
|
|
444
|
+
dragStepMinutes={dragStepMinutes}
|
|
445
|
+
onPressCell={onPressCell}
|
|
446
|
+
resetPageOnPressCell={resetPageOnPressCell}
|
|
447
|
+
onLongPressCell={onLongPressCell}
|
|
448
|
+
onCreateEvent={onCreateEvent}
|
|
449
|
+
onPressDateHeader={onPressDateHeader}
|
|
450
|
+
onChangeDate={handleChangeDate}
|
|
451
|
+
renderHeader={renderTimeGridHeader}
|
|
452
|
+
/>
|
|
453
|
+
)}
|
|
454
|
+
</CalendarThemeProvider>
|
|
455
|
+
);
|
|
456
|
+
}
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { StyleSheet, Text, TouchableOpacity } from "react-native";
|
|
2
|
+
import Animated, { useAnimatedStyle, useDerivedValue } from "react-native-reanimated";
|
|
3
|
+
import { useCalendarTheme } from "../theme";
|
|
4
|
+
import type { RenderEventArgs } from "../types";
|
|
5
|
+
import {
|
|
6
|
+
eventAccessibilityLabel,
|
|
7
|
+
eventChipLayout,
|
|
8
|
+
eventTimeLabel,
|
|
9
|
+
titleEllipsizeMode,
|
|
10
|
+
titleNumberOfLines,
|
|
11
|
+
} from "@super-calendar/core";
|
|
12
|
+
|
|
13
|
+
// Box vertical padding (mirrors styles.box) and the reserved height for the time
|
|
14
|
+
// line. The time wraps to at most two lines on a narrow column, so reserve two
|
|
15
|
+
// lines' worth; the title gets every whole line that fits in what remains.
|
|
16
|
+
const BOX_PADDING_V = 2;
|
|
17
|
+
const TIME_LINE_HEIGHT = 30;
|
|
18
|
+
const FALLBACK_TITLE_LINE_HEIGHT = 16;
|
|
19
|
+
|
|
20
|
+
const numericStyle = (value: number | string | undefined, fallback: number) =>
|
|
21
|
+
typeof value === "number" ? value : fallback;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The built-in event renderer: a filled, rounded box showing the event title
|
|
25
|
+
* and (on the day/week grid, when the box is tall enough) its time range. The
|
|
26
|
+
* title fills the box in whole lines, never a half-cut last line, and clips
|
|
27
|
+
* without an ellipsis unless `ellipsizeTitle` is set. The time is secondary: it
|
|
28
|
+
* only shows once a full line is free beneath the title. Pass your own
|
|
29
|
+
* `renderEvent` to `<Calendar>` to replace it entirely.
|
|
30
|
+
*/
|
|
31
|
+
export function DefaultEvent<T>({
|
|
32
|
+
event,
|
|
33
|
+
mode,
|
|
34
|
+
boxHeight,
|
|
35
|
+
isAllDay,
|
|
36
|
+
ampm = false,
|
|
37
|
+
showTime = true,
|
|
38
|
+
ellipsizeTitle = false,
|
|
39
|
+
allDayLabel,
|
|
40
|
+
cellStyle,
|
|
41
|
+
onPress,
|
|
42
|
+
onLongPress,
|
|
43
|
+
}: RenderEventArgs<T>) {
|
|
44
|
+
const theme = useCalendarTheme();
|
|
45
|
+
const isAllDayEvent = isAllDay ?? false;
|
|
46
|
+
const timeLabel = eventTimeLabel({
|
|
47
|
+
mode,
|
|
48
|
+
isAllDay: isAllDayEvent,
|
|
49
|
+
start: event.start,
|
|
50
|
+
end: event.end,
|
|
51
|
+
ampm,
|
|
52
|
+
showTime,
|
|
53
|
+
allDayLabel,
|
|
54
|
+
});
|
|
55
|
+
const ellipsizeMode = titleEllipsizeMode(ellipsizeTitle);
|
|
56
|
+
const titleLineHeight = numericStyle(
|
|
57
|
+
theme.text.eventTitle.lineHeight,
|
|
58
|
+
FALLBACK_TITLE_LINE_HEIGHT,
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
// Announce the full event to screen readers: title plus the all-day label or
|
|
62
|
+
// the time range (which is otherwise only shown visually).
|
|
63
|
+
const accessibilityLabel = eventAccessibilityLabel({
|
|
64
|
+
title: event.title,
|
|
65
|
+
isAllDay: isAllDayEvent,
|
|
66
|
+
start: event.start,
|
|
67
|
+
end: event.end,
|
|
68
|
+
ampm,
|
|
69
|
+
allDayLabel,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Month cells and the all-day lane get a single clipped line; the timed grid
|
|
73
|
+
// (and the roomy schedule rows) wrap to fill the box. `titleNumberOfLines`
|
|
74
|
+
// returns 1 for the single-line contexts and undefined for the wrapping ones.
|
|
75
|
+
const fixedTitleLines = titleNumberOfLines(mode, isAllDayEvent);
|
|
76
|
+
|
|
77
|
+
// The schedule view is a list, not a grid, so its rows are a roomier card with
|
|
78
|
+
// a larger title and time, mirroring the dom renderer's default agenda row.
|
|
79
|
+
const isSchedule = mode === "schedule";
|
|
80
|
+
|
|
81
|
+
// Whole-line title clamp + time visibility, recomputed on the UI thread as the
|
|
82
|
+
// box grows or shrinks with pinch-zoom, so the title never shows a half-cut
|
|
83
|
+
// line and the secondary time only appears once a full line is free below it.
|
|
84
|
+
const layout = useDerivedValue(
|
|
85
|
+
() =>
|
|
86
|
+
eventChipLayout({
|
|
87
|
+
boxHeightPx: boxHeight?.value,
|
|
88
|
+
mode,
|
|
89
|
+
hasTime: timeLabel != null,
|
|
90
|
+
titleLineHeightPx: titleLineHeight,
|
|
91
|
+
timeLineHeightPx: TIME_LINE_HEIGHT,
|
|
92
|
+
paddingYPx: BOX_PADDING_V,
|
|
93
|
+
}),
|
|
94
|
+
[boxHeight, mode, timeLabel, titleLineHeight],
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// Clip the wrapped title to a whole number of lines (overflow hidden on the
|
|
98
|
+
// wrapper), so the visible text is always full words on full lines.
|
|
99
|
+
const titleClipStyle = useAnimatedStyle(() => {
|
|
100
|
+
const lines = layout.value.titleMaxLines;
|
|
101
|
+
return { maxHeight: lines > 0 ? lines * titleLineHeight : undefined };
|
|
102
|
+
}, [layout, titleLineHeight]);
|
|
103
|
+
|
|
104
|
+
const timeStyle = useAnimatedStyle(() => {
|
|
105
|
+
return { display: layout.value.showTime ? "flex" : "none" };
|
|
106
|
+
}, [layout]);
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<TouchableOpacity
|
|
110
|
+
style={[
|
|
111
|
+
styles.box,
|
|
112
|
+
isSchedule && styles.scheduleBox,
|
|
113
|
+
{ backgroundColor: theme.colors.eventBackground },
|
|
114
|
+
event.disabled && styles.disabled,
|
|
115
|
+
cellStyle,
|
|
116
|
+
]}
|
|
117
|
+
onPress={onPress}
|
|
118
|
+
onLongPress={onLongPress}
|
|
119
|
+
activeOpacity={0.7}
|
|
120
|
+
accessibilityRole="button"
|
|
121
|
+
accessibilityLabel={accessibilityLabel}
|
|
122
|
+
accessibilityState={{ disabled: event.disabled ?? false }}
|
|
123
|
+
>
|
|
124
|
+
{event.title ? (
|
|
125
|
+
fixedTitleLines == null ? (
|
|
126
|
+
<Animated.View style={[styles.titleClip, titleClipStyle]}>
|
|
127
|
+
<Text
|
|
128
|
+
style={[
|
|
129
|
+
theme.text.eventTitle,
|
|
130
|
+
isSchedule && styles.scheduleTitle,
|
|
131
|
+
{ color: theme.colors.eventText },
|
|
132
|
+
]}
|
|
133
|
+
ellipsizeMode={ellipsizeMode}
|
|
134
|
+
allowFontScaling={false}
|
|
135
|
+
>
|
|
136
|
+
{event.title}
|
|
137
|
+
</Text>
|
|
138
|
+
</Animated.View>
|
|
139
|
+
) : (
|
|
140
|
+
<Text
|
|
141
|
+
style={[
|
|
142
|
+
theme.text.eventTitle,
|
|
143
|
+
styles.title,
|
|
144
|
+
isSchedule && styles.scheduleTitle,
|
|
145
|
+
{ color: theme.colors.eventText },
|
|
146
|
+
]}
|
|
147
|
+
// Month cells and the all-day lane are compact: a single clipped line.
|
|
148
|
+
numberOfLines={fixedTitleLines}
|
|
149
|
+
ellipsizeMode={ellipsizeMode}
|
|
150
|
+
allowFontScaling={false}
|
|
151
|
+
>
|
|
152
|
+
{event.title}
|
|
153
|
+
</Text>
|
|
154
|
+
)
|
|
155
|
+
) : null}
|
|
156
|
+
{timeLabel ? (
|
|
157
|
+
<Animated.View style={timeStyle}>
|
|
158
|
+
<Text
|
|
159
|
+
style={[
|
|
160
|
+
styles.time,
|
|
161
|
+
isSchedule && styles.scheduleTime,
|
|
162
|
+
{ color: theme.colors.eventText },
|
|
163
|
+
]}
|
|
164
|
+
// Wrap rather than clip horizontally: a narrow column shows the full
|
|
165
|
+
// range across two lines instead of a cut-off "11:00 - 1".
|
|
166
|
+
numberOfLines={2}
|
|
167
|
+
ellipsizeMode={ellipsizeMode}
|
|
168
|
+
allowFontScaling={false}
|
|
169
|
+
>
|
|
170
|
+
{timeLabel}
|
|
171
|
+
</Text>
|
|
172
|
+
</Animated.View>
|
|
173
|
+
) : null}
|
|
174
|
+
</TouchableOpacity>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const styles = StyleSheet.create({
|
|
179
|
+
box: {
|
|
180
|
+
// Grow to fill the grid's sized box, but fall back to the content height
|
|
181
|
+
// (flexBasis: auto) so month chips — whose wrapper has no fixed height —
|
|
182
|
+
// size to their text instead of collapsing.
|
|
183
|
+
flexGrow: 1,
|
|
184
|
+
flexShrink: 1,
|
|
185
|
+
flexBasis: "auto",
|
|
186
|
+
borderRadius: 6,
|
|
187
|
+
paddingVertical: BOX_PADDING_V,
|
|
188
|
+
paddingHorizontal: 4,
|
|
189
|
+
overflow: "hidden",
|
|
190
|
+
},
|
|
191
|
+
// The roomy schedule-row card, matching the dom default agenda row.
|
|
192
|
+
scheduleBox: {
|
|
193
|
+
borderRadius: 8,
|
|
194
|
+
paddingVertical: 6,
|
|
195
|
+
paddingHorizontal: 10,
|
|
196
|
+
},
|
|
197
|
+
scheduleTitle: {
|
|
198
|
+
fontSize: 14,
|
|
199
|
+
fontWeight: "600",
|
|
200
|
+
lineHeight: 18,
|
|
201
|
+
},
|
|
202
|
+
scheduleTime: {
|
|
203
|
+
fontSize: 13,
|
|
204
|
+
lineHeight: 18,
|
|
205
|
+
opacity: 0.75,
|
|
206
|
+
},
|
|
207
|
+
// Clips the wrapped title to the animated whole-line max-height.
|
|
208
|
+
titleClip: {
|
|
209
|
+
overflow: "hidden",
|
|
210
|
+
},
|
|
211
|
+
// The all-day single line shrinks and clips before the time line (whose default
|
|
212
|
+
// flexShrink of 0 keeps it whole), so a short lane never shows a half-cut time.
|
|
213
|
+
title: {
|
|
214
|
+
flexShrink: 1,
|
|
215
|
+
},
|
|
216
|
+
time: {
|
|
217
|
+
fontSize: 11,
|
|
218
|
+
lineHeight: 15,
|
|
219
|
+
},
|
|
220
|
+
disabled: {
|
|
221
|
+
opacity: 0.5,
|
|
222
|
+
},
|
|
223
|
+
});
|