@super-calendar/native 2.4.0 → 2.6.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/dist/index.d.mts +80 -5
- package/dist/index.d.ts +80 -5
- package/dist/index.js +475 -105
- package/dist/index.mjs +477 -108
- package/dist/picker.js +1 -1
- package/dist/picker.mjs +1 -1
- package/package.json +2 -2
- package/src/components/Calendar.tsx +32 -2
- package/src/components/ResourceTimeline.tsx +305 -0
- package/src/components/YearView.tsx +276 -0
- package/src/index.tsx +1 -0
- package/dist/{MonthList-zPLklHAY.mjs → MonthList-CMzYn9T0.mjs} +1 -1
- package/dist/{MonthList-Aczb2RSY.js → MonthList-Gu6p64fE.js} +1 -1
package/dist/picker.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
-
const require_MonthList = require("./MonthList-
|
|
2
|
+
const require_MonthList = require("./MonthList-Gu6p64fE.js");
|
|
3
3
|
let _super_calendar_core = require("@super-calendar/core");
|
|
4
4
|
exports.CalendarThemeProvider = require_MonthList.CalendarThemeProvider;
|
|
5
5
|
exports.DefaultMonthEvent = require_MonthList.DefaultMonthEvent;
|
package/dist/picker.mjs
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { d as darkTheme, f as defaultTheme, i as MonthView, m as useCalendarTheme, n as DefaultMonthEvent, p as mergeTheme, r as MonthPager, t as MonthList, u as CalendarThemeProvider } from "./MonthList-
|
|
1
|
+
import { d as darkTheme, f as defaultTheme, i as MonthView, m as useCalendarTheme, n as DefaultMonthEvent, p as mergeTheme, r as MonthPager, t as MonthList, u as CalendarThemeProvider } from "./MonthList-CMzYn9T0.mjs";
|
|
2
2
|
import { buildMonthGrid, buildMonthWeeks, daySelectionState, getIsToday, getWeekDays, isDateSelectable, isRangeEndpoint, isSameCalendarDay, isWeekend, isWithinDateRange, minutesIntoDay, nextDateRange, useDateRange, useMonthGrid } from "@super-calendar/core";
|
|
3
3
|
export { CalendarThemeProvider, DefaultMonthEvent, MonthList, MonthPager, MonthView, buildMonthGrid, buildMonthWeeks, darkTheme, daySelectionState, defaultTheme, getIsToday, getWeekDays, isDateSelectable, isRangeEndpoint, isSameCalendarDay, isWeekend, isWithinDateRange, mergeTheme, minutesIntoDay, nextDateRange, useCalendarTheme, useDateRange, useMonthGrid };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@super-calendar/native",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Gesture-driven, virtualized month / week / day calendar and date picker for React Native (and web via react-native-web).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"agenda",
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
"access": "public"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@super-calendar/core": "2.
|
|
67
|
+
"@super-calendar/core": "2.6.0"
|
|
68
68
|
},
|
|
69
69
|
"peerDependencies": {
|
|
70
70
|
"@legendapp/list": ">=3",
|
|
@@ -5,10 +5,12 @@ import {
|
|
|
5
5
|
endOfDay,
|
|
6
6
|
endOfMonth,
|
|
7
7
|
endOfWeek,
|
|
8
|
+
endOfYear,
|
|
8
9
|
type Locale,
|
|
9
10
|
startOfDay,
|
|
10
11
|
startOfMonth,
|
|
11
12
|
startOfWeek,
|
|
13
|
+
startOfYear,
|
|
12
14
|
} from "date-fns";
|
|
13
15
|
import { type ReactElement, useCallback, useMemo } from "react";
|
|
14
16
|
import type { StyleProp, ViewStyle } from "react-native";
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
getViewDays,
|
|
32
34
|
} from "@super-calendar/core";
|
|
33
35
|
import { type SlotStyleProps } from "../utils/slots";
|
|
36
|
+
import { YearView, type YearViewSlot } from "./YearView";
|
|
34
37
|
import { Agenda, type AgendaSlot } from "./Agenda";
|
|
35
38
|
import { DefaultEvent } from "./DefaultEvent";
|
|
36
39
|
import { MonthPager } from "./MonthPager";
|
|
@@ -49,7 +52,7 @@ import {
|
|
|
49
52
|
* month pager, the grid slots reach the time grid, and the agenda slots reach
|
|
50
53
|
* the schedule view; slots for views that aren't rendered are ignored.
|
|
51
54
|
*/
|
|
52
|
-
export type CalendarSlot = MonthViewSlot | TimeGridSlot | AgendaSlot;
|
|
55
|
+
export type CalendarSlot = MonthViewSlot | TimeGridSlot | AgendaSlot | YearViewSlot;
|
|
53
56
|
|
|
54
57
|
/** Props for the {@link Calendar} component. */
|
|
55
58
|
export type CalendarProps<T> = SlotStyleProps<CalendarSlot> & {
|
|
@@ -85,8 +88,10 @@ export type CalendarProps<T> = SlotStyleProps<CalendarSlot> & {
|
|
|
85
88
|
* drag-to-move and drag-to-resize working while hiding the visible indicator.
|
|
86
89
|
*/
|
|
87
90
|
showDragHandle?: boolean;
|
|
88
|
-
/** Tap a day cell (month
|
|
91
|
+
/** Tap a day cell (month and year modes) — e.g. drill into the day view. */
|
|
89
92
|
onPressDay?: (date: Date) => void;
|
|
93
|
+
/** Tap a month's title in the year view — e.g. jump to that month. */
|
|
94
|
+
onPressMonth?: (month: Date) => void;
|
|
90
95
|
/** Long-press a day cell (month mode). */
|
|
91
96
|
onLongPressDay?: (date: Date) => void;
|
|
92
97
|
/** Tap the "+N more" overflow label in a month cell. */
|
|
@@ -261,6 +266,12 @@ function visibleRange(
|
|
|
261
266
|
endOfWeek(endOfMonth(date), { weekStartsOn }),
|
|
262
267
|
];
|
|
263
268
|
}
|
|
269
|
+
if (mode === "year") {
|
|
270
|
+
return [
|
|
271
|
+
startOfWeek(startOfYear(date), { weekStartsOn }),
|
|
272
|
+
endOfWeek(endOfYear(date), { weekStartsOn }),
|
|
273
|
+
];
|
|
274
|
+
}
|
|
264
275
|
const days = getViewDays(mode, date, weekStartsOn, numberOfDays, false, weekEndsOn);
|
|
265
276
|
return [startOfDay(days[0]), endOfDay(days[days.length - 1])];
|
|
266
277
|
}
|
|
@@ -283,6 +294,12 @@ function expansionRange(
|
|
|
283
294
|
addWeeks(endOfWeek(endOfMonth(addMonths(date, 1)), { weekStartsOn }), 1),
|
|
284
295
|
];
|
|
285
296
|
}
|
|
297
|
+
if (mode === "year") {
|
|
298
|
+
return [
|
|
299
|
+
startOfWeek(startOfYear(date), { weekStartsOn }),
|
|
300
|
+
endOfWeek(endOfYear(date), { weekStartsOn }),
|
|
301
|
+
];
|
|
302
|
+
}
|
|
286
303
|
if (mode === "schedule") {
|
|
287
304
|
// The agenda lists forward from the anchor date; three months is a sensible
|
|
288
305
|
// default look-ahead. Pre-expand for a different window.
|
|
@@ -332,6 +349,7 @@ export function Calendar<T>({
|
|
|
332
349
|
dragStepMinutes,
|
|
333
350
|
showDragHandle,
|
|
334
351
|
onPressDay,
|
|
352
|
+
onPressMonth,
|
|
335
353
|
onLongPressDay,
|
|
336
354
|
onPressMore,
|
|
337
355
|
onPressCell,
|
|
@@ -523,6 +541,18 @@ export function Calendar<T>({
|
|
|
523
541
|
classNames={classNames}
|
|
524
542
|
styles={styleOverrides}
|
|
525
543
|
/>
|
|
544
|
+
) : mode === "year" ? (
|
|
545
|
+
<YearView
|
|
546
|
+
date={date}
|
|
547
|
+
events={displayEvents}
|
|
548
|
+
weekStartsOn={weekStartsOn}
|
|
549
|
+
locale={locale}
|
|
550
|
+
activeDate={activeDate}
|
|
551
|
+
onPressDay={onPressDay}
|
|
552
|
+
onPressMonth={onPressMonth}
|
|
553
|
+
classNames={classNames}
|
|
554
|
+
styles={styleOverrides}
|
|
555
|
+
/>
|
|
526
556
|
) : mode === "schedule" ? (
|
|
527
557
|
<Agenda
|
|
528
558
|
events={displayEvents}
|
|
@@ -2,6 +2,8 @@ import type { ComponentType, ReactElement } from "react";
|
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
3
3
|
import {
|
|
4
4
|
type AccessibilityActionEvent,
|
|
5
|
+
type GestureResponderEvent,
|
|
6
|
+
Platform,
|
|
5
7
|
Pressable,
|
|
6
8
|
ScrollView,
|
|
7
9
|
StyleSheet,
|
|
@@ -12,6 +14,8 @@ import { Gesture, GestureDetector } from "react-native-gesture-handler";
|
|
|
12
14
|
import Animated, { runOnJS, useAnimatedStyle, useSharedValue } from "react-native-reanimated";
|
|
13
15
|
import {
|
|
14
16
|
type CalendarEvent,
|
|
17
|
+
cellRangeFromDrag,
|
|
18
|
+
closedHourBands,
|
|
15
19
|
eventTimeLabel,
|
|
16
20
|
formatHour,
|
|
17
21
|
layoutDayEvents,
|
|
@@ -23,7 +27,11 @@ import { useCalendarTheme } from "../theme";
|
|
|
23
27
|
|
|
24
28
|
// Native long-press duration (ms) before a bar is picked up to move.
|
|
25
29
|
const MOVE_ACTIVATE_MS = 250;
|
|
30
|
+
// Web: activate the create sweep past a small drag instead of a long-press, so
|
|
31
|
+
// a plain mouse drag creates (mirrors the time grid).
|
|
32
|
+
const DRAG_ACTIVATE_PX = 8;
|
|
26
33
|
const MINUTES_PER_HOUR = 60;
|
|
34
|
+
const isWeb = Platform.OS === "web";
|
|
27
35
|
|
|
28
36
|
/** A schedulable lane (room, person, machine) events are grouped under. */
|
|
29
37
|
export interface Resource {
|
|
@@ -81,12 +89,35 @@ export interface ResourceTimelineProps<T = unknown> {
|
|
|
81
89
|
renderEvent?: ComponentType<ResourceEventArgs<T>>;
|
|
82
90
|
/** Tap an event. */
|
|
83
91
|
onPressEvent?: (event: CalendarEvent<T>) => void;
|
|
92
|
+
/**
|
|
93
|
+
* Long-press an event. When `onDragEvent` is also set, a long-press picks the
|
|
94
|
+
* bar up to move instead, so this fires only for non-draggable bars.
|
|
95
|
+
*/
|
|
96
|
+
onLongPressEvent?: (event: CalendarEvent<T>) => void;
|
|
84
97
|
/**
|
|
85
98
|
* Enables drag-to-move and edge-resize along the time axis: long-press a bar to
|
|
86
99
|
* move it, or drag its right edge to resize. Called with the proposed new
|
|
87
100
|
* start/end; return `false` to reject the drop (it snaps back).
|
|
88
101
|
*/
|
|
89
102
|
onDragEvent?: (event: CalendarEvent<T>, start: Date, end: Date) => void | boolean;
|
|
103
|
+
/** Tap empty lane space; called with the snapped time and the lane's resource. */
|
|
104
|
+
onPressCell?: (at: Date, resource: Resource) => void;
|
|
105
|
+
/**
|
|
106
|
+
* Long-press empty lane space. When `onCreateEvent` is also set, the long-press
|
|
107
|
+
* starts the create drag instead, so this fires only without it.
|
|
108
|
+
*/
|
|
109
|
+
onLongPressCell?: (at: Date, resource: Resource) => void;
|
|
110
|
+
/**
|
|
111
|
+
* Long-press empty lane space, then drag along the time axis to sweep out a new
|
|
112
|
+
* event; called with the swept start/end and the lane's resource.
|
|
113
|
+
*/
|
|
114
|
+
onCreateEvent?: (start: Date, end: Date, resource: Resource) => void;
|
|
115
|
+
/**
|
|
116
|
+
* Shade the hours outside this window, per lane. Same shape as the Calendar's
|
|
117
|
+
* `businessHours` plus the lane's resource, so per-resource opening hours work
|
|
118
|
+
* (return `null` for a fully closed lane). A date-only function is accepted.
|
|
119
|
+
*/
|
|
120
|
+
businessHours?: (date: Date, resource: Resource) => { start: number; end: number } | null;
|
|
90
121
|
/** Snap dragged events to this many minutes (default 15). */
|
|
91
122
|
dragStepMinutes?: number;
|
|
92
123
|
}
|
|
@@ -154,6 +185,7 @@ type ResourceBarProps<T> = {
|
|
|
154
185
|
snapMinutes: number;
|
|
155
186
|
Renderer: ComponentType<ResourceEventArgs<T>>;
|
|
156
187
|
onPress: () => void;
|
|
188
|
+
onLongPress?: () => void;
|
|
157
189
|
onDragEvent: (event: CalendarEvent<T>, start: Date, end: Date) => void | boolean;
|
|
158
190
|
theme: ReturnType<typeof useCalendarTheme>;
|
|
159
191
|
};
|
|
@@ -174,6 +206,7 @@ function ResourceBar<T>({
|
|
|
174
206
|
snapMinutes,
|
|
175
207
|
Renderer,
|
|
176
208
|
onPress,
|
|
209
|
+
onLongPress,
|
|
177
210
|
onDragEvent,
|
|
178
211
|
theme,
|
|
179
212
|
}: ResourceBarProps<T>): ReactElement {
|
|
@@ -311,6 +344,9 @@ function ResourceBar<T>({
|
|
|
311
344
|
<GestureDetector gesture={moveGesture}>
|
|
312
345
|
<Pressable
|
|
313
346
|
onPress={onPress}
|
|
347
|
+
// When the bar is draggable, the move gesture claims the long-press
|
|
348
|
+
// first; this fires only for non-draggable (disabled) bars.
|
|
349
|
+
onLongPress={onLongPress}
|
|
314
350
|
accessibilityRole="button"
|
|
315
351
|
accessibilityLabel={pe.event.title}
|
|
316
352
|
accessibilityActions={barActions}
|
|
@@ -342,6 +378,171 @@ function ResourceBar<T>({
|
|
|
342
378
|
);
|
|
343
379
|
}
|
|
344
380
|
|
|
381
|
+
type LaneInteractionLayerProps = {
|
|
382
|
+
resource: Resource;
|
|
383
|
+
/** Time flows down instead of across. */
|
|
384
|
+
vertical: boolean;
|
|
385
|
+
/** Pixels per hour along the time axis. */
|
|
386
|
+
hourSize: number;
|
|
387
|
+
startHour: number;
|
|
388
|
+
date: Date;
|
|
389
|
+
snapMinutes: number;
|
|
390
|
+
onPressCell?: (at: Date, resource: Resource) => void;
|
|
391
|
+
onLongPressCell?: (at: Date, resource: Resource) => void;
|
|
392
|
+
onCreateEvent?: (start: Date, end: Date, resource: Resource) => void;
|
|
393
|
+
theme: ReturnType<typeof useCalendarTheme>;
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
// The tap/long-press/create surface behind a lane's bars, mirroring the time
|
|
397
|
+
// grid's cell layer: taps snap to `snapMinutes`, and (when `onCreateEvent` is
|
|
398
|
+
// set) a long-press starts a drag that sweeps out a ghost along the time axis.
|
|
399
|
+
// Hidden from screen readers — it's a pointer convenience, not the accessible
|
|
400
|
+
// path.
|
|
401
|
+
function LaneInteractionLayer({
|
|
402
|
+
resource,
|
|
403
|
+
vertical,
|
|
404
|
+
hourSize,
|
|
405
|
+
startHour,
|
|
406
|
+
date,
|
|
407
|
+
snapMinutes,
|
|
408
|
+
onPressCell,
|
|
409
|
+
onLongPressCell,
|
|
410
|
+
onCreateEvent,
|
|
411
|
+
theme,
|
|
412
|
+
}: LaneInteractionLayerProps): ReactElement {
|
|
413
|
+
const ghostStart = useSharedValue(0);
|
|
414
|
+
const ghostSize = useSharedValue(0);
|
|
415
|
+
const ghostVisible = useSharedValue(0);
|
|
416
|
+
|
|
417
|
+
const timeAt = useCallback(
|
|
418
|
+
(px: number) => cellRangeFromDrag(date, px, px, hourSize, startHour, snapMinutes)?.start,
|
|
419
|
+
[date, hourSize, startHour, snapMinutes],
|
|
420
|
+
);
|
|
421
|
+
const commitCreate = useCallback(
|
|
422
|
+
(fromPx: number, toPx: number) => {
|
|
423
|
+
const range = cellRangeFromDrag(date, fromPx, toPx, hourSize, startHour, snapMinutes);
|
|
424
|
+
if (range) onCreateEvent?.(range.start, range.end, resource);
|
|
425
|
+
},
|
|
426
|
+
[date, hourSize, startHour, snapMinutes, onCreateEvent, resource],
|
|
427
|
+
);
|
|
428
|
+
|
|
429
|
+
const pressAt = useCallback(
|
|
430
|
+
(px: number) => {
|
|
431
|
+
const at = timeAt(px);
|
|
432
|
+
if (at) onPressCell?.(at, resource);
|
|
433
|
+
},
|
|
434
|
+
[timeAt, onPressCell, resource],
|
|
435
|
+
);
|
|
436
|
+
|
|
437
|
+
// Web: a GestureDetector swallows the Pressable's presses on react-native-web,
|
|
438
|
+
// so taps come back through a Tap gesture, and the create sweep activates past
|
|
439
|
+
// a small drag instead of a long-press so a plain mouse drag creates. Both
|
|
440
|
+
// mirror the time grid's cell layer.
|
|
441
|
+
const laneGesture = useMemo(() => {
|
|
442
|
+
const pan = onCreateEvent
|
|
443
|
+
? Gesture.Pan()
|
|
444
|
+
.onStart((e) => {
|
|
445
|
+
ghostStart.value = vertical ? e.y : e.x;
|
|
446
|
+
ghostSize.value = 0;
|
|
447
|
+
ghostVisible.value = 1;
|
|
448
|
+
})
|
|
449
|
+
.onUpdate((e) => {
|
|
450
|
+
ghostSize.value = (vertical ? e.y : e.x) - ghostStart.value;
|
|
451
|
+
})
|
|
452
|
+
// `success` is false when the gesture is cancelled/failed after it
|
|
453
|
+
// activated (RNGH still calls onEnd); only a real end should create.
|
|
454
|
+
.onEnd((e, success) => {
|
|
455
|
+
ghostVisible.value = 0;
|
|
456
|
+
if (success) runOnJS(commitCreate)(ghostStart.value, vertical ? e.y : e.x);
|
|
457
|
+
})
|
|
458
|
+
.onFinalize(() => {
|
|
459
|
+
ghostVisible.value = 0;
|
|
460
|
+
})
|
|
461
|
+
: null;
|
|
462
|
+
const activatedPan = pan
|
|
463
|
+
? isWeb
|
|
464
|
+
? vertical
|
|
465
|
+
? pan.activeOffsetY([-DRAG_ACTIVATE_PX, DRAG_ACTIVATE_PX])
|
|
466
|
+
: pan.activeOffsetX([-DRAG_ACTIVATE_PX, DRAG_ACTIVATE_PX])
|
|
467
|
+
: pan.activateAfterLongPress(MOVE_ACTIVATE_MS)
|
|
468
|
+
: null;
|
|
469
|
+
const tap =
|
|
470
|
+
isWeb && onPressCell
|
|
471
|
+
? Gesture.Tap().onEnd((e) => {
|
|
472
|
+
runOnJS(pressAt)(vertical ? e.y : e.x);
|
|
473
|
+
})
|
|
474
|
+
: null;
|
|
475
|
+
if (activatedPan && tap) return Gesture.Exclusive(activatedPan, tap);
|
|
476
|
+
return activatedPan ?? tap;
|
|
477
|
+
}, [
|
|
478
|
+
onCreateEvent,
|
|
479
|
+
onPressCell,
|
|
480
|
+
vertical,
|
|
481
|
+
commitCreate,
|
|
482
|
+
pressAt,
|
|
483
|
+
ghostStart,
|
|
484
|
+
ghostSize,
|
|
485
|
+
ghostVisible,
|
|
486
|
+
]);
|
|
487
|
+
|
|
488
|
+
const ghostStyle = useAnimatedStyle(() => {
|
|
489
|
+
// Snap the preview to the same grid the commit resolves to, so what you see
|
|
490
|
+
// is what cellRangeFromDrag will create.
|
|
491
|
+
const stepPx = (snapMinutes / MINUTES_PER_HOUR) * hourSize;
|
|
492
|
+
const snap = (v: number) => (stepPx > 0 ? Math.round(v / stepPx) * stepPx : v);
|
|
493
|
+
const a = snap(ghostStart.value);
|
|
494
|
+
const b = snap(ghostStart.value + ghostSize.value);
|
|
495
|
+
const from = Math.min(a, b);
|
|
496
|
+
const size = Math.max(Math.abs(b - a), 2);
|
|
497
|
+
return vertical
|
|
498
|
+
? { opacity: ghostVisible.value, top: from, height: size }
|
|
499
|
+
: { opacity: ghostVisible.value, left: from, width: size };
|
|
500
|
+
}, [vertical, hourSize, snapMinutes]);
|
|
501
|
+
|
|
502
|
+
const pxOf = (e: GestureResponderEvent) =>
|
|
503
|
+
vertical ? e.nativeEvent.locationY : e.nativeEvent.locationX;
|
|
504
|
+
const handlePress = onPressCell ? (e: GestureResponderEvent) => pressAt(pxOf(e)) : undefined;
|
|
505
|
+
// When create is on, a long-press starts the create drag, so don't also fire
|
|
506
|
+
// the consumer's long-press handler (mirrors the time grid).
|
|
507
|
+
const handleLongPress =
|
|
508
|
+
onLongPressCell && !onCreateEvent
|
|
509
|
+
? (e: GestureResponderEvent) => {
|
|
510
|
+
const at = timeAt(pxOf(e));
|
|
511
|
+
if (at) onLongPressCell(at, resource);
|
|
512
|
+
}
|
|
513
|
+
: undefined;
|
|
514
|
+
|
|
515
|
+
const layer = (
|
|
516
|
+
<Pressable
|
|
517
|
+
testID="resource-cell-layer"
|
|
518
|
+
style={StyleSheet.absoluteFill}
|
|
519
|
+
onPress={handlePress}
|
|
520
|
+
onLongPress={handleLongPress}
|
|
521
|
+
accessibilityElementsHidden
|
|
522
|
+
importantForAccessibility="no"
|
|
523
|
+
/>
|
|
524
|
+
);
|
|
525
|
+
return (
|
|
526
|
+
<>
|
|
527
|
+
{laneGesture ? <GestureDetector gesture={laneGesture}>{layer}</GestureDetector> : layer}
|
|
528
|
+
{onCreateEvent ? (
|
|
529
|
+
<Animated.View
|
|
530
|
+
testID="resource-create-ghost"
|
|
531
|
+
pointerEvents="none"
|
|
532
|
+
style={[
|
|
533
|
+
vertical ? styles.vcreateGhost : styles.hcreateGhost,
|
|
534
|
+
{
|
|
535
|
+
backgroundColor: theme.colors.eventBackground,
|
|
536
|
+
borderColor: theme.colors.todayBackground,
|
|
537
|
+
},
|
|
538
|
+
ghostStyle,
|
|
539
|
+
]}
|
|
540
|
+
/>
|
|
541
|
+
) : null}
|
|
542
|
+
</>
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
345
546
|
/**
|
|
346
547
|
* A horizontal resource timeline: rows are resources (rooms, people, machines)
|
|
347
548
|
* and the x-axis is one day's hours. Events sit in their resource's row, and
|
|
@@ -375,11 +576,22 @@ export function ResourceTimeline<T = unknown>({
|
|
|
375
576
|
ampm = false,
|
|
376
577
|
renderEvent,
|
|
377
578
|
onPressEvent,
|
|
579
|
+
onLongPressEvent,
|
|
378
580
|
onDragEvent,
|
|
581
|
+
onPressCell,
|
|
582
|
+
onLongPressCell,
|
|
583
|
+
onCreateEvent,
|
|
584
|
+
businessHours,
|
|
379
585
|
dragStepMinutes = 15,
|
|
380
586
|
}: ResourceTimelineProps<T>): ReactElement {
|
|
381
587
|
const theme = useCalendarTheme();
|
|
382
588
|
const Renderer = renderEvent ?? DefaultBar;
|
|
589
|
+
const cellEnabled = !!onPressCell || !!onLongPressCell || !!onCreateEvent;
|
|
590
|
+
// The closed-hours bands to shade in a lane, resolved per resource.
|
|
591
|
+
const bandsFor = (resource: Resource) =>
|
|
592
|
+
businessHours
|
|
593
|
+
? closedHourBands(date, (d) => businessHours(d, resource), startHour, endHour)
|
|
594
|
+
: [];
|
|
383
595
|
|
|
384
596
|
const hours = useMemo(
|
|
385
597
|
() => Array.from({ length: Math.max(0, endHour - startHour) }, (_, i) => startHour + i),
|
|
@@ -448,6 +660,22 @@ export function ResourceTimeline<T = unknown>({
|
|
|
448
660
|
theme.containers.resourceRow,
|
|
449
661
|
]}
|
|
450
662
|
>
|
|
663
|
+
{/* Closed-hours shade, behind the grid lines and bars. */}
|
|
664
|
+
{bandsFor(resource).map((b) => (
|
|
665
|
+
<View
|
|
666
|
+
key={`shade-${b.start}`}
|
|
667
|
+
pointerEvents="none"
|
|
668
|
+
testID="resource-hours-shade"
|
|
669
|
+
style={[
|
|
670
|
+
styles.vshadeBand,
|
|
671
|
+
{
|
|
672
|
+
top: (b.start - startHour) * hourHeight,
|
|
673
|
+
height: (b.end - b.start) * hourHeight,
|
|
674
|
+
backgroundColor: theme.colors.outsideHoursBackground,
|
|
675
|
+
},
|
|
676
|
+
]}
|
|
677
|
+
/>
|
|
678
|
+
))}
|
|
451
679
|
{hours.slice(1).map((h) => (
|
|
452
680
|
<View
|
|
453
681
|
key={h}
|
|
@@ -461,6 +689,20 @@ export function ResourceTimeline<T = unknown>({
|
|
|
461
689
|
]}
|
|
462
690
|
/>
|
|
463
691
|
))}
|
|
692
|
+
{cellEnabled ? (
|
|
693
|
+
<LaneInteractionLayer
|
|
694
|
+
resource={resource}
|
|
695
|
+
vertical
|
|
696
|
+
hourSize={hourHeight}
|
|
697
|
+
startHour={startHour}
|
|
698
|
+
date={date}
|
|
699
|
+
snapMinutes={dragStepMinutes}
|
|
700
|
+
onPressCell={onPressCell}
|
|
701
|
+
onLongPressCell={onLongPressCell}
|
|
702
|
+
onCreateEvent={onCreateEvent}
|
|
703
|
+
theme={theme}
|
|
704
|
+
/>
|
|
705
|
+
) : null}
|
|
464
706
|
{positioned.map((pe, idx) => {
|
|
465
707
|
const top =
|
|
466
708
|
clamp(pe.startHours - startHour, 0, endHour - startHour) * hourHeight;
|
|
@@ -488,6 +730,9 @@ export function ResourceTimeline<T = unknown>({
|
|
|
488
730
|
snapMinutes={dragStepMinutes}
|
|
489
731
|
Renderer={Renderer}
|
|
490
732
|
onPress={onPress}
|
|
733
|
+
onLongPress={
|
|
734
|
+
onLongPressEvent ? () => onLongPressEvent(pe.event) : undefined
|
|
735
|
+
}
|
|
491
736
|
onDragEvent={onDragEvent}
|
|
492
737
|
theme={theme}
|
|
493
738
|
/>
|
|
@@ -497,6 +742,9 @@ export function ResourceTimeline<T = unknown>({
|
|
|
497
742
|
<Pressable
|
|
498
743
|
key={idx}
|
|
499
744
|
onPress={onPress}
|
|
745
|
+
onLongPress={
|
|
746
|
+
onLongPressEvent ? () => onLongPressEvent(pe.event) : undefined
|
|
747
|
+
}
|
|
500
748
|
accessibilityRole="button"
|
|
501
749
|
accessibilityLabel={pe.event.title}
|
|
502
750
|
style={{ position: "absolute", left, width, top, height, padding: 1 }}
|
|
@@ -564,6 +812,22 @@ export function ResourceTimeline<T = unknown>({
|
|
|
564
812
|
</Text>
|
|
565
813
|
</View>
|
|
566
814
|
<View style={{ width: trackWidth }}>
|
|
815
|
+
{/* Closed-hours shade, behind the grid lines and bars. */}
|
|
816
|
+
{bandsFor(resource).map((b) => (
|
|
817
|
+
<View
|
|
818
|
+
key={`shade-${b.start}`}
|
|
819
|
+
pointerEvents="none"
|
|
820
|
+
testID="resource-hours-shade"
|
|
821
|
+
style={[
|
|
822
|
+
styles.hshadeBand,
|
|
823
|
+
{
|
|
824
|
+
left: (b.start - startHour) * hourWidth,
|
|
825
|
+
width: (b.end - b.start) * hourWidth,
|
|
826
|
+
backgroundColor: theme.colors.outsideHoursBackground,
|
|
827
|
+
},
|
|
828
|
+
]}
|
|
829
|
+
/>
|
|
830
|
+
))}
|
|
567
831
|
{/* Hour grid lines */}
|
|
568
832
|
{hours.slice(1).map((h) => (
|
|
569
833
|
<View
|
|
@@ -575,6 +839,20 @@ export function ResourceTimeline<T = unknown>({
|
|
|
575
839
|
]}
|
|
576
840
|
/>
|
|
577
841
|
))}
|
|
842
|
+
{cellEnabled ? (
|
|
843
|
+
<LaneInteractionLayer
|
|
844
|
+
resource={resource}
|
|
845
|
+
vertical={false}
|
|
846
|
+
hourSize={hourWidth}
|
|
847
|
+
startHour={startHour}
|
|
848
|
+
date={date}
|
|
849
|
+
snapMinutes={dragStepMinutes}
|
|
850
|
+
onPressCell={onPressCell}
|
|
851
|
+
onLongPressCell={onLongPressCell}
|
|
852
|
+
onCreateEvent={onCreateEvent}
|
|
853
|
+
theme={theme}
|
|
854
|
+
/>
|
|
855
|
+
) : null}
|
|
578
856
|
{positioned.map((pe, idx) => {
|
|
579
857
|
const left = clamp(pe.startHours - startHour, 0, endHour - startHour) * hourWidth;
|
|
580
858
|
const right =
|
|
@@ -599,6 +877,9 @@ export function ResourceTimeline<T = unknown>({
|
|
|
599
877
|
snapMinutes={dragStepMinutes}
|
|
600
878
|
Renderer={Renderer}
|
|
601
879
|
onPress={onPress}
|
|
880
|
+
onLongPress={
|
|
881
|
+
onLongPressEvent ? () => onLongPressEvent(pe.event) : undefined
|
|
882
|
+
}
|
|
602
883
|
onDragEvent={onDragEvent}
|
|
603
884
|
theme={theme}
|
|
604
885
|
/>
|
|
@@ -608,6 +889,7 @@ export function ResourceTimeline<T = unknown>({
|
|
|
608
889
|
<Pressable
|
|
609
890
|
key={idx}
|
|
610
891
|
onPress={onPress}
|
|
892
|
+
onLongPress={onLongPressEvent ? () => onLongPressEvent(pe.event) : undefined}
|
|
611
893
|
accessibilityRole="button"
|
|
612
894
|
accessibilityLabel={pe.event.title}
|
|
613
895
|
style={{
|
|
@@ -679,4 +961,27 @@ const styles = StyleSheet.create({
|
|
|
679
961
|
vcolumn: { flex: 1, borderLeftWidth: StyleSheet.hairlineWidth },
|
|
680
962
|
vhourLabel: { position: "absolute", right: 6, fontSize: 10 },
|
|
681
963
|
vgridLine: { position: "absolute", left: 0, right: 0, height: StyleSheet.hairlineWidth },
|
|
964
|
+
// The drag-to-create preview, sized along the time axis by the gesture. It
|
|
965
|
+
// sits above the bars (like the dom renderer and the time grid) so the sweep
|
|
966
|
+
// stays visible when it crosses an existing event.
|
|
967
|
+
vcreateGhost: {
|
|
968
|
+
position: "absolute",
|
|
969
|
+
left: 2,
|
|
970
|
+
right: 2,
|
|
971
|
+
borderWidth: 1,
|
|
972
|
+
borderRadius: 6,
|
|
973
|
+
opacity: 0,
|
|
974
|
+
zIndex: 2,
|
|
975
|
+
},
|
|
976
|
+
hcreateGhost: {
|
|
977
|
+
position: "absolute",
|
|
978
|
+
top: 2,
|
|
979
|
+
bottom: 2,
|
|
980
|
+
borderWidth: 1,
|
|
981
|
+
borderRadius: 6,
|
|
982
|
+
opacity: 0,
|
|
983
|
+
zIndex: 2,
|
|
984
|
+
},
|
|
985
|
+
hshadeBand: { position: "absolute", top: 0, bottom: 0 },
|
|
986
|
+
vshadeBand: { position: "absolute", left: 0, right: 0 },
|
|
682
987
|
});
|