@mantine/schedule 9.6.3 → 9.7.1
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/cjs/components/DayView/DayView.cjs +46 -31
- package/cjs/components/DayView/DayView.cjs.map +1 -1
- package/cjs/components/DayView/DayView.module.cjs.map +1 -1
- package/cjs/components/MonthView/MonthView.cjs +11 -4
- package/cjs/components/MonthView/MonthView.cjs.map +1 -1
- package/cjs/components/MonthView/get-month-view-events/get-month-positioned-events.cjs +10 -8
- package/cjs/components/MonthView/get-month-view-events/get-month-positioned-events.cjs.map +1 -1
- package/cjs/components/MonthView/get-month-view-events/reserve-row.cjs +12 -0
- package/cjs/components/MonthView/get-month-view-events/reserve-row.cjs.map +1 -0
- package/cjs/components/ResourcesDayView/ResourcesDayView.cjs +75 -60
- package/cjs/components/ResourcesDayView/ResourcesDayView.cjs.map +1 -1
- package/cjs/components/ResourcesDayView/ResourcesDayView.module.cjs.map +1 -1
- package/cjs/components/ResourcesMonthView/ResourcesMonthView.cjs +19 -7
- package/cjs/components/ResourcesMonthView/ResourcesMonthView.cjs.map +1 -1
- package/cjs/components/ResourcesSchedule/ResourcesSchedule.cjs +6 -1
- package/cjs/components/ResourcesSchedule/ResourcesSchedule.cjs.map +1 -1
- package/cjs/components/ResourcesWeekView/ResourcesWeekView.cjs +85 -58
- package/cjs/components/ResourcesWeekView/ResourcesWeekView.cjs.map +1 -1
- package/cjs/components/ResourcesWeekView/ResourcesWeekView.module.cjs.map +1 -1
- package/cjs/components/Schedule/Schedule.cjs +14 -1
- package/cjs/components/Schedule/Schedule.cjs.map +1 -1
- package/cjs/components/ScheduleEvent/ScheduleEvent.module.cjs.map +1 -1
- package/cjs/components/WeekView/WeekView.cjs +70 -46
- package/cjs/components/WeekView/WeekView.cjs.map +1 -1
- package/cjs/components/WeekView/WeekView.module.cjs.map +1 -1
- package/cjs/hooks/use-day-grid-event-resize.cjs +38 -8
- package/cjs/hooks/use-day-grid-event-resize.cjs.map +1 -1
- package/cjs/hooks/use-drag-drop-handlers.cjs +126 -10
- package/cjs/hooks/use-drag-drop-handlers.cjs.map +1 -1
- package/cjs/hooks/use-event-resize.cjs +36 -12
- package/cjs/hooks/use-event-resize.cjs.map +1 -1
- package/cjs/hooks/use-horizontal-event-resize.cjs +37 -12
- package/cjs/hooks/use-horizontal-event-resize.cjs.map +1 -1
- package/cjs/hooks/validate-event-resize.cjs +37 -0
- package/cjs/hooks/validate-event-resize.cjs.map +1 -0
- package/cjs/index.cjs +2 -0
- package/cjs/utils/is-event-placement-allowed/is-event-placement-allowed.cjs +52 -0
- package/cjs/utils/is-event-placement-allowed/is-event-placement-allowed.cjs.map +1 -0
- package/esm/components/DayView/DayView.mjs +46 -31
- package/esm/components/DayView/DayView.mjs.map +1 -1
- package/esm/components/DayView/DayView.module.mjs.map +1 -1
- package/esm/components/MonthView/MonthView.mjs +11 -4
- package/esm/components/MonthView/MonthView.mjs.map +1 -1
- package/esm/components/MonthView/get-month-view-events/get-month-positioned-events.mjs +10 -8
- package/esm/components/MonthView/get-month-view-events/get-month-positioned-events.mjs.map +1 -1
- package/esm/components/MonthView/get-month-view-events/reserve-row.mjs +12 -0
- package/esm/components/MonthView/get-month-view-events/reserve-row.mjs.map +1 -0
- package/esm/components/ResourcesDayView/ResourcesDayView.mjs +75 -60
- package/esm/components/ResourcesDayView/ResourcesDayView.mjs.map +1 -1
- package/esm/components/ResourcesDayView/ResourcesDayView.module.mjs.map +1 -1
- package/esm/components/ResourcesMonthView/ResourcesMonthView.mjs +19 -7
- package/esm/components/ResourcesMonthView/ResourcesMonthView.mjs.map +1 -1
- package/esm/components/ResourcesSchedule/ResourcesSchedule.mjs +6 -1
- package/esm/components/ResourcesSchedule/ResourcesSchedule.mjs.map +1 -1
- package/esm/components/ResourcesWeekView/ResourcesWeekView.mjs +85 -58
- package/esm/components/ResourcesWeekView/ResourcesWeekView.mjs.map +1 -1
- package/esm/components/ResourcesWeekView/ResourcesWeekView.module.mjs.map +1 -1
- package/esm/components/Schedule/Schedule.mjs +14 -1
- package/esm/components/Schedule/Schedule.mjs.map +1 -1
- package/esm/components/ScheduleEvent/ScheduleEvent.module.mjs.map +1 -1
- package/esm/components/WeekView/WeekView.mjs +70 -46
- package/esm/components/WeekView/WeekView.mjs.map +1 -1
- package/esm/components/WeekView/WeekView.module.mjs.map +1 -1
- package/esm/hooks/use-day-grid-event-resize.mjs +38 -8
- package/esm/hooks/use-day-grid-event-resize.mjs.map +1 -1
- package/esm/hooks/use-drag-drop-handlers.mjs +126 -10
- package/esm/hooks/use-drag-drop-handlers.mjs.map +1 -1
- package/esm/hooks/use-event-resize.mjs +36 -12
- package/esm/hooks/use-event-resize.mjs.map +1 -1
- package/esm/hooks/use-horizontal-event-resize.mjs +37 -12
- package/esm/hooks/use-horizontal-event-resize.mjs.map +1 -1
- package/esm/hooks/validate-event-resize.mjs +37 -0
- package/esm/hooks/validate-event-resize.mjs.map +1 -0
- package/esm/index.mjs +2 -1
- package/esm/utils/is-event-placement-allowed/is-event-placement-allowed.mjs +50 -0
- package/esm/utils/is-event-placement-allowed/is-event-placement-allowed.mjs.map +1 -0
- package/lib/components/DayView/DayView.d.ts +11 -1
- package/lib/components/MonthView/MonthView.d.ts +9 -1
- package/lib/components/MonthView/get-month-view-events/reserve-row.d.ts +7 -0
- package/lib/components/ResourcesDayView/ResourcesDayView.d.ts +11 -1
- package/lib/components/ResourcesMonthView/ResourcesMonthView.d.ts +11 -1
- package/lib/components/ResourcesSchedule/ResourcesSchedule.d.ts +12 -2
- package/lib/components/ResourcesWeekView/ResourcesWeekView.d.ts +11 -1
- package/lib/components/Schedule/Schedule.d.ts +12 -2
- package/lib/components/WeekView/WeekView.d.ts +11 -1
- package/lib/hooks/use-day-grid-event-resize.d.ts +4 -2
- package/lib/hooks/use-drag-drop-handlers.d.ts +17 -1
- package/lib/hooks/use-event-resize.d.ts +4 -2
- package/lib/hooks/use-horizontal-event-resize.d.ts +4 -2
- package/lib/hooks/validate-event-resize.d.ts +33 -0
- package/lib/types.d.ts +65 -0
- package/lib/utils/index.d.ts +2 -0
- package/lib/utils/is-event-placement-allowed/is-event-placement-allowed.d.ts +22 -0
- package/package.json +6 -6
- package/styles.css +29 -0
- package/styles.layer.css +29 -0
- package/cjs/components/MonthView/get-month-view-events/find-available-row.cjs +0 -18
- package/cjs/components/MonthView/get-month-view-events/find-available-row.cjs.map +0 -1
- package/esm/components/MonthView/get-month-view-events/find-available-row.mjs +0 -16
- package/esm/components/MonthView/get-month-view-events/find-available-row.mjs.map +0 -1
- package/lib/components/MonthView/get-month-view-events/find-available-row.d.ts +0 -10
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
+
import { isEventPlacementAllowed } from "../utils/is-event-placement-allowed/is-event-placement-allowed.mjs";
|
|
2
3
|
import { useDragState } from "./use-drag-state.mjs";
|
|
3
4
|
import dayjs from "dayjs";
|
|
4
5
|
import { useCallback, useEffectEvent, useState } from "react";
|
|
5
6
|
//#region packages/@mantine/schedule/src/hooks/use-drag-drop-handlers.ts
|
|
7
|
+
const DATE_TIME_FORMAT = "YYYY-MM-DD HH:mm:ss";
|
|
6
8
|
/**
|
|
7
9
|
* Hook that provides unified drag-drop handlers for Schedule views.
|
|
8
10
|
* Handles drag state management and event drops across Day, Week, and Month views.
|
|
@@ -10,18 +12,97 @@ import { useCallback, useEffectEvent, useState } from "react";
|
|
|
10
12
|
* @template T - Type of the drop target (e.g., slot index, day string, etc.)
|
|
11
13
|
*/
|
|
12
14
|
function useDragDropHandlers(options) {
|
|
13
|
-
const { enabled, mode, onEventDrop, canDragEvent, onEventDragStart, onEventDragEnd, calculateDropTarget, onExternalDrop } = options;
|
|
15
|
+
const { enabled, mode, onEventDrop, canDragEvent, onEventDragStart, onEventDragEnd, calculateDropTarget, onExternalDrop, events, preventEventOverlap, canDropEvent, canDropExternalEvent, onEventPlacementRejected, getTargetResourceId, getExternalDropDateTime } = options;
|
|
14
16
|
const stableOnEventDrop = useEffectEvent(onEventDrop || (() => {}));
|
|
17
|
+
const stableOnEventPlacementRejected = useEffectEvent(onEventPlacementRejected || (() => {}));
|
|
15
18
|
const stableOnEventDragStart = useEffectEvent(onEventDragStart || (() => {}));
|
|
16
19
|
const stableOnEventDragEnd = useEffectEvent(onEventDragEnd || (() => {}));
|
|
17
20
|
const stableOnExternalDrop = useEffectEvent(onExternalDrop || (() => {}));
|
|
18
21
|
const dragState = useDragState();
|
|
19
22
|
const [dropTarget, setDropTarget] = useState(null);
|
|
20
23
|
const [dragPreview, setDragPreview] = useState(null);
|
|
24
|
+
const [dropValid, setDropValid] = useState(true);
|
|
25
|
+
const validateDrop = useCallback((target, draggedEvent) => {
|
|
26
|
+
const range = calculateDropTarget(target, draggedEvent);
|
|
27
|
+
const start = dayjs(range.start).format(DATE_TIME_FORMAT);
|
|
28
|
+
const end = dayjs(range.end).format(DATE_TIME_FORMAT);
|
|
29
|
+
const resourceId = getTargetResourceId?.(target);
|
|
30
|
+
const { allowed, conflicts } = isEventPlacementAllowed({
|
|
31
|
+
event: draggedEvent,
|
|
32
|
+
start,
|
|
33
|
+
end,
|
|
34
|
+
events: events || [],
|
|
35
|
+
preventEventOverlap,
|
|
36
|
+
resourceId
|
|
37
|
+
});
|
|
38
|
+
if (!allowed) return {
|
|
39
|
+
valid: false,
|
|
40
|
+
start,
|
|
41
|
+
end,
|
|
42
|
+
resourceId,
|
|
43
|
+
conflicts,
|
|
44
|
+
reason: "overlap"
|
|
45
|
+
};
|
|
46
|
+
if (canDropEvent && canDropEvent({
|
|
47
|
+
event: draggedEvent,
|
|
48
|
+
start,
|
|
49
|
+
end,
|
|
50
|
+
resourceId
|
|
51
|
+
}) === false) return {
|
|
52
|
+
valid: false,
|
|
53
|
+
start,
|
|
54
|
+
end,
|
|
55
|
+
resourceId,
|
|
56
|
+
conflicts: [],
|
|
57
|
+
reason: "rejected"
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
valid: true,
|
|
61
|
+
start,
|
|
62
|
+
end,
|
|
63
|
+
resourceId,
|
|
64
|
+
conflicts: [],
|
|
65
|
+
reason: null
|
|
66
|
+
};
|
|
67
|
+
}, [
|
|
68
|
+
calculateDropTarget,
|
|
69
|
+
getTargetResourceId,
|
|
70
|
+
events,
|
|
71
|
+
preventEventOverlap,
|
|
72
|
+
canDropEvent
|
|
73
|
+
]);
|
|
74
|
+
const validateExternalDrop = useCallback((target, dataTransfer) => {
|
|
75
|
+
if (!canDropExternalEvent || !getExternalDropDateTime) return {
|
|
76
|
+
valid: true,
|
|
77
|
+
start: void 0,
|
|
78
|
+
resourceId: void 0
|
|
79
|
+
};
|
|
80
|
+
const start = getExternalDropDateTime(target);
|
|
81
|
+
if (start === null) return {
|
|
82
|
+
valid: true,
|
|
83
|
+
start: void 0,
|
|
84
|
+
resourceId: void 0
|
|
85
|
+
};
|
|
86
|
+
const resourceId = getTargetResourceId?.(target);
|
|
87
|
+
return {
|
|
88
|
+
valid: canDropExternalEvent({
|
|
89
|
+
dataTransfer,
|
|
90
|
+
start,
|
|
91
|
+
resourceId
|
|
92
|
+
}) !== false,
|
|
93
|
+
start,
|
|
94
|
+
resourceId
|
|
95
|
+
};
|
|
96
|
+
}, [
|
|
97
|
+
canDropExternalEvent,
|
|
98
|
+
getExternalDropDateTime,
|
|
99
|
+
getTargetResourceId
|
|
100
|
+
]);
|
|
21
101
|
const handleDragEnd = useCallback(() => {
|
|
22
102
|
dragState.endDrag();
|
|
23
103
|
setDropTarget(null);
|
|
24
104
|
setDragPreview(null);
|
|
105
|
+
setDropValid(true);
|
|
25
106
|
stableOnEventDragEnd();
|
|
26
107
|
}, [dragState]);
|
|
27
108
|
const handleDragStart = useCallback((event) => {
|
|
@@ -43,14 +124,20 @@ function useDragDropHandlers(options) {
|
|
|
43
124
|
if (isInternalDrag && !enabled) return;
|
|
44
125
|
if (!isInternalDrag && !onExternalDrop) return;
|
|
45
126
|
event.preventDefault();
|
|
127
|
+
const draggedEvent = dragState.state.draggedEvent;
|
|
128
|
+
const valid = isInternalDrag && draggedEvent ? validateDrop(target, draggedEvent).valid : validateExternalDrop(target, event.dataTransfer).valid;
|
|
129
|
+
setDropValid(valid);
|
|
46
130
|
event.dataTransfer.dropEffect = isInternalDrag ? "move" : "copy";
|
|
47
131
|
setDropTarget(target);
|
|
48
132
|
}, [
|
|
49
133
|
enabled,
|
|
50
134
|
mode,
|
|
51
135
|
dragState.state.isDragging,
|
|
136
|
+
dragState.state.draggedEvent,
|
|
52
137
|
onExternalDrop,
|
|
53
|
-
handleDragEnd
|
|
138
|
+
handleDragEnd,
|
|
139
|
+
validateDrop,
|
|
140
|
+
validateExternalDrop
|
|
54
141
|
]);
|
|
55
142
|
const handleDragLeave = useCallback((event) => {
|
|
56
143
|
if (event?.currentTarget) {
|
|
@@ -59,39 +146,67 @@ function useDragDropHandlers(options) {
|
|
|
59
146
|
}
|
|
60
147
|
setDropTarget(null);
|
|
61
148
|
setDragPreview(null);
|
|
149
|
+
setDropValid(true);
|
|
62
150
|
}, []);
|
|
63
151
|
const handleDrop = useCallback((event, target) => {
|
|
64
152
|
event.preventDefault();
|
|
65
153
|
const isInternalDrag = dragState.state.isDragging && event.dataTransfer.types.includes("application/json");
|
|
66
154
|
if (isInternalDrag && enabled && dragState.state.draggedEvent && onEventDrop) {
|
|
67
|
-
const
|
|
155
|
+
const draggedEvent = dragState.state.draggedEvent;
|
|
156
|
+
const { valid, start, end, resourceId, conflicts, reason } = validateDrop(target, draggedEvent);
|
|
157
|
+
if (!valid) {
|
|
158
|
+
stableOnEventPlacementRejected({
|
|
159
|
+
action: "drop",
|
|
160
|
+
event: draggedEvent,
|
|
161
|
+
start,
|
|
162
|
+
end,
|
|
163
|
+
resourceId,
|
|
164
|
+
conflicts,
|
|
165
|
+
reason
|
|
166
|
+
});
|
|
167
|
+
handleDragEnd();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
68
170
|
stableOnEventDrop({
|
|
69
171
|
eventId: dragState.state.draggedEventId,
|
|
70
|
-
newStart:
|
|
71
|
-
newEnd:
|
|
72
|
-
event:
|
|
172
|
+
newStart: start,
|
|
173
|
+
newEnd: end,
|
|
174
|
+
event: draggedEvent
|
|
73
175
|
});
|
|
74
176
|
handleDragEnd();
|
|
75
177
|
return;
|
|
76
178
|
}
|
|
77
179
|
if (!isInternalDrag && onExternalDrop) {
|
|
78
180
|
if (dragState.state.isDragging) handleDragEnd();
|
|
79
|
-
|
|
181
|
+
const external = validateExternalDrop(target, event.dataTransfer);
|
|
182
|
+
if (!external.valid) stableOnEventPlacementRejected({
|
|
183
|
+
action: "external-drop",
|
|
184
|
+
dataTransfer: event.dataTransfer,
|
|
185
|
+
start: external.start,
|
|
186
|
+
resourceId: external.resourceId,
|
|
187
|
+
conflicts: [],
|
|
188
|
+
reason: "rejected"
|
|
189
|
+
});
|
|
190
|
+
else stableOnExternalDrop(event, target);
|
|
80
191
|
setDropTarget(null);
|
|
81
192
|
setDragPreview(null);
|
|
193
|
+
setDropValid(true);
|
|
82
194
|
return;
|
|
83
195
|
}
|
|
84
196
|
setDropTarget(null);
|
|
85
197
|
setDragPreview(null);
|
|
198
|
+
setDropValid(true);
|
|
86
199
|
}, [
|
|
87
200
|
enabled,
|
|
88
201
|
dragState.state,
|
|
89
202
|
onEventDrop,
|
|
90
203
|
onExternalDrop,
|
|
91
|
-
calculateDropTarget,
|
|
92
204
|
handleDragEnd,
|
|
205
|
+
validateDrop,
|
|
206
|
+
validateExternalDrop,
|
|
93
207
|
stableOnEventDrop,
|
|
94
|
-
stableOnExternalDrop
|
|
208
|
+
stableOnExternalDrop,
|
|
209
|
+
stableOnEventPlacementRejected
|
|
95
210
|
]);
|
|
96
211
|
const isDraggableEvent = useCallback((event) => {
|
|
97
212
|
return enabled && mode !== "static" && event.display !== "background" && (canDragEvent ? canDragEvent(event) : true);
|
|
@@ -123,7 +238,8 @@ function useDragDropHandlers(options) {
|
|
|
123
238
|
isDraggableEvent,
|
|
124
239
|
isDropTarget,
|
|
125
240
|
dragPreview,
|
|
126
|
-
setDragPreview
|
|
241
|
+
setDragPreview,
|
|
242
|
+
dropValid
|
|
127
243
|
};
|
|
128
244
|
}
|
|
129
245
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-drag-drop-handlers.mjs","names":[],"sources":["../../src/hooks/use-drag-drop-handlers.ts"],"sourcesContent":["import dayjs from 'dayjs';\nimport { useCallback, useEffectEvent, useState } from 'react';\nimport { DragContextValue } from '../components/DragContext/DragContext';\nimport { DateTimeStringValue, ScheduleEventData, ScheduleMode } from '../types';\nimport { useDragState } from './use-drag-state';\n\nexport interface DragPreview<T = any> {\n start: DateTimeStringValue;\n end: DateTimeStringValue;\n target: T;\n}\n\nexport interface UseDragDropHandlersOptions<T = any> {\n /** Whether drag and drop is enabled */\n enabled: boolean;\n\n /** Schedule interaction mode */\n mode: ScheduleMode;\n\n /** Called when event is dropped at new location */\n onEventDrop?: (data: {\n eventId: string | number;\n newStart: DateTimeStringValue;\n newEnd: DateTimeStringValue;\n event: ScheduleEventData;\n }) => void;\n\n /** Function to determine if event can be dragged */\n canDragEvent?: (event: ScheduleEventData) => boolean;\n\n /** Called when any event drag starts */\n onEventDragStart?: (event: ScheduleEventData) => void;\n\n /** Called when any event drag ends */\n onEventDragEnd?: () => void;\n\n /**\n * Function to calculate drop target dates from drop location.\n * Receives the target location and the dragged event.\n */\n calculateDropTarget: (target: T, draggedEvent: ScheduleEventData) => { start: Date; end: Date };\n\n /** Called when an external item is dropped onto the schedule */\n onExternalDrop?: (e: React.DragEvent, target: T) => void;\n}\n\nexport interface DragDropHandlers<T = any> {\n /** Context value for DragContext.Provider */\n dragContextValue: DragContextValue;\n\n /** Current drop target */\n dropTarget: T | null;\n\n /** Handle drag start event */\n handleDragStart: (event: ScheduleEventData) => void;\n\n /** Handle drag end event */\n handleDragEnd: () => void;\n\n /** Handle drag over event */\n handleDragOver: (e: React.DragEvent, target: T) => void;\n\n /** Handle drag leave event */\n handleDragLeave: (event?: React.DragEvent) => void;\n\n /** Handle drop event */\n handleDrop: (e: React.DragEvent, target: T) => void;\n\n /** Check if event is draggable */\n isDraggableEvent: (event: ScheduleEventData) => boolean;\n\n /** Check if target is the current drop target */\n isDropTarget: (target: T) => boolean;\n\n /** Snapped drop preview used to render the drag ghost, or `null` when not dragging */\n dragPreview: DragPreview<T> | null;\n\n /** Sets the current drag preview */\n setDragPreview: (preview: DragPreview<T> | null) => void;\n}\n\n/**\n * Hook that provides unified drag-drop handlers for Schedule views.\n * Handles drag state management and event drops across Day, Week, and Month views.\n *\n * @template T - Type of the drop target (e.g., slot index, day string, etc.)\n */\nexport function useDragDropHandlers<T = any>(\n options: UseDragDropHandlersOptions<T>\n): DragDropHandlers<T> {\n const {\n enabled,\n mode,\n onEventDrop,\n canDragEvent,\n onEventDragStart,\n onEventDragEnd,\n calculateDropTarget,\n onExternalDrop,\n } = options;\n\n const stableOnEventDrop = useEffectEvent(onEventDrop || (() => {}));\n const stableOnEventDragStart = useEffectEvent(onEventDragStart || (() => {}));\n const stableOnEventDragEnd = useEffectEvent(onEventDragEnd || (() => {}));\n const stableOnExternalDrop = useEffectEvent(onExternalDrop || (() => {}));\n\n const dragState = useDragState();\n const [dropTarget, setDropTarget] = useState<T | null>(null);\n const [dragPreview, setDragPreview] = useState<DragPreview<T> | null>(null);\n\n const handleDragEnd = useCallback(() => {\n dragState.endDrag();\n setDropTarget(null);\n setDragPreview(null);\n stableOnEventDragEnd();\n }, [dragState]);\n\n const handleDragStart = useCallback(\n (event: ScheduleEventData) => {\n if (!enabled || mode === 'static') {\n return;\n }\n dragState.startDrag(event);\n stableOnEventDragStart(event);\n },\n [enabled, mode, dragState]\n );\n\n const handleDragOver = useCallback(\n (event: React.DragEvent, target: T) => {\n if (mode === 'static') {\n return;\n }\n\n let isInternalDrag = dragState.state.isDragging;\n\n if (isInternalDrag && !event.dataTransfer.types.includes('application/json')) {\n handleDragEnd();\n isInternalDrag = false;\n }\n\n if (isInternalDrag && !enabled) {\n return;\n }\n\n if (!isInternalDrag && !onExternalDrop) {\n return;\n }\n\n event.preventDefault();\n event.dataTransfer.dropEffect = isInternalDrag ? 'move' : 'copy';\n setDropTarget(target);\n },\n [enabled, mode, dragState.state.isDragging, onExternalDrop, handleDragEnd]\n );\n\n const handleDragLeave = useCallback((event?: React.DragEvent) => {\n if (event?.currentTarget) {\n const rect = event.currentTarget.getBoundingClientRect();\n const isInside =\n event.clientX >= rect.left &&\n event.clientX < rect.right &&\n event.clientY >= rect.top &&\n event.clientY < rect.bottom;\n if (isInside) {\n return;\n }\n }\n\n setDropTarget(null);\n setDragPreview(null);\n }, []);\n\n const handleDrop = useCallback(\n (event: React.DragEvent, target: T) => {\n event.preventDefault();\n\n const isInternalDrag =\n dragState.state.isDragging && event.dataTransfer.types.includes('application/json');\n\n if (isInternalDrag && enabled && dragState.state.draggedEvent && onEventDrop) {\n const { start, end } = calculateDropTarget(target, dragState.state.draggedEvent);\n stableOnEventDrop({\n eventId: dragState.state.draggedEventId!,\n newStart: dayjs(start).format('YYYY-MM-DD HH:mm:ss'),\n newEnd: dayjs(end).format('YYYY-MM-DD HH:mm:ss'),\n event: dragState.state.draggedEvent,\n });\n handleDragEnd();\n return;\n }\n\n if (!isInternalDrag && onExternalDrop) {\n if (dragState.state.isDragging) {\n handleDragEnd();\n }\n stableOnExternalDrop(event, target);\n setDropTarget(null);\n setDragPreview(null);\n return;\n }\n\n setDropTarget(null);\n setDragPreview(null);\n },\n [\n enabled,\n dragState.state,\n onEventDrop,\n onExternalDrop,\n calculateDropTarget,\n handleDragEnd,\n stableOnEventDrop,\n stableOnExternalDrop,\n ]\n );\n\n const isDraggableEvent = useCallback(\n (event: ScheduleEventData) => {\n return (\n enabled &&\n mode !== 'static' &&\n event.display !== 'background' &&\n (canDragEvent ? canDragEvent(event) : true)\n );\n },\n [enabled, mode, canDragEvent]\n );\n\n const isDropTarget = useCallback(\n (target: T) => {\n // Handle complex target comparison (for WeekView with day + slotIndex)\n if (dropTarget && typeof dropTarget === 'object' && typeof target === 'object') {\n return JSON.stringify(dropTarget) === JSON.stringify(target);\n }\n return dropTarget === target;\n },\n [dropTarget]\n );\n\n const dragContextValue: DragContextValue = {\n isDragging: dragState.state.isDragging,\n draggedEventId: dragState.state.draggedEventId,\n draggedEvent: dragState.state.draggedEvent,\n dropTarget: dragState.state.dropTarget,\n onDragStart: handleDragStart,\n onDragEnd: handleDragEnd,\n setDropTarget: dragState.setDropTarget,\n };\n\n return {\n dragContextValue,\n dropTarget,\n handleDragStart,\n handleDragEnd,\n handleDragOver,\n handleDragLeave,\n handleDrop,\n isDraggableEvent,\n isDropTarget,\n dragPreview,\n setDragPreview,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-drag-drop-handlers.mjs","names":[],"sources":["../../src/hooks/use-drag-drop-handlers.ts"],"sourcesContent":["import dayjs from 'dayjs';\nimport { useCallback, useEffectEvent, useState } from 'react';\nimport { DragContextValue } from '../components/DragContext/DragContext';\nimport {\n DateTimeStringValue,\n PreventEventOverlap,\n ScheduleCanDropEventData,\n ScheduleCanDropExternalEventData,\n ScheduleEventData,\n ScheduleEventPlacementRejectedData,\n ScheduleMode,\n} from '../types';\nimport { isEventPlacementAllowed } from '../utils/is-event-placement-allowed/is-event-placement-allowed';\nimport { useDragState } from './use-drag-state';\n\nconst DATE_TIME_FORMAT = 'YYYY-MM-DD HH:mm:ss';\n\nexport interface DragPreview<T = any> {\n start: DateTimeStringValue;\n end: DateTimeStringValue;\n target: T;\n}\n\nexport interface UseDragDropHandlersOptions<T = any> {\n /** Whether drag and drop is enabled */\n enabled: boolean;\n\n /** Schedule interaction mode */\n mode: ScheduleMode;\n\n /** Called when event is dropped at new location */\n onEventDrop?: (data: {\n eventId: string | number;\n newStart: DateTimeStringValue;\n newEnd: DateTimeStringValue;\n event: ScheduleEventData;\n }) => void;\n\n /** Function to determine if event can be dragged */\n canDragEvent?: (event: ScheduleEventData) => boolean;\n\n /** Called when any event drag starts */\n onEventDragStart?: (event: ScheduleEventData) => void;\n\n /** Called when any event drag ends */\n onEventDragEnd?: () => void;\n\n /**\n * Function to calculate drop target dates from drop location.\n * Receives the target location and the dragged event.\n */\n calculateDropTarget: (target: T, draggedEvent: ScheduleEventData) => { start: Date; end: Date };\n\n /** Called when an external item is dropped onto the schedule */\n onExternalDrop?: (e: React.DragEvent, target: T) => void;\n\n /** Events the candidate range is checked against when `preventEventOverlap` is set */\n events?: ScheduleEventData[];\n\n /** If set, drops that would make the event overlap another event are rejected */\n preventEventOverlap?: PreventEventOverlap;\n\n /** Called on every `dragover` to compute live feedback and again before the drop is committed, return `false` to reject the drop */\n canDropEvent?: (data: ScheduleCanDropEventData) => boolean;\n\n /** Called on every `dragover` of an external item to compute live feedback and again before the drop is committed, return `false` to reject the drop */\n canDropExternalEvent?: (data: ScheduleCanDropExternalEventData) => boolean;\n\n /** Called when a drop is rejected */\n onEventPlacementRejected?: (data: ScheduleEventPlacementRejectedData) => void;\n\n /** Resolves the target resource from the drop target, used by `Resources*` views */\n getTargetResourceId?: (target: T) => string | number | undefined;\n\n /** Resolves the datetime an external item would be dropped at, `null` when the target has no datetime */\n getExternalDropDateTime?: (target: T) => DateTimeStringValue | null;\n}\n\nexport interface DragDropHandlers<T = any> {\n /** Context value for DragContext.Provider */\n dragContextValue: DragContextValue;\n\n /** Current drop target */\n dropTarget: T | null;\n\n /** Handle drag start event */\n handleDragStart: (event: ScheduleEventData) => void;\n\n /** Handle drag end event */\n handleDragEnd: () => void;\n\n /** Handle drag over event */\n handleDragOver: (e: React.DragEvent, target: T) => void;\n\n /** Handle drag leave event */\n handleDragLeave: (event?: React.DragEvent) => void;\n\n /** Handle drop event */\n handleDrop: (e: React.DragEvent, target: T) => void;\n\n /** Check if event is draggable */\n isDraggableEvent: (event: ScheduleEventData) => boolean;\n\n /** Check if target is the current drop target */\n isDropTarget: (target: T) => boolean;\n\n /** Snapped drop preview used to render the drag ghost, or `null` when not dragging */\n dragPreview: DragPreview<T> | null;\n\n /** Sets the current drag preview */\n setDragPreview: (preview: DragPreview<T> | null) => void;\n\n /** False while the pointer is over a target that would be rejected */\n dropValid: boolean;\n}\n\n/**\n * Hook that provides unified drag-drop handlers for Schedule views.\n * Handles drag state management and event drops across Day, Week, and Month views.\n *\n * @template T - Type of the drop target (e.g., slot index, day string, etc.)\n */\nexport function useDragDropHandlers<T = any>(\n options: UseDragDropHandlersOptions<T>\n): DragDropHandlers<T> {\n const {\n enabled,\n mode,\n onEventDrop,\n canDragEvent,\n onEventDragStart,\n onEventDragEnd,\n calculateDropTarget,\n onExternalDrop,\n events,\n preventEventOverlap,\n canDropEvent,\n canDropExternalEvent,\n onEventPlacementRejected,\n getTargetResourceId,\n getExternalDropDateTime,\n } = options;\n\n const stableOnEventDrop = useEffectEvent(onEventDrop || (() => {}));\n const stableOnEventPlacementRejected = useEffectEvent(onEventPlacementRejected || (() => {}));\n const stableOnEventDragStart = useEffectEvent(onEventDragStart || (() => {}));\n const stableOnEventDragEnd = useEffectEvent(onEventDragEnd || (() => {}));\n const stableOnExternalDrop = useEffectEvent(onExternalDrop || (() => {}));\n\n const dragState = useDragState();\n const [dropTarget, setDropTarget] = useState<T | null>(null);\n const [dragPreview, setDragPreview] = useState<DragPreview<T> | null>(null);\n const [dropValid, setDropValid] = useState(true);\n\n const validateDrop = useCallback(\n (target: T, draggedEvent: ScheduleEventData) => {\n const range = calculateDropTarget(target, draggedEvent);\n const start = dayjs(range.start).format(DATE_TIME_FORMAT);\n const end = dayjs(range.end).format(DATE_TIME_FORMAT);\n const resourceId = getTargetResourceId?.(target);\n\n const { allowed, conflicts } = isEventPlacementAllowed({\n event: draggedEvent,\n start,\n end,\n events: events || [],\n preventEventOverlap,\n resourceId,\n });\n\n if (!allowed) {\n return { valid: false, start, end, resourceId, conflicts, reason: 'overlap' as const };\n }\n\n if (canDropEvent && canDropEvent({ event: draggedEvent, start, end, resourceId }) === false) {\n return { valid: false, start, end, resourceId, conflicts: [], reason: 'rejected' as const };\n }\n\n return { valid: true, start, end, resourceId, conflicts: [], reason: null };\n },\n [calculateDropTarget, getTargetResourceId, events, preventEventOverlap, canDropEvent]\n );\n\n const validateExternalDrop = useCallback(\n (target: T, dataTransfer: DataTransfer) => {\n if (!canDropExternalEvent || !getExternalDropDateTime) {\n return { valid: true, start: undefined, resourceId: undefined };\n }\n\n const start = getExternalDropDateTime(target);\n\n if (start === null) {\n return { valid: true, start: undefined, resourceId: undefined };\n }\n\n const resourceId = getTargetResourceId?.(target);\n return {\n valid: canDropExternalEvent({ dataTransfer, start, resourceId }) !== false,\n start,\n resourceId,\n };\n },\n [canDropExternalEvent, getExternalDropDateTime, getTargetResourceId]\n );\n\n const handleDragEnd = useCallback(() => {\n dragState.endDrag();\n setDropTarget(null);\n setDragPreview(null);\n setDropValid(true);\n stableOnEventDragEnd();\n }, [dragState]);\n\n const handleDragStart = useCallback(\n (event: ScheduleEventData) => {\n if (!enabled || mode === 'static') {\n return;\n }\n dragState.startDrag(event);\n stableOnEventDragStart(event);\n },\n [enabled, mode, dragState]\n );\n\n const handleDragOver = useCallback(\n (event: React.DragEvent, target: T) => {\n if (mode === 'static') {\n return;\n }\n\n let isInternalDrag = dragState.state.isDragging;\n\n if (isInternalDrag && !event.dataTransfer.types.includes('application/json')) {\n handleDragEnd();\n isInternalDrag = false;\n }\n\n if (isInternalDrag && !enabled) {\n return;\n }\n\n if (!isInternalDrag && !onExternalDrop) {\n return;\n }\n\n event.preventDefault();\n\n const draggedEvent = dragState.state.draggedEvent;\n const valid =\n isInternalDrag && draggedEvent\n ? validateDrop(target, draggedEvent).valid\n : validateExternalDrop(target, event.dataTransfer).valid;\n\n setDropValid(valid);\n\n // The drop effect stays permissive even for an invalid target: setting it to 'none' makes the\n // browser end the drag with dragleave/dragend and never fire `drop`, which would stop\n // `handleDrop` from rejecting the placement and reporting it through onEventPlacementRejected.\n // Invalid targets are signalled with `data-invalid` on the drag preview instead.\n event.dataTransfer.dropEffect = isInternalDrag ? 'move' : 'copy';\n setDropTarget(target);\n },\n [\n enabled,\n mode,\n dragState.state.isDragging,\n dragState.state.draggedEvent,\n onExternalDrop,\n handleDragEnd,\n validateDrop,\n validateExternalDrop,\n ]\n );\n\n const handleDragLeave = useCallback((event?: React.DragEvent) => {\n if (event?.currentTarget) {\n const rect = event.currentTarget.getBoundingClientRect();\n const isInside =\n event.clientX >= rect.left &&\n event.clientX < rect.right &&\n event.clientY >= rect.top &&\n event.clientY < rect.bottom;\n if (isInside) {\n return;\n }\n }\n\n setDropTarget(null);\n setDragPreview(null);\n setDropValid(true);\n }, []);\n\n const handleDrop = useCallback(\n (event: React.DragEvent, target: T) => {\n event.preventDefault();\n\n const isInternalDrag =\n dragState.state.isDragging && event.dataTransfer.types.includes('application/json');\n\n if (isInternalDrag && enabled && dragState.state.draggedEvent && onEventDrop) {\n const draggedEvent = dragState.state.draggedEvent;\n const { valid, start, end, resourceId, conflicts, reason } = validateDrop(\n target,\n draggedEvent\n );\n\n if (!valid) {\n stableOnEventPlacementRejected({\n action: 'drop',\n event: draggedEvent,\n start,\n end,\n resourceId,\n conflicts,\n reason: reason!,\n });\n handleDragEnd();\n return;\n }\n\n stableOnEventDrop({\n eventId: dragState.state.draggedEventId!,\n newStart: start,\n newEnd: end,\n event: draggedEvent,\n });\n handleDragEnd();\n return;\n }\n\n if (!isInternalDrag && onExternalDrop) {\n if (dragState.state.isDragging) {\n handleDragEnd();\n }\n\n const external = validateExternalDrop(target, event.dataTransfer);\n\n if (!external.valid) {\n stableOnEventPlacementRejected({\n action: 'external-drop',\n dataTransfer: event.dataTransfer,\n start: external.start!,\n resourceId: external.resourceId,\n conflicts: [],\n reason: 'rejected',\n });\n } else {\n stableOnExternalDrop(event, target);\n }\n\n setDropTarget(null);\n setDragPreview(null);\n setDropValid(true);\n return;\n }\n\n setDropTarget(null);\n setDragPreview(null);\n setDropValid(true);\n },\n [\n enabled,\n dragState.state,\n onEventDrop,\n onExternalDrop,\n handleDragEnd,\n validateDrop,\n validateExternalDrop,\n stableOnEventDrop,\n stableOnExternalDrop,\n stableOnEventPlacementRejected,\n ]\n );\n\n const isDraggableEvent = useCallback(\n (event: ScheduleEventData) => {\n return (\n enabled &&\n mode !== 'static' &&\n event.display !== 'background' &&\n (canDragEvent ? canDragEvent(event) : true)\n );\n },\n [enabled, mode, canDragEvent]\n );\n\n const isDropTarget = useCallback(\n (target: T) => {\n // Handle complex target comparison (for WeekView with day + slotIndex)\n if (dropTarget && typeof dropTarget === 'object' && typeof target === 'object') {\n return JSON.stringify(dropTarget) === JSON.stringify(target);\n }\n return dropTarget === target;\n },\n [dropTarget]\n );\n\n const dragContextValue: DragContextValue = {\n isDragging: dragState.state.isDragging,\n draggedEventId: dragState.state.draggedEventId,\n draggedEvent: dragState.state.draggedEvent,\n dropTarget: dragState.state.dropTarget,\n onDragStart: handleDragStart,\n onDragEnd: handleDragEnd,\n setDropTarget: dragState.setDropTarget,\n };\n\n return {\n dragContextValue,\n dropTarget,\n handleDragStart,\n handleDragEnd,\n handleDragOver,\n handleDragLeave,\n handleDrop,\n isDraggableEvent,\n isDropTarget,\n dragPreview,\n setDragPreview,\n dropValid,\n };\n}\n"],"mappings":"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"}
|
|
@@ -1,14 +1,26 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { clampIntervalMinutes } from "../utils/clamp-interval-minutes/clamp-interval-minutes.mjs";
|
|
3
3
|
import { parseTimeString } from "../utils/parse-time-string/parse-time-string.mjs";
|
|
4
|
+
import { validateEventResize } from "./validate-event-resize.mjs";
|
|
4
5
|
import dayjs from "dayjs";
|
|
5
6
|
import { useCallback, useEffect, useEffectEvent, useRef, useState } from "react";
|
|
6
7
|
//#region packages/@mantine/schedule/src/hooks/use-event-resize.ts
|
|
7
|
-
function useEventResize({ enabled = false, mode = "default", startTime, endTime, intervalMinutes, resizeIntervalMinutes, onEventResize, canResizeEvent, withBackgroundEvents = false }) {
|
|
8
|
+
function useEventResize({ enabled = false, mode = "default", startTime, endTime, intervalMinutes, resizeIntervalMinutes, onEventResize, canResizeEvent, events, preventEventOverlap, canResizeEventTo, onEventPlacementRejected, withBackgroundEvents = false }) {
|
|
8
9
|
const [resizeState, setResizeState] = useState(null);
|
|
10
|
+
const [resizeValid, setResizeValid] = useState(true);
|
|
9
11
|
const resizeRef = useRef(null);
|
|
10
12
|
const justResizedRef = useRef(false);
|
|
11
13
|
const stableOnEventResize = useEffectEvent(onEventResize || (() => {}));
|
|
14
|
+
const stableOnEventPlacementRejected = useEffectEvent(onEventPlacementRejected || (() => {}));
|
|
15
|
+
const validateResize = useEffectEvent((event, start, end, edge) => validateEventResize({
|
|
16
|
+
event,
|
|
17
|
+
start,
|
|
18
|
+
end,
|
|
19
|
+
edge,
|
|
20
|
+
events,
|
|
21
|
+
preventEventOverlap,
|
|
22
|
+
canResizeEventTo
|
|
23
|
+
}));
|
|
12
24
|
const parsedStartTime = parseTimeString(startTime);
|
|
13
25
|
const parsedEndTime = parseTimeString(endTime);
|
|
14
26
|
const startMinutes = parsedStartTime.hours * 60 + parsedStartTime.minutes;
|
|
@@ -67,7 +79,13 @@ function useEventResize({ enabled = false, mode = "default", startTime, endTime,
|
|
|
67
79
|
};
|
|
68
80
|
resizeRef.current = state;
|
|
69
81
|
setResizeState(state);
|
|
82
|
+
setResizeValid(true);
|
|
70
83
|
}, [enabled, mode]);
|
|
84
|
+
const getResizeRange = useCallback((state) => ({
|
|
85
|
+
start: state.edge === "top" ? percentToDateTime(state.currentTop, state.eventDate) : state.originalStart,
|
|
86
|
+
end: state.edge === "top" ? state.originalEnd : percentToDateTime(state.currentTop + state.currentHeight, state.eventDate),
|
|
87
|
+
edge: state.edge === "top" ? "start" : "end"
|
|
88
|
+
}), [percentToDateTime]);
|
|
71
89
|
const isResizing = resizeState !== null;
|
|
72
90
|
useEffect(() => {
|
|
73
91
|
if (!isResizing) return;
|
|
@@ -98,30 +116,35 @@ function useEventResize({ enabled = false, mode = "default", startTime, endTime,
|
|
|
98
116
|
currentHeight: newHeight
|
|
99
117
|
};
|
|
100
118
|
setResizeState(resizeRef.current);
|
|
119
|
+
const range = getResizeRange(resizeRef.current);
|
|
120
|
+
setResizeValid(validateResize(state.event, range.start, range.end, range.edge).valid);
|
|
101
121
|
};
|
|
102
122
|
const handlePointerUp = () => {
|
|
103
123
|
const state = resizeRef.current;
|
|
104
124
|
if (state) {
|
|
105
125
|
if (state.currentTop !== state.originalTop || state.currentHeight !== state.originalHeight) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
if (
|
|
109
|
-
newStart = percentToDateTime(state.currentTop, state.eventDate);
|
|
110
|
-
newEnd = state.originalEnd;
|
|
111
|
-
} else {
|
|
112
|
-
newStart = state.originalStart;
|
|
113
|
-
newEnd = percentToDateTime(state.currentTop + state.currentHeight, state.eventDate);
|
|
114
|
-
}
|
|
115
|
-
stableOnEventResize({
|
|
126
|
+
const { start: newStart, end: newEnd, edge } = getResizeRange(state);
|
|
127
|
+
const { valid, conflicts, reason } = validateResize(state.event, newStart, newEnd, edge);
|
|
128
|
+
if (valid) stableOnEventResize({
|
|
116
129
|
eventId: state.eventId,
|
|
117
130
|
newStart,
|
|
118
131
|
newEnd,
|
|
119
132
|
event: state.event
|
|
120
133
|
});
|
|
134
|
+
else stableOnEventPlacementRejected({
|
|
135
|
+
action: "resize",
|
|
136
|
+
event: state.event,
|
|
137
|
+
start: newStart,
|
|
138
|
+
end: newEnd,
|
|
139
|
+
edge,
|
|
140
|
+
conflicts,
|
|
141
|
+
reason
|
|
142
|
+
});
|
|
121
143
|
}
|
|
122
144
|
}
|
|
123
145
|
resizeRef.current = null;
|
|
124
146
|
setResizeState(null);
|
|
147
|
+
setResizeValid(true);
|
|
125
148
|
justResizedRef.current = true;
|
|
126
149
|
requestAnimationFrame(() => {
|
|
127
150
|
justResizedRef.current = false;
|
|
@@ -160,7 +183,8 @@ function useEventResize({ enabled = false, mode = "default", startTime, endTime,
|
|
|
160
183
|
resizingEventId: resizeState?.eventId ?? null,
|
|
161
184
|
getResizePosition,
|
|
162
185
|
isResizableEvent,
|
|
163
|
-
wasResizing
|
|
186
|
+
wasResizing,
|
|
187
|
+
resizeValid
|
|
164
188
|
};
|
|
165
189
|
}
|
|
166
190
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-event-resize.mjs","names":[],"sources":["../../src/hooks/use-event-resize.ts"],"sourcesContent":["import dayjs from 'dayjs';\nimport { useCallback, useEffect, useEffectEvent, useRef, useState } from 'react';\nimport { DateTimeStringValue, ScheduleEventData, ScheduleMode } from '../types';\nimport { clampIntervalMinutes } from '../utils/clamp-interval-minutes/clamp-interval-minutes';\nimport { parseTimeString } from '../utils/parse-time-string/parse-time-string';\n\ntype ResizeEdge = 'top' | 'bottom';\n\ninterface ResizeState {\n eventId: string | number;\n event: ScheduleEventData;\n edge: ResizeEdge;\n container: HTMLElement;\n originalTop: number;\n originalHeight: number;\n currentTop: number;\n currentHeight: number;\n eventDate: string;\n originalStart: DateTimeStringValue;\n originalEnd: DateTimeStringValue;\n}\n\nexport interface UseEventResizeInput {\n enabled?: boolean;\n mode?: ScheduleMode;\n startTime: string;\n endTime: string;\n intervalMinutes: number;\n resizeIntervalMinutes?: number;\n onEventResize?: (data: {\n eventId: string | number;\n newStart: DateTimeStringValue;\n newEnd: DateTimeStringValue;\n event: ScheduleEventData;\n }) => void;\n canResizeEvent?: (event: ScheduleEventData) => boolean;\n withBackgroundEvents?: boolean;\n}\n\nexport function useEventResize({\n enabled = false,\n mode = 'default',\n startTime,\n endTime,\n intervalMinutes,\n resizeIntervalMinutes,\n onEventResize,\n canResizeEvent,\n withBackgroundEvents = false,\n}: UseEventResizeInput) {\n const [resizeState, setResizeState] = useState<ResizeState | null>(null);\n const resizeRef = useRef<ResizeState | null>(null);\n const justResizedRef = useRef(false);\n const stableOnEventResize = useEffectEvent(onEventResize || (() => {}));\n\n const parsedStartTime = parseTimeString(startTime);\n const parsedEndTime = parseTimeString(endTime);\n const startMinutes = parsedStartTime.hours * 60 + parsedStartTime.minutes;\n const endMinutes = parsedEndTime.hours * 60 + parsedEndTime.minutes;\n const clampedInterval = clampIntervalMinutes(intervalMinutes);\n const clampedResizeInterval = clampIntervalMinutes(resizeIntervalMinutes ?? intervalMinutes);\n const literalRange = endMinutes - startMinutes;\n const totalMinutes = Math.ceil(literalRange / clampedInterval) * clampedInterval;\n\n // Snapping happens in absolute minutes from midnight – the same grid `calculateDropTime`\n // uses for drag and drop. Snapping relative to `startTime` would put resized edges on a\n // different grid than dragged events whenever `startTime` is not a multiple of the interval\n // (with `startTime=\"08:10\"` and a 30 minute step, drops land on :00/:30 but resizes on\n // :10/:40, so the two operations could never align an event).\n const clampAndSnap = useCallback(\n (minutes: number): number => {\n const snapped =\n Math.round((startMinutes + minutes) / clampedResizeInterval) * clampedResizeInterval;\n return Math.max(0, Math.min(literalRange, snapped - startMinutes));\n },\n [literalRange, clampedResizeInterval, startMinutes]\n );\n\n const percentToDateTime = useCallback(\n (percent: number, eventDate: string): DateTimeStringValue => {\n const minutes = (percent / 100) * totalMinutes;\n const snappedMinutes = clampAndSnap(minutes);\n const totalMins = startMinutes + snappedMinutes;\n const hours = Math.floor(totalMins / 60);\n const mins = totalMins % 60;\n return `${eventDate} ${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}:00`;\n },\n [totalMinutes, startMinutes, clampAndSnap]\n );\n\n const snapEdgeMinutes = useCallback(\n (minutes: number, direction: 'up' | 'down'): number => {\n const absolute = startMinutes + minutes;\n const snapped =\n direction === 'up'\n ? Math.ceil(absolute / clampedResizeInterval) * clampedResizeInterval\n : Math.floor(absolute / clampedResizeInterval) * clampedResizeInterval;\n return Math.max(0, Math.min(literalRange, snapped - startMinutes));\n },\n [literalRange, clampedResizeInterval, startMinutes]\n );\n\n const percentToMinutes = useCallback(\n (percent: number): number => (percent / 100) * totalMinutes,\n [totalMinutes]\n );\n\n const minutesToPercent = useCallback(\n (minutes: number): number => (minutes / totalMinutes) * 100,\n [totalMinutes]\n );\n\n const handleResizeStart = useCallback(\n ({\n event,\n edge,\n container,\n originalTop,\n originalHeight,\n eventDate,\n pointerEvent,\n }: {\n event: ScheduleEventData;\n edge: ResizeEdge;\n container: HTMLElement;\n originalTop: number;\n originalHeight: number;\n eventDate: string;\n pointerEvent: React.PointerEvent;\n }) => {\n if (!enabled || mode === 'static') {\n return;\n }\n\n pointerEvent.preventDefault();\n pointerEvent.stopPropagation();\n\n const state: ResizeState = {\n eventId: event.id,\n event,\n edge,\n container,\n originalTop,\n originalHeight,\n currentTop: originalTop,\n currentHeight: originalHeight,\n eventDate,\n originalStart: dayjs(event.start).format('YYYY-MM-DD HH:mm:ss'),\n originalEnd: dayjs(event.end).format('YYYY-MM-DD HH:mm:ss'),\n };\n\n resizeRef.current = state;\n setResizeState(state);\n },\n [enabled, mode]\n );\n\n const isResizing = resizeState !== null;\n\n useEffect(() => {\n if (!isResizing) {\n return undefined;\n }\n\n const savedUserSelect = document.body.style.userSelect;\n document.body.style.userSelect = 'none';\n\n const handlePointerMove = (e: PointerEvent) => {\n const state = resizeRef.current;\n if (!state) {\n return;\n }\n\n const containerRect = state.container.getBoundingClientRect();\n const relativeY = e.clientY - containerRect.top;\n const rawPercent = Math.max(0, Math.min(100, (relativeY / containerRect.height) * 100));\n const draggedMinutes = clampAndSnap(percentToMinutes(rawPercent));\n\n let newTop = state.originalTop;\n let newHeight = state.originalHeight;\n\n if (state.edge === 'bottom') {\n const topMinutes = percentToMinutes(state.originalTop);\n const minEndMinutes = snapEdgeMinutes(topMinutes + clampedResizeInterval, 'up');\n const endMinutes = Math.max(draggedMinutes, minEndMinutes);\n newHeight = Math.max(0, minutesToPercent(endMinutes) - state.originalTop);\n } else {\n const bottomMinutes = percentToMinutes(state.originalTop + state.originalHeight);\n const maxStartMinutes = snapEdgeMinutes(bottomMinutes - clampedResizeInterval, 'down');\n const startMinutesValue = Math.min(draggedMinutes, maxStartMinutes);\n newTop = minutesToPercent(startMinutesValue);\n newHeight = Math.max(0, state.originalTop + state.originalHeight - newTop);\n }\n\n resizeRef.current = { ...state, currentTop: newTop, currentHeight: newHeight };\n setResizeState(resizeRef.current);\n };\n\n const handlePointerUp = () => {\n const state = resizeRef.current;\n if (state) {\n if (\n state.currentTop !== state.originalTop ||\n state.currentHeight !== state.originalHeight\n ) {\n let newStart: DateTimeStringValue;\n let newEnd: DateTimeStringValue;\n\n if (state.edge === 'top') {\n newStart = percentToDateTime(state.currentTop, state.eventDate);\n newEnd = state.originalEnd;\n } else {\n newStart = state.originalStart;\n newEnd = percentToDateTime(state.currentTop + state.currentHeight, state.eventDate);\n }\n\n stableOnEventResize({\n eventId: state.eventId,\n newStart,\n newEnd,\n event: state.event,\n });\n }\n }\n resizeRef.current = null;\n setResizeState(null);\n justResizedRef.current = true;\n requestAnimationFrame(() => {\n justResizedRef.current = false;\n });\n };\n\n document.addEventListener('pointermove', handlePointerMove);\n document.addEventListener('pointerup', handlePointerUp);\n\n return () => {\n document.body.style.userSelect = savedUserSelect;\n document.removeEventListener('pointermove', handlePointerMove);\n document.removeEventListener('pointerup', handlePointerUp);\n };\n }, [isResizing]);\n\n const getResizePosition = useCallback(\n (eventId: string | number, eventDate?: string) => {\n if (!resizeState || resizeState.eventId !== eventId) {\n return null;\n }\n if (eventDate !== undefined && resizeState.eventDate !== eventDate) {\n return null;\n }\n return { top: resizeState.currentTop, height: resizeState.currentHeight };\n },\n [resizeState]\n );\n\n const isResizableEvent = useCallback(\n (event: ScheduleEventData) => {\n if (!enabled || mode === 'static') {\n return false;\n }\n if (event.display === 'background' && !withBackgroundEvents) {\n return false;\n }\n return canResizeEvent ? canResizeEvent(event) : true;\n },\n [enabled, mode, canResizeEvent, withBackgroundEvents]\n );\n\n const wasResizing = useCallback(() => justResizedRef.current, []);\n\n return {\n handleResizeStart,\n isResizing,\n resizingEventId: resizeState?.eventId ?? null,\n getResizePosition,\n isResizableEvent,\n wasResizing,\n };\n}\n"],"mappings":";;;;;;AAuCA,SAAgB,eAAe,EAC7B,UAAU,OACV,OAAO,WACP,WACA,SACA,iBACA,uBACA,eACA,gBACA,uBAAuB,SACD;CACtB,MAAM,CAAC,aAAa,kBAAkB,SAA6B,IAAI;CACvE,MAAM,YAAY,OAA2B,IAAI;CACjD,MAAM,iBAAiB,OAAO,KAAK;CACnC,MAAM,sBAAsB,eAAe,wBAAwB,CAAC,EAAE;CAEtE,MAAM,kBAAkB,gBAAgB,SAAS;CACjD,MAAM,gBAAgB,gBAAgB,OAAO;CAC7C,MAAM,eAAe,gBAAgB,QAAQ,KAAK,gBAAgB;CAClE,MAAM,aAAa,cAAc,QAAQ,KAAK,cAAc;CAC5D,MAAM,kBAAkB,qBAAqB,eAAe;CAC5D,MAAM,wBAAwB,qBAAqB,yBAAyB,eAAe;CAC3F,MAAM,eAAe,aAAa;CAClC,MAAM,eAAe,KAAK,KAAK,eAAe,eAAe,IAAI;CAOjE,MAAM,eAAe,aAClB,YAA4B;EAC3B,MAAM,UACJ,KAAK,OAAO,eAAe,WAAW,qBAAqB,IAAI;EACjE,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,cAAc,UAAU,YAAY,CAAC;CACnE,GACA;EAAC;EAAc;EAAuB;CAAY,CACpD;CAEA,MAAM,oBAAoB,aACvB,SAAiB,cAA2C;EAC3D,MAAM,UAAW,UAAU,MAAO;EAClC,MAAM,iBAAiB,aAAa,OAAO;EAC3C,MAAM,YAAY,eAAe;EACjC,MAAM,QAAQ,KAAK,MAAM,YAAY,EAAE;EACvC,MAAM,OAAO,YAAY;EACzB,OAAO,GAAG,UAAU,GAAG,OAAO,KAAK,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE,GAAG,OAAO,IAAI,CAAC,CAAC,SAAS,GAAG,GAAG,EAAE;CACzF,GACA;EAAC;EAAc;EAAc;CAAY,CAC3C;CAEA,MAAM,kBAAkB,aACrB,SAAiB,cAAqC;EACrD,MAAM,WAAW,eAAe;EAChC,MAAM,UACJ,cAAc,OACV,KAAK,KAAK,WAAW,qBAAqB,IAAI,wBAC9C,KAAK,MAAM,WAAW,qBAAqB,IAAI;EACrD,OAAO,KAAK,IAAI,GAAG,KAAK,IAAI,cAAc,UAAU,YAAY,CAAC;CACnE,GACA;EAAC;EAAc;EAAuB;CAAY,CACpD;CAEA,MAAM,mBAAmB,aACtB,YAA6B,UAAU,MAAO,cAC/C,CAAC,YAAY,CACf;CAEA,MAAM,mBAAmB,aACtB,YAA6B,UAAU,eAAgB,KACxD,CAAC,YAAY,CACf;CAEA,MAAM,oBAAoB,aACvB,EACC,OACA,MACA,WACA,aACA,gBACA,WACA,mBASI;EACJ,IAAI,CAAC,WAAW,SAAS,UACvB;EAGF,aAAa,eAAe;EAC5B,aAAa,gBAAgB;EAE7B,MAAM,QAAqB;GACzB,SAAS,MAAM;GACf;GACA;GACA;GACA;GACA;GACA,YAAY;GACZ,eAAe;GACf;GACA,eAAe,MAAM,MAAM,KAAK,CAAC,CAAC,OAAO,qBAAqB;GAC9D,aAAa,MAAM,MAAM,GAAG,CAAC,CAAC,OAAO,qBAAqB;EAC5D;EAEA,UAAU,UAAU;EACpB,eAAe,KAAK;CACtB,GACA,CAAC,SAAS,IAAI,CAChB;CAEA,MAAM,aAAa,gBAAgB;CAEnC,gBAAgB;EACd,IAAI,CAAC,YACH;EAGF,MAAM,kBAAkB,SAAS,KAAK,MAAM;EAC5C,SAAS,KAAK,MAAM,aAAa;EAEjC,MAAM,qBAAqB,MAAoB;GAC7C,MAAM,QAAQ,UAAU;GACxB,IAAI,CAAC,OACH;GAGF,MAAM,gBAAgB,MAAM,UAAU,sBAAsB;GAC5D,MAAM,YAAY,EAAE,UAAU,cAAc;GAC5C,MAAM,aAAa,KAAK,IAAI,GAAG,KAAK,IAAI,KAAM,YAAY,cAAc,SAAU,GAAG,CAAC;GACtF,MAAM,iBAAiB,aAAa,iBAAiB,UAAU,CAAC;GAEhE,IAAI,SAAS,MAAM;GACnB,IAAI,YAAY,MAAM;GAEtB,IAAI,MAAM,SAAS,UAAU;IAC3B,MAAM,aAAa,iBAAiB,MAAM,WAAW;IACrD,MAAM,gBAAgB,gBAAgB,aAAa,uBAAuB,IAAI;IAE9E,YAAY,KAAK,IAAI,GAAG,iBADL,KAAK,IAAI,gBAAgB,aACM,CAAC,IAAI,MAAM,WAAW;GAC1E,OAAO;IACL,MAAM,gBAAgB,iBAAiB,MAAM,cAAc,MAAM,cAAc;IAC/E,MAAM,kBAAkB,gBAAgB,gBAAgB,uBAAuB,MAAM;IAErF,SAAS,iBADiB,KAAK,IAAI,gBAAgB,eACT,CAAC;IAC3C,YAAY,KAAK,IAAI,GAAG,MAAM,cAAc,MAAM,iBAAiB,MAAM;GAC3E;GAEA,UAAU,UAAU;IAAE,GAAG;IAAO,YAAY;IAAQ,eAAe;GAAU;GAC7E,eAAe,UAAU,OAAO;EAClC;EAEA,MAAM,wBAAwB;GAC5B,MAAM,QAAQ,UAAU;GACxB,IAAI,OAEA;QAAA,MAAM,eAAe,MAAM,eAC3B,MAAM,kBAAkB,MAAM,gBAC9B;KACA,IAAI;KACJ,IAAI;KAEJ,IAAI,MAAM,SAAS,OAAO;MACxB,WAAW,kBAAkB,MAAM,YAAY,MAAM,SAAS;MAC9D,SAAS,MAAM;KACjB,OAAO;MACL,WAAW,MAAM;MACjB,SAAS,kBAAkB,MAAM,aAAa,MAAM,eAAe,MAAM,SAAS;KACpF;KAEA,oBAAoB;MAClB,SAAS,MAAM;MACf;MACA;MACA,OAAO,MAAM;KACf,CAAC;IACH;;GAEF,UAAU,UAAU;GACpB,eAAe,IAAI;GACnB,eAAe,UAAU;GACzB,4BAA4B;IAC1B,eAAe,UAAU;GAC3B,CAAC;EACH;EAEA,SAAS,iBAAiB,eAAe,iBAAiB;EAC1D,SAAS,iBAAiB,aAAa,eAAe;EAEtD,aAAa;GACX,SAAS,KAAK,MAAM,aAAa;GACjC,SAAS,oBAAoB,eAAe,iBAAiB;GAC7D,SAAS,oBAAoB,aAAa,eAAe;EAC3D;CACF,GAAG,CAAC,UAAU,CAAC;CAEf,MAAM,oBAAoB,aACvB,SAA0B,cAAuB;EAChD,IAAI,CAAC,eAAe,YAAY,YAAY,SAC1C,OAAO;EAET,IAAI,cAAc,KAAA,KAAa,YAAY,cAAc,WACvD,OAAO;EAET,OAAO;GAAE,KAAK,YAAY;GAAY,QAAQ,YAAY;EAAc;CAC1E,GACA,CAAC,WAAW,CACd;CAEA,MAAM,mBAAmB,aACtB,UAA6B;EAC5B,IAAI,CAAC,WAAW,SAAS,UACvB,OAAO;EAET,IAAI,MAAM,YAAY,gBAAgB,CAAC,sBACrC,OAAO;EAET,OAAO,iBAAiB,eAAe,KAAK,IAAI;CAClD,GACA;EAAC;EAAS;EAAM;EAAgB;CAAoB,CACtD;CAEA,MAAM,cAAc,kBAAkB,eAAe,SAAS,CAAC,CAAC;CAEhE,OAAO;EACL;EACA;EACA,iBAAiB,aAAa,WAAW;EACzC;EACA;EACA;CACF;AACF"}
|
|
1
|
+
{"version":3,"file":"use-event-resize.mjs","names":[],"sources":["../../src/hooks/use-event-resize.ts"],"sourcesContent":["import dayjs from 'dayjs';\nimport { useCallback, useEffect, useEffectEvent, useRef, useState } from 'react';\nimport { DateTimeStringValue, ScheduleEventData, ScheduleMode } from '../types';\nimport { clampIntervalMinutes } from '../utils/clamp-interval-minutes/clamp-interval-minutes';\nimport { parseTimeString } from '../utils/parse-time-string/parse-time-string';\nimport { EventResizeValidationOptions, validateEventResize } from './validate-event-resize';\n\ntype ResizeEdge = 'top' | 'bottom';\n\ninterface ResizeState {\n eventId: string | number;\n event: ScheduleEventData;\n edge: ResizeEdge;\n container: HTMLElement;\n originalTop: number;\n originalHeight: number;\n currentTop: number;\n currentHeight: number;\n eventDate: string;\n originalStart: DateTimeStringValue;\n originalEnd: DateTimeStringValue;\n}\n\nexport interface UseEventResizeInput extends EventResizeValidationOptions {\n enabled?: boolean;\n mode?: ScheduleMode;\n startTime: string;\n endTime: string;\n intervalMinutes: number;\n resizeIntervalMinutes?: number;\n onEventResize?: (data: {\n eventId: string | number;\n newStart: DateTimeStringValue;\n newEnd: DateTimeStringValue;\n event: ScheduleEventData;\n }) => void;\n canResizeEvent?: (event: ScheduleEventData) => boolean;\n withBackgroundEvents?: boolean;\n}\n\nexport function useEventResize({\n enabled = false,\n mode = 'default',\n startTime,\n endTime,\n intervalMinutes,\n resizeIntervalMinutes,\n onEventResize,\n canResizeEvent,\n events,\n preventEventOverlap,\n canResizeEventTo,\n onEventPlacementRejected,\n withBackgroundEvents = false,\n}: UseEventResizeInput) {\n const [resizeState, setResizeState] = useState<ResizeState | null>(null);\n const [resizeValid, setResizeValid] = useState(true);\n const resizeRef = useRef<ResizeState | null>(null);\n const justResizedRef = useRef(false);\n const stableOnEventResize = useEffectEvent(onEventResize || (() => {}));\n const stableOnEventPlacementRejected = useEffectEvent(onEventPlacementRejected || (() => {}));\n\n const validateResize = useEffectEvent(\n (\n event: ScheduleEventData,\n start: DateTimeStringValue,\n end: DateTimeStringValue,\n edge: 'start' | 'end'\n ) =>\n validateEventResize({\n event,\n start,\n end,\n edge,\n events,\n preventEventOverlap,\n canResizeEventTo,\n })\n );\n\n const parsedStartTime = parseTimeString(startTime);\n const parsedEndTime = parseTimeString(endTime);\n const startMinutes = parsedStartTime.hours * 60 + parsedStartTime.minutes;\n const endMinutes = parsedEndTime.hours * 60 + parsedEndTime.minutes;\n const clampedInterval = clampIntervalMinutes(intervalMinutes);\n const clampedResizeInterval = clampIntervalMinutes(resizeIntervalMinutes ?? intervalMinutes);\n const literalRange = endMinutes - startMinutes;\n const totalMinutes = Math.ceil(literalRange / clampedInterval) * clampedInterval;\n\n // Snapping happens in absolute minutes from midnight – the same grid `calculateDropTime`\n // uses for drag and drop. Snapping relative to `startTime` would put resized edges on a\n // different grid than dragged events whenever `startTime` is not a multiple of the interval\n // (with `startTime=\"08:10\"` and a 30 minute step, drops land on :00/:30 but resizes on\n // :10/:40, so the two operations could never align an event).\n const clampAndSnap = useCallback(\n (minutes: number): number => {\n const snapped =\n Math.round((startMinutes + minutes) / clampedResizeInterval) * clampedResizeInterval;\n return Math.max(0, Math.min(literalRange, snapped - startMinutes));\n },\n [literalRange, clampedResizeInterval, startMinutes]\n );\n\n const percentToDateTime = useCallback(\n (percent: number, eventDate: string): DateTimeStringValue => {\n const minutes = (percent / 100) * totalMinutes;\n const snappedMinutes = clampAndSnap(minutes);\n const totalMins = startMinutes + snappedMinutes;\n const hours = Math.floor(totalMins / 60);\n const mins = totalMins % 60;\n return `${eventDate} ${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}:00`;\n },\n [totalMinutes, startMinutes, clampAndSnap]\n );\n\n const snapEdgeMinutes = useCallback(\n (minutes: number, direction: 'up' | 'down'): number => {\n const absolute = startMinutes + minutes;\n const snapped =\n direction === 'up'\n ? Math.ceil(absolute / clampedResizeInterval) * clampedResizeInterval\n : Math.floor(absolute / clampedResizeInterval) * clampedResizeInterval;\n return Math.max(0, Math.min(literalRange, snapped - startMinutes));\n },\n [literalRange, clampedResizeInterval, startMinutes]\n );\n\n const percentToMinutes = useCallback(\n (percent: number): number => (percent / 100) * totalMinutes,\n [totalMinutes]\n );\n\n const minutesToPercent = useCallback(\n (minutes: number): number => (minutes / totalMinutes) * 100,\n [totalMinutes]\n );\n\n const handleResizeStart = useCallback(\n ({\n event,\n edge,\n container,\n originalTop,\n originalHeight,\n eventDate,\n pointerEvent,\n }: {\n event: ScheduleEventData;\n edge: ResizeEdge;\n container: HTMLElement;\n originalTop: number;\n originalHeight: number;\n eventDate: string;\n pointerEvent: React.PointerEvent;\n }) => {\n if (!enabled || mode === 'static') {\n return;\n }\n\n pointerEvent.preventDefault();\n pointerEvent.stopPropagation();\n\n const state: ResizeState = {\n eventId: event.id,\n event,\n edge,\n container,\n originalTop,\n originalHeight,\n currentTop: originalTop,\n currentHeight: originalHeight,\n eventDate,\n originalStart: dayjs(event.start).format('YYYY-MM-DD HH:mm:ss'),\n originalEnd: dayjs(event.end).format('YYYY-MM-DD HH:mm:ss'),\n };\n\n resizeRef.current = state;\n setResizeState(state);\n setResizeValid(true);\n },\n [enabled, mode]\n );\n\n const getResizeRange = useCallback(\n (state: ResizeState) => ({\n start:\n state.edge === 'top'\n ? percentToDateTime(state.currentTop, state.eventDate)\n : state.originalStart,\n end:\n state.edge === 'top'\n ? state.originalEnd\n : percentToDateTime(state.currentTop + state.currentHeight, state.eventDate),\n edge: (state.edge === 'top' ? 'start' : 'end') as 'start' | 'end',\n }),\n [percentToDateTime]\n );\n\n const isResizing = resizeState !== null;\n\n useEffect(() => {\n if (!isResizing) {\n return undefined;\n }\n\n const savedUserSelect = document.body.style.userSelect;\n document.body.style.userSelect = 'none';\n\n const handlePointerMove = (e: PointerEvent) => {\n const state = resizeRef.current;\n if (!state) {\n return;\n }\n\n const containerRect = state.container.getBoundingClientRect();\n const relativeY = e.clientY - containerRect.top;\n const rawPercent = Math.max(0, Math.min(100, (relativeY / containerRect.height) * 100));\n const draggedMinutes = clampAndSnap(percentToMinutes(rawPercent));\n\n let newTop = state.originalTop;\n let newHeight = state.originalHeight;\n\n if (state.edge === 'bottom') {\n const topMinutes = percentToMinutes(state.originalTop);\n const minEndMinutes = snapEdgeMinutes(topMinutes + clampedResizeInterval, 'up');\n const endMinutes = Math.max(draggedMinutes, minEndMinutes);\n newHeight = Math.max(0, minutesToPercent(endMinutes) - state.originalTop);\n } else {\n const bottomMinutes = percentToMinutes(state.originalTop + state.originalHeight);\n const maxStartMinutes = snapEdgeMinutes(bottomMinutes - clampedResizeInterval, 'down');\n const startMinutesValue = Math.min(draggedMinutes, maxStartMinutes);\n newTop = minutesToPercent(startMinutesValue);\n newHeight = Math.max(0, state.originalTop + state.originalHeight - newTop);\n }\n\n resizeRef.current = { ...state, currentTop: newTop, currentHeight: newHeight };\n setResizeState(resizeRef.current);\n\n const range = getResizeRange(resizeRef.current);\n setResizeValid(validateResize(state.event, range.start, range.end, range.edge).valid);\n };\n\n const handlePointerUp = () => {\n const state = resizeRef.current;\n if (state) {\n if (\n state.currentTop !== state.originalTop ||\n state.currentHeight !== state.originalHeight\n ) {\n const { start: newStart, end: newEnd, edge } = getResizeRange(state);\n const { valid, conflicts, reason } = validateResize(state.event, newStart, newEnd, edge);\n\n if (valid) {\n stableOnEventResize({\n eventId: state.eventId,\n newStart,\n newEnd,\n event: state.event,\n });\n } else {\n stableOnEventPlacementRejected({\n action: 'resize',\n event: state.event,\n start: newStart,\n end: newEnd,\n edge,\n conflicts,\n reason: reason!,\n });\n }\n }\n }\n resizeRef.current = null;\n setResizeState(null);\n setResizeValid(true);\n justResizedRef.current = true;\n requestAnimationFrame(() => {\n justResizedRef.current = false;\n });\n };\n\n document.addEventListener('pointermove', handlePointerMove);\n document.addEventListener('pointerup', handlePointerUp);\n\n return () => {\n document.body.style.userSelect = savedUserSelect;\n document.removeEventListener('pointermove', handlePointerMove);\n document.removeEventListener('pointerup', handlePointerUp);\n };\n }, [isResizing]);\n\n const getResizePosition = useCallback(\n (eventId: string | number, eventDate?: string) => {\n if (!resizeState || resizeState.eventId !== eventId) {\n return null;\n }\n if (eventDate !== undefined && resizeState.eventDate !== eventDate) {\n return null;\n }\n return { top: resizeState.currentTop, height: resizeState.currentHeight };\n },\n [resizeState]\n );\n\n const isResizableEvent = useCallback(\n (event: ScheduleEventData) => {\n if (!enabled || mode === 'static') {\n return false;\n }\n if (event.display === 'background' && !withBackgroundEvents) {\n return false;\n }\n return canResizeEvent ? canResizeEvent(event) : true;\n },\n [enabled, mode, canResizeEvent, withBackgroundEvents]\n );\n\n const wasResizing = useCallback(() => justResizedRef.current, []);\n\n return {\n handleResizeStart,\n isResizing,\n resizingEventId: resizeState?.eventId ?? null,\n getResizePosition,\n isResizableEvent,\n wasResizing,\n resizeValid,\n };\n}\n"],"mappings":"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"}
|
|
@@ -2,14 +2,27 @@
|
|
|
2
2
|
import { clampIntervalMinutes } from "../utils/clamp-interval-minutes/clamp-interval-minutes.mjs";
|
|
3
3
|
import { parseTimeString } from "../utils/parse-time-string/parse-time-string.mjs";
|
|
4
4
|
import { getDayRelativePercent } from "../utils/get-day-relative-percent/get-day-relative-percent.mjs";
|
|
5
|
+
import { validateEventResize } from "./validate-event-resize.mjs";
|
|
5
6
|
import dayjs from "dayjs";
|
|
6
7
|
import { useCallback, useEffect, useEffectEvent, useRef, useState } from "react";
|
|
7
8
|
//#region packages/@mantine/schedule/src/hooks/use-horizontal-event-resize.ts
|
|
8
|
-
function useHorizontalEventResize({ enabled = false, mode = "default", startTime, endTime, intervalMinutes, resizeIntervalMinutes, onEventResize, canResizeEvent, withBackgroundEvents = false }) {
|
|
9
|
+
function useHorizontalEventResize({ enabled = false, mode = "default", startTime, endTime, intervalMinutes, resizeIntervalMinutes, onEventResize, canResizeEvent, events, preventEventOverlap, canResizeEventTo, onEventPlacementRejected, withBackgroundEvents = false }) {
|
|
9
10
|
const [resizeState, setResizeState] = useState(null);
|
|
11
|
+
const [resizeValid, setResizeValid] = useState(true);
|
|
10
12
|
const resizeRef = useRef(null);
|
|
11
13
|
const justResizedRef = useRef(false);
|
|
12
14
|
const stableOnEventResize = useEffectEvent(onEventResize || (() => {}));
|
|
15
|
+
const stableOnEventPlacementRejected = useEffectEvent(onEventPlacementRejected || (() => {}));
|
|
16
|
+
const validateResize = useEffectEvent((event, start, end, edge) => validateEventResize({
|
|
17
|
+
event,
|
|
18
|
+
start,
|
|
19
|
+
end,
|
|
20
|
+
edge,
|
|
21
|
+
events,
|
|
22
|
+
preventEventOverlap,
|
|
23
|
+
canResizeEventTo,
|
|
24
|
+
resourceId: event.resourceId
|
|
25
|
+
}));
|
|
13
26
|
const parsedStartTime = parseTimeString(startTime);
|
|
14
27
|
const parsedEndTime = parseTimeString(endTime);
|
|
15
28
|
const startMinutes = parsedStartTime.hours * 60 + parsedStartTime.minutes;
|
|
@@ -70,7 +83,12 @@ function useHorizontalEventResize({ enabled = false, mode = "default", startTime
|
|
|
70
83
|
};
|
|
71
84
|
resizeRef.current = state;
|
|
72
85
|
setResizeState(state);
|
|
86
|
+
setResizeValid(true);
|
|
73
87
|
}, [enabled, mode]);
|
|
88
|
+
const getResizeRange = useCallback((state) => ({
|
|
89
|
+
start: state.edge === "start" ? percentToDateTime(state.currentLeft, state.eventDate) : state.originalStart,
|
|
90
|
+
end: state.edge === "start" ? state.originalEnd : percentToDateTime(state.currentLeft + state.currentWidth, state.eventDate)
|
|
91
|
+
}), [percentToDateTime]);
|
|
74
92
|
const isResizing = resizeState !== null;
|
|
75
93
|
useEffect(() => {
|
|
76
94
|
if (!isResizing) return;
|
|
@@ -108,30 +126,36 @@ function useHorizontalEventResize({ enabled = false, mode = "default", startTime
|
|
|
108
126
|
currentWidth: newWidth
|
|
109
127
|
};
|
|
110
128
|
setResizeState(resizeRef.current);
|
|
129
|
+
const range = getResizeRange(resizeRef.current);
|
|
130
|
+
setResizeValid(validateResize(state.event, range.start, range.end, state.edge).valid);
|
|
111
131
|
};
|
|
112
132
|
const handlePointerUp = () => {
|
|
113
133
|
const state = resizeRef.current;
|
|
114
134
|
if (state) {
|
|
115
135
|
if (state.currentLeft !== state.originalLeft || state.currentWidth !== state.originalWidth) {
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
if (
|
|
119
|
-
newStart = percentToDateTime(state.currentLeft, state.eventDate);
|
|
120
|
-
newEnd = state.originalEnd;
|
|
121
|
-
} else {
|
|
122
|
-
newStart = state.originalStart;
|
|
123
|
-
newEnd = percentToDateTime(state.currentLeft + state.currentWidth, state.eventDate);
|
|
124
|
-
}
|
|
125
|
-
stableOnEventResize({
|
|
136
|
+
const { start: newStart, end: newEnd } = getResizeRange(state);
|
|
137
|
+
const { valid, conflicts, reason } = validateResize(state.event, newStart, newEnd, state.edge);
|
|
138
|
+
if (valid) stableOnEventResize({
|
|
126
139
|
eventId: state.eventId,
|
|
127
140
|
newStart,
|
|
128
141
|
newEnd,
|
|
129
142
|
event: state.event
|
|
130
143
|
});
|
|
144
|
+
else stableOnEventPlacementRejected({
|
|
145
|
+
action: "resize",
|
|
146
|
+
event: state.event,
|
|
147
|
+
start: newStart,
|
|
148
|
+
end: newEnd,
|
|
149
|
+
edge: state.edge,
|
|
150
|
+
resourceId: state.event.resourceId,
|
|
151
|
+
conflicts,
|
|
152
|
+
reason
|
|
153
|
+
});
|
|
131
154
|
}
|
|
132
155
|
}
|
|
133
156
|
resizeRef.current = null;
|
|
134
157
|
setResizeState(null);
|
|
158
|
+
setResizeValid(true);
|
|
135
159
|
justResizedRef.current = true;
|
|
136
160
|
requestAnimationFrame(() => {
|
|
137
161
|
justResizedRef.current = false;
|
|
@@ -172,7 +196,8 @@ function useHorizontalEventResize({ enabled = false, mode = "default", startTime
|
|
|
172
196
|
resizingEdge: resizeState?.edge ?? null,
|
|
173
197
|
getResizePosition,
|
|
174
198
|
isResizableEvent,
|
|
175
|
-
wasResizing
|
|
199
|
+
wasResizing,
|
|
200
|
+
resizeValid
|
|
176
201
|
};
|
|
177
202
|
}
|
|
178
203
|
//#endregion
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-horizontal-event-resize.mjs","names":[],"sources":["../../src/hooks/use-horizontal-event-resize.ts"],"sourcesContent":["import dayjs from 'dayjs';\nimport { useCallback, useEffect, useEffectEvent, useRef, useState } from 'react';\nimport { DateTimeStringValue, ScheduleEventData, ScheduleMode } from '../types';\nimport { clampIntervalMinutes } from '../utils/clamp-interval-minutes/clamp-interval-minutes';\nimport { getDayRelativePercent } from '../utils/get-day-relative-percent/get-day-relative-percent';\nimport { parseTimeString } from '../utils/parse-time-string/parse-time-string';\n\ntype ResizeEdge = 'start' | 'end';\n\ninterface ResizeState {\n eventId: string | number;\n event: ScheduleEventData;\n edge: ResizeEdge;\n container: HTMLElement;\n originalLeft: number;\n originalWidth: number;\n currentLeft: number;\n currentWidth: number;\n eventDate: string;\n originalStart: DateTimeStringValue;\n originalEnd: DateTimeStringValue;\n dayIndex: number;\n dayCount: number;\n}\n\nexport interface UseHorizontalEventResizeInput {\n enabled?: boolean;\n mode?: ScheduleMode;\n startTime: string;\n endTime: string;\n intervalMinutes: number;\n resizeIntervalMinutes?: number;\n onEventResize?: (data: {\n eventId: string | number;\n newStart: DateTimeStringValue;\n newEnd: DateTimeStringValue;\n event: ScheduleEventData;\n }) => void;\n canResizeEvent?: (event: ScheduleEventData) => boolean;\n withBackgroundEvents?: boolean;\n}\n\nexport function useHorizontalEventResize({\n enabled = false,\n mode = 'default',\n startTime,\n endTime,\n intervalMinutes,\n resizeIntervalMinutes,\n onEventResize,\n canResizeEvent,\n withBackgroundEvents = false,\n}: UseHorizontalEventResizeInput) {\n const [resizeState, setResizeState] = useState<ResizeState | null>(null);\n const resizeRef = useRef<ResizeState | null>(null);\n const justResizedRef = useRef(false);\n const stableOnEventResize = useEffectEvent(onEventResize || (() => {}));\n\n const parsedStartTime = parseTimeString(startTime);\n const parsedEndTime = parseTimeString(endTime);\n const startMinutes = parsedStartTime.hours * 60 + parsedStartTime.minutes;\n const endMinutes = parsedEndTime.hours * 60 + parsedEndTime.minutes;\n const clampedInterval = clampIntervalMinutes(intervalMinutes);\n const clampedResizeInterval = clampIntervalMinutes(resizeIntervalMinutes ?? intervalMinutes);\n const literalRange = endMinutes - startMinutes;\n const totalMinutes = Math.ceil(literalRange / clampedInterval) * clampedInterval;\n\n // Snapped in absolute minutes from midnight so resized edges land on the same grid as\n // dragged events – see the matching comment in `use-event-resize`.\n const clampAndSnap = useCallback(\n (minutes: number): number => {\n const snapped =\n Math.round((startMinutes + minutes) / clampedResizeInterval) * clampedResizeInterval;\n return Math.max(0, Math.min(literalRange, snapped - startMinutes));\n },\n [literalRange, clampedResizeInterval, startMinutes]\n );\n\n const percentToDateTime = useCallback(\n (percent: number, eventDate: string): DateTimeStringValue => {\n const minutes = (percent / 100) * totalMinutes;\n const snappedMinutes = clampAndSnap(minutes);\n const totalMins = startMinutes + snappedMinutes;\n const hours = Math.floor(totalMins / 60);\n const mins = totalMins % 60;\n return `${eventDate} ${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}:00`;\n },\n [totalMinutes, startMinutes, clampAndSnap]\n );\n\n const snapEdgeMinutes = useCallback(\n (minutes: number, direction: 'up' | 'down'): number => {\n const absolute = startMinutes + minutes;\n const snapped =\n direction === 'up'\n ? Math.ceil(absolute / clampedResizeInterval) * clampedResizeInterval\n : Math.floor(absolute / clampedResizeInterval) * clampedResizeInterval;\n return Math.max(0, Math.min(literalRange, snapped - startMinutes));\n },\n [literalRange, clampedResizeInterval, startMinutes]\n );\n\n const percentToMinutes = useCallback(\n (percent: number): number => (percent / 100) * totalMinutes,\n [totalMinutes]\n );\n\n const minutesToPercent = useCallback(\n (minutes: number): number => (minutes / totalMinutes) * 100,\n [totalMinutes]\n );\n\n const handleResizeStart = useCallback(\n ({\n event,\n edge,\n container,\n originalLeft,\n originalWidth,\n eventDate,\n dayIndex = 0,\n dayCount = 1,\n pointerEvent,\n }: {\n event: ScheduleEventData;\n edge: ResizeEdge;\n container: HTMLElement;\n originalLeft: number;\n originalWidth: number;\n eventDate: string;\n dayIndex?: number;\n dayCount?: number;\n pointerEvent: React.PointerEvent;\n }) => {\n if (!enabled || mode === 'static') {\n return;\n }\n\n pointerEvent.preventDefault();\n pointerEvent.stopPropagation();\n\n const state: ResizeState = {\n eventId: event.id,\n event,\n edge,\n container,\n originalLeft,\n originalWidth,\n currentLeft: originalLeft,\n currentWidth: originalWidth,\n eventDate,\n originalStart: dayjs(event.start).format('YYYY-MM-DD HH:mm:ss'),\n originalEnd: dayjs(event.end).format('YYYY-MM-DD HH:mm:ss'),\n dayIndex,\n dayCount,\n };\n\n resizeRef.current = state;\n setResizeState(state);\n },\n [enabled, mode]\n );\n\n const isResizing = resizeState !== null;\n\n useEffect(() => {\n if (!isResizing) {\n return undefined;\n }\n\n const savedUserSelect = document.body.style.userSelect;\n const savedCursor = document.body.style.cursor;\n document.body.style.userSelect = 'none';\n document.body.style.cursor = 'ew-resize';\n\n const handlePointerMove = (e: PointerEvent) => {\n const state = resizeRef.current;\n if (!state) {\n return;\n }\n\n const containerRect = state.container.getBoundingClientRect();\n const rawPercent = getDayRelativePercent({\n clientX: e.clientX,\n containerLeft: containerRect.left,\n containerWidth: containerRect.width,\n dayIndex: state.dayIndex,\n dayCount: state.dayCount,\n });\n const draggedMinutes = clampAndSnap(percentToMinutes(rawPercent));\n\n let newLeft = state.originalLeft;\n let newWidth = state.originalWidth;\n\n if (state.edge === 'end') {\n const leftMinutes = percentToMinutes(state.originalLeft);\n const minEndMinutes = snapEdgeMinutes(leftMinutes + clampedResizeInterval, 'up');\n const endMinutes = Math.max(draggedMinutes, minEndMinutes);\n newWidth = Math.max(0, minutesToPercent(endMinutes) - state.originalLeft);\n } else {\n const rightMinutes = percentToMinutes(state.originalLeft + state.originalWidth);\n const maxStartMinutes = snapEdgeMinutes(rightMinutes - clampedResizeInterval, 'down');\n const startMinutesValue = Math.min(draggedMinutes, maxStartMinutes);\n newLeft = minutesToPercent(startMinutesValue);\n newWidth = Math.max(0, state.originalLeft + state.originalWidth - newLeft);\n }\n\n resizeRef.current = { ...state, currentLeft: newLeft, currentWidth: newWidth };\n setResizeState(resizeRef.current);\n };\n\n const handlePointerUp = () => {\n const state = resizeRef.current;\n if (state) {\n if (\n state.currentLeft !== state.originalLeft ||\n state.currentWidth !== state.originalWidth\n ) {\n let newStart: DateTimeStringValue;\n let newEnd: DateTimeStringValue;\n\n if (state.edge === 'start') {\n newStart = percentToDateTime(state.currentLeft, state.eventDate);\n newEnd = state.originalEnd;\n } else {\n newStart = state.originalStart;\n newEnd = percentToDateTime(state.currentLeft + state.currentWidth, state.eventDate);\n }\n\n stableOnEventResize({\n eventId: state.eventId,\n newStart,\n newEnd,\n event: state.event,\n });\n }\n }\n resizeRef.current = null;\n setResizeState(null);\n justResizedRef.current = true;\n requestAnimationFrame(() => {\n justResizedRef.current = false;\n });\n };\n\n document.addEventListener('pointermove', handlePointerMove);\n document.addEventListener('pointerup', handlePointerUp);\n\n return () => {\n document.body.style.userSelect = savedUserSelect;\n document.body.style.cursor = savedCursor;\n document.removeEventListener('pointermove', handlePointerMove);\n document.removeEventListener('pointerup', handlePointerUp);\n };\n }, [isResizing]);\n\n const getResizePosition = useCallback(\n (eventId: string | number, eventDate?: string) => {\n if (!resizeState || resizeState.eventId !== eventId) {\n return null;\n }\n if (eventDate !== undefined && resizeState.eventDate !== eventDate) {\n return null;\n }\n return { left: resizeState.currentLeft, width: resizeState.currentWidth };\n },\n [resizeState]\n );\n\n const isResizableEvent = useCallback(\n (event: ScheduleEventData) => {\n if (!enabled || mode === 'static') {\n return false;\n }\n if (event.display === 'background' && !withBackgroundEvents) {\n return false;\n }\n return canResizeEvent ? canResizeEvent(event) : true;\n },\n [enabled, mode, canResizeEvent, withBackgroundEvents]\n );\n\n const wasResizing = useCallback(() => justResizedRef.current, []);\n\n return {\n handleResizeStart,\n isResizing,\n resizingEventId: resizeState?.eventId ?? null,\n resizingEdge: resizeState?.edge ?? null,\n getResizePosition,\n isResizableEvent,\n wasResizing,\n };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"use-horizontal-event-resize.mjs","names":[],"sources":["../../src/hooks/use-horizontal-event-resize.ts"],"sourcesContent":["import dayjs from 'dayjs';\nimport { useCallback, useEffect, useEffectEvent, useRef, useState } from 'react';\nimport { DateTimeStringValue, ScheduleEventData, ScheduleMode } from '../types';\nimport { clampIntervalMinutes } from '../utils/clamp-interval-minutes/clamp-interval-minutes';\nimport { getDayRelativePercent } from '../utils/get-day-relative-percent/get-day-relative-percent';\nimport { parseTimeString } from '../utils/parse-time-string/parse-time-string';\nimport { EventResizeValidationOptions, validateEventResize } from './validate-event-resize';\n\ntype ResizeEdge = 'start' | 'end';\n\ninterface ResizeState {\n eventId: string | number;\n event: ScheduleEventData;\n edge: ResizeEdge;\n container: HTMLElement;\n originalLeft: number;\n originalWidth: number;\n currentLeft: number;\n currentWidth: number;\n eventDate: string;\n originalStart: DateTimeStringValue;\n originalEnd: DateTimeStringValue;\n dayIndex: number;\n dayCount: number;\n}\n\nexport interface UseHorizontalEventResizeInput extends EventResizeValidationOptions {\n enabled?: boolean;\n mode?: ScheduleMode;\n startTime: string;\n endTime: string;\n intervalMinutes: number;\n resizeIntervalMinutes?: number;\n onEventResize?: (data: {\n eventId: string | number;\n newStart: DateTimeStringValue;\n newEnd: DateTimeStringValue;\n event: ScheduleEventData;\n }) => void;\n canResizeEvent?: (event: ScheduleEventData) => boolean;\n withBackgroundEvents?: boolean;\n}\n\nexport function useHorizontalEventResize({\n enabled = false,\n mode = 'default',\n startTime,\n endTime,\n intervalMinutes,\n resizeIntervalMinutes,\n onEventResize,\n canResizeEvent,\n events,\n preventEventOverlap,\n canResizeEventTo,\n onEventPlacementRejected,\n withBackgroundEvents = false,\n}: UseHorizontalEventResizeInput) {\n const [resizeState, setResizeState] = useState<ResizeState | null>(null);\n const [resizeValid, setResizeValid] = useState(true);\n const resizeRef = useRef<ResizeState | null>(null);\n const justResizedRef = useRef(false);\n const stableOnEventResize = useEffectEvent(onEventResize || (() => {}));\n const stableOnEventPlacementRejected = useEffectEvent(onEventPlacementRejected || (() => {}));\n\n const validateResize = useEffectEvent(\n (\n event: ScheduleEventData,\n start: DateTimeStringValue,\n end: DateTimeStringValue,\n edge: 'start' | 'end'\n ) =>\n validateEventResize({\n event,\n start,\n end,\n edge,\n events,\n preventEventOverlap,\n canResizeEventTo,\n resourceId: event.resourceId,\n })\n );\n\n const parsedStartTime = parseTimeString(startTime);\n const parsedEndTime = parseTimeString(endTime);\n const startMinutes = parsedStartTime.hours * 60 + parsedStartTime.minutes;\n const endMinutes = parsedEndTime.hours * 60 + parsedEndTime.minutes;\n const clampedInterval = clampIntervalMinutes(intervalMinutes);\n const clampedResizeInterval = clampIntervalMinutes(resizeIntervalMinutes ?? intervalMinutes);\n const literalRange = endMinutes - startMinutes;\n const totalMinutes = Math.ceil(literalRange / clampedInterval) * clampedInterval;\n\n // Snapped in absolute minutes from midnight so resized edges land on the same grid as\n // dragged events – see the matching comment in `use-event-resize`.\n const clampAndSnap = useCallback(\n (minutes: number): number => {\n const snapped =\n Math.round((startMinutes + minutes) / clampedResizeInterval) * clampedResizeInterval;\n return Math.max(0, Math.min(literalRange, snapped - startMinutes));\n },\n [literalRange, clampedResizeInterval, startMinutes]\n );\n\n const percentToDateTime = useCallback(\n (percent: number, eventDate: string): DateTimeStringValue => {\n const minutes = (percent / 100) * totalMinutes;\n const snappedMinutes = clampAndSnap(minutes);\n const totalMins = startMinutes + snappedMinutes;\n const hours = Math.floor(totalMins / 60);\n const mins = totalMins % 60;\n return `${eventDate} ${String(hours).padStart(2, '0')}:${String(mins).padStart(2, '0')}:00`;\n },\n [totalMinutes, startMinutes, clampAndSnap]\n );\n\n const snapEdgeMinutes = useCallback(\n (minutes: number, direction: 'up' | 'down'): number => {\n const absolute = startMinutes + minutes;\n const snapped =\n direction === 'up'\n ? Math.ceil(absolute / clampedResizeInterval) * clampedResizeInterval\n : Math.floor(absolute / clampedResizeInterval) * clampedResizeInterval;\n return Math.max(0, Math.min(literalRange, snapped - startMinutes));\n },\n [literalRange, clampedResizeInterval, startMinutes]\n );\n\n const percentToMinutes = useCallback(\n (percent: number): number => (percent / 100) * totalMinutes,\n [totalMinutes]\n );\n\n const minutesToPercent = useCallback(\n (minutes: number): number => (minutes / totalMinutes) * 100,\n [totalMinutes]\n );\n\n const handleResizeStart = useCallback(\n ({\n event,\n edge,\n container,\n originalLeft,\n originalWidth,\n eventDate,\n dayIndex = 0,\n dayCount = 1,\n pointerEvent,\n }: {\n event: ScheduleEventData;\n edge: ResizeEdge;\n container: HTMLElement;\n originalLeft: number;\n originalWidth: number;\n eventDate: string;\n dayIndex?: number;\n dayCount?: number;\n pointerEvent: React.PointerEvent;\n }) => {\n if (!enabled || mode === 'static') {\n return;\n }\n\n pointerEvent.preventDefault();\n pointerEvent.stopPropagation();\n\n const state: ResizeState = {\n eventId: event.id,\n event,\n edge,\n container,\n originalLeft,\n originalWidth,\n currentLeft: originalLeft,\n currentWidth: originalWidth,\n eventDate,\n originalStart: dayjs(event.start).format('YYYY-MM-DD HH:mm:ss'),\n originalEnd: dayjs(event.end).format('YYYY-MM-DD HH:mm:ss'),\n dayIndex,\n dayCount,\n };\n\n resizeRef.current = state;\n setResizeState(state);\n setResizeValid(true);\n },\n [enabled, mode]\n );\n\n const getResizeRange = useCallback(\n (state: ResizeState) => ({\n start:\n state.edge === 'start'\n ? percentToDateTime(state.currentLeft, state.eventDate)\n : state.originalStart,\n end:\n state.edge === 'start'\n ? state.originalEnd\n : percentToDateTime(state.currentLeft + state.currentWidth, state.eventDate),\n }),\n [percentToDateTime]\n );\n\n const isResizing = resizeState !== null;\n\n useEffect(() => {\n if (!isResizing) {\n return undefined;\n }\n\n const savedUserSelect = document.body.style.userSelect;\n const savedCursor = document.body.style.cursor;\n document.body.style.userSelect = 'none';\n document.body.style.cursor = 'ew-resize';\n\n const handlePointerMove = (e: PointerEvent) => {\n const state = resizeRef.current;\n if (!state) {\n return;\n }\n\n const containerRect = state.container.getBoundingClientRect();\n const rawPercent = getDayRelativePercent({\n clientX: e.clientX,\n containerLeft: containerRect.left,\n containerWidth: containerRect.width,\n dayIndex: state.dayIndex,\n dayCount: state.dayCount,\n });\n const draggedMinutes = clampAndSnap(percentToMinutes(rawPercent));\n\n let newLeft = state.originalLeft;\n let newWidth = state.originalWidth;\n\n if (state.edge === 'end') {\n const leftMinutes = percentToMinutes(state.originalLeft);\n const minEndMinutes = snapEdgeMinutes(leftMinutes + clampedResizeInterval, 'up');\n const endMinutes = Math.max(draggedMinutes, minEndMinutes);\n newWidth = Math.max(0, minutesToPercent(endMinutes) - state.originalLeft);\n } else {\n const rightMinutes = percentToMinutes(state.originalLeft + state.originalWidth);\n const maxStartMinutes = snapEdgeMinutes(rightMinutes - clampedResizeInterval, 'down');\n const startMinutesValue = Math.min(draggedMinutes, maxStartMinutes);\n newLeft = minutesToPercent(startMinutesValue);\n newWidth = Math.max(0, state.originalLeft + state.originalWidth - newLeft);\n }\n\n resizeRef.current = { ...state, currentLeft: newLeft, currentWidth: newWidth };\n setResizeState(resizeRef.current);\n\n const range = getResizeRange(resizeRef.current);\n setResizeValid(validateResize(state.event, range.start, range.end, state.edge).valid);\n };\n\n const handlePointerUp = () => {\n const state = resizeRef.current;\n if (state) {\n if (\n state.currentLeft !== state.originalLeft ||\n state.currentWidth !== state.originalWidth\n ) {\n const { start: newStart, end: newEnd } = getResizeRange(state);\n const { valid, conflicts, reason } = validateResize(\n state.event,\n newStart,\n newEnd,\n state.edge\n );\n\n if (valid) {\n stableOnEventResize({\n eventId: state.eventId,\n newStart,\n newEnd,\n event: state.event,\n });\n } else {\n stableOnEventPlacementRejected({\n action: 'resize',\n event: state.event,\n start: newStart,\n end: newEnd,\n edge: state.edge,\n resourceId: state.event.resourceId,\n conflicts,\n reason: reason!,\n });\n }\n }\n }\n resizeRef.current = null;\n setResizeState(null);\n setResizeValid(true);\n justResizedRef.current = true;\n requestAnimationFrame(() => {\n justResizedRef.current = false;\n });\n };\n\n document.addEventListener('pointermove', handlePointerMove);\n document.addEventListener('pointerup', handlePointerUp);\n\n return () => {\n document.body.style.userSelect = savedUserSelect;\n document.body.style.cursor = savedCursor;\n document.removeEventListener('pointermove', handlePointerMove);\n document.removeEventListener('pointerup', handlePointerUp);\n };\n }, [isResizing]);\n\n const getResizePosition = useCallback(\n (eventId: string | number, eventDate?: string) => {\n if (!resizeState || resizeState.eventId !== eventId) {\n return null;\n }\n if (eventDate !== undefined && resizeState.eventDate !== eventDate) {\n return null;\n }\n return { left: resizeState.currentLeft, width: resizeState.currentWidth };\n },\n [resizeState]\n );\n\n const isResizableEvent = useCallback(\n (event: ScheduleEventData) => {\n if (!enabled || mode === 'static') {\n return false;\n }\n if (event.display === 'background' && !withBackgroundEvents) {\n return false;\n }\n return canResizeEvent ? canResizeEvent(event) : true;\n },\n [enabled, mode, canResizeEvent, withBackgroundEvents]\n );\n\n const wasResizing = useCallback(() => justResizedRef.current, []);\n\n return {\n handleResizeStart,\n isResizing,\n resizingEventId: resizeState?.eventId ?? null,\n resizingEdge: resizeState?.edge ?? null,\n getResizePosition,\n isResizableEvent,\n wasResizing,\n resizeValid,\n };\n}\n"],"mappings":"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"}
|