react-timelane 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/static.yml +55 -0
- package/README.md +54 -0
- package/docs/README.md +54 -0
- package/docs/eslint.config.js +28 -0
- package/docs/index.html +12 -0
- package/docs/package-lock.json +5101 -0
- package/docs/package.json +35 -0
- package/docs/src/App.css +5 -0
- package/docs/src/App.tsx +59 -0
- package/docs/src/assets/react.svg +1 -0
- package/docs/src/components/AllocationComponent.tsx +82 -0
- package/docs/src/components/Timeline.tsx +183 -0
- package/docs/src/constants.ts +42 -0
- package/docs/src/hooks/useLocalStorage.ts +21 -0
- package/docs/src/main.tsx +9 -0
- package/docs/src/models/Allocation.ts +11 -0
- package/docs/src/models/AllocationId.ts +1 -0
- package/docs/src/models/Resource.ts +8 -0
- package/docs/src/models/ResourceId.ts +1 -0
- package/docs/src/vite-env.d.ts +1 -0
- package/docs/tsconfig.json +27 -0
- package/docs/vite.config.ts +8 -0
- package/eslint.config.js +28 -0
- package/index.html +13 -0
- package/package.json +43 -0
- package/src/components/Timeline.scss +297 -0
- package/src/components/TimelineAside.tsx +81 -0
- package/src/components/TimelineBackground.tsx +53 -0
- package/src/components/TimelineBody.tsx +54 -0
- package/src/components/TimelineHeader/DaysHeader.tsx +44 -0
- package/src/components/TimelineHeader/MonthsHeader.tsx +62 -0
- package/src/components/TimelineHeader/TimelineHeader.tsx +64 -0
- package/src/components/TimelineHeader/WeeksHeader.tsx +63 -0
- package/src/components/TimelineHeader/index.ts +9 -0
- package/src/components/TimelineHeader/renderingUtils.tsx +57 -0
- package/src/components/TimelineSelectionLayer.tsx +179 -0
- package/src/components/TimelineSettingsContext.tsx +27 -0
- package/src/components/TimelineSettingsProvider.tsx +24 -0
- package/src/components/TimelineWrapper.tsx +51 -0
- package/src/components/core/CoreItem/CoreItemComponent.tsx +69 -0
- package/src/components/core/CoreItem/DragResizeComponent.tsx +180 -0
- package/src/components/core/CoreSwimlane/AvailableSpaceIndicator.tsx +156 -0
- package/src/components/core/CoreSwimlane/CoreSwimlane.tsx +245 -0
- package/src/components/core/CoreSwimlane/DropPreview.tsx +30 -0
- package/src/components/core/CoreSwimlane/DropTarget.tsx +83 -0
- package/src/components/core/CoreSwimlane/OverlapIndicator.tsx +22 -0
- package/src/components/core/CoreSwimlane/utils.ts +375 -0
- package/src/components/core/utils.ts +154 -0
- package/src/components/layout/TimelineLayout.tsx +93 -0
- package/src/components/layout/layout.scss +107 -0
- package/src/global.d.ts +9 -0
- package/src/hooks/useScroll.tsx +71 -0
- package/src/hooks/useTimelineContext.tsx +6 -0
- package/src/index.ts +15 -0
- package/src/types/AvailableSpace.ts +6 -0
- package/src/types/CoreItem.ts +23 -0
- package/src/types/DateBounds.ts +4 -0
- package/src/types/Dimensions.ts +4 -0
- package/src/types/GrabInfo.ts +6 -0
- package/src/types/Grid.ts +6 -0
- package/src/types/ItemId.ts +1 -0
- package/src/types/OffsetBounds.ts +4 -0
- package/src/types/Pixels.ts +4 -0
- package/src/types/Position.ts +4 -0
- package/src/types/Rectangle.ts +6 -0
- package/src/types/SwimlaneId.ts +1 -0
- package/src/types/SwimlaneT.ts +6 -0
- package/src/types/TimeRange.ts +4 -0
- package/src/types/TimelineSettings.ts +11 -0
- package/src/types/index.ts +15 -0
- package/tsconfig.json +32 -0
- package/vite.config.ts +32 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { PropsWithChildren, useEffect, useState } from "react";
|
|
2
|
+
import { dateToPixel, offsetToPixel } from "../utils";
|
|
3
|
+
import { getAvailableSpace } from "./utils";
|
|
4
|
+
import { addDays, setHours } from "date-fns";
|
|
5
|
+
import {
|
|
6
|
+
AvailableSpace,
|
|
7
|
+
CoreItem,
|
|
8
|
+
Pixels,
|
|
9
|
+
Rectangle,
|
|
10
|
+
SwimlaneT,
|
|
11
|
+
TimeRange,
|
|
12
|
+
} from "../../../types";
|
|
13
|
+
|
|
14
|
+
interface AvailableSpaceIndicatorProps<T> {
|
|
15
|
+
pixels: Pixels;
|
|
16
|
+
range: TimeRange;
|
|
17
|
+
swimlane: SwimlaneT;
|
|
18
|
+
debug: boolean;
|
|
19
|
+
items: CoreItem<T>[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A component that displays the available space at the mouse cursor position.
|
|
24
|
+
* The available space is a rectangle that does not overlap with it's surrounding items.
|
|
25
|
+
* It is determined by some heuristics.
|
|
26
|
+
*
|
|
27
|
+
* Only used for debugging purposes.
|
|
28
|
+
*
|
|
29
|
+
* @param param0
|
|
30
|
+
* @returns
|
|
31
|
+
*/
|
|
32
|
+
export default function AvailableSpaceIndicator<T>({
|
|
33
|
+
pixels,
|
|
34
|
+
range,
|
|
35
|
+
swimlane,
|
|
36
|
+
items,
|
|
37
|
+
debug = false,
|
|
38
|
+
children,
|
|
39
|
+
}: PropsWithChildren<AvailableSpaceIndicatorProps<T>>) {
|
|
40
|
+
const [availableSpace, setAvailableSpace] = useState<AvailableSpace | null>(
|
|
41
|
+
null
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
function getDate(e: React.MouseEvent) {
|
|
45
|
+
const clientRect = e.currentTarget.getBoundingClientRect();
|
|
46
|
+
return setHours(
|
|
47
|
+
addDays(range.start, (e.clientX - clientRect.left) / pixels.pixelsPerDay),
|
|
48
|
+
12
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function getOffset(e: React.MouseEvent) {
|
|
53
|
+
const clientRect = e.currentTarget.getBoundingClientRect();
|
|
54
|
+
const relativePxOffsetY = (e.pageY - clientRect.top) / clientRect.height;
|
|
55
|
+
|
|
56
|
+
return relativePxOffsetY * swimlane.capacity;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<div
|
|
61
|
+
style={{
|
|
62
|
+
width: "100%",
|
|
63
|
+
height: "100%",
|
|
64
|
+
position: "relative",
|
|
65
|
+
}}
|
|
66
|
+
onMouseMove={(e) => {
|
|
67
|
+
if (!debug) return;
|
|
68
|
+
|
|
69
|
+
const clickedDate = getDate(e);
|
|
70
|
+
const clickedOffset = getOffset(e);
|
|
71
|
+
|
|
72
|
+
const availableSpace: AvailableSpace | null = getAvailableSpace(
|
|
73
|
+
clickedDate,
|
|
74
|
+
clickedOffset,
|
|
75
|
+
swimlane,
|
|
76
|
+
items,
|
|
77
|
+
range
|
|
78
|
+
);
|
|
79
|
+
|
|
80
|
+
setAvailableSpace(availableSpace);
|
|
81
|
+
}}
|
|
82
|
+
onMouseLeave={() => {
|
|
83
|
+
if (!debug) return;
|
|
84
|
+
setAvailableSpace(null);
|
|
85
|
+
}}
|
|
86
|
+
>
|
|
87
|
+
{children}
|
|
88
|
+
<AvailableSpaceIndicatorItem
|
|
89
|
+
availableSpace={availableSpace}
|
|
90
|
+
pixels={pixels}
|
|
91
|
+
swimlane={swimlane}
|
|
92
|
+
range={range}
|
|
93
|
+
/>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface AvailableSpaceIndicatorItemProps {
|
|
99
|
+
availableSpace: AvailableSpace | null;
|
|
100
|
+
range: TimeRange;
|
|
101
|
+
pixels: Pixels;
|
|
102
|
+
swimlane: SwimlaneT;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function AvailableSpaceIndicatorItem({
|
|
106
|
+
availableSpace,
|
|
107
|
+
range,
|
|
108
|
+
pixels,
|
|
109
|
+
swimlane,
|
|
110
|
+
}: AvailableSpaceIndicatorItemProps) {
|
|
111
|
+
const [hoverRect, setHoverRect] = useState<Rectangle | null>(null);
|
|
112
|
+
|
|
113
|
+
useEffect(() => {
|
|
114
|
+
if (!availableSpace) {
|
|
115
|
+
setHoverRect(null);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const x = dateToPixel(availableSpace.start, range.start, pixels);
|
|
120
|
+
|
|
121
|
+
const width = dateToPixel(availableSpace.end, range.start, pixels) - x;
|
|
122
|
+
|
|
123
|
+
const y = offsetToPixel(
|
|
124
|
+
availableSpace.minOffset,
|
|
125
|
+
swimlane.capacity,
|
|
126
|
+
pixels
|
|
127
|
+
);
|
|
128
|
+
const height =
|
|
129
|
+
offsetToPixel(availableSpace.maxOffset, swimlane.capacity, pixels) - y;
|
|
130
|
+
|
|
131
|
+
setHoverRect({
|
|
132
|
+
x,
|
|
133
|
+
y,
|
|
134
|
+
width,
|
|
135
|
+
height,
|
|
136
|
+
});
|
|
137
|
+
}, [availableSpace, pixels, range.start, swimlane.capacity]);
|
|
138
|
+
|
|
139
|
+
return (
|
|
140
|
+
<div
|
|
141
|
+
style={
|
|
142
|
+
hoverRect
|
|
143
|
+
? {
|
|
144
|
+
position: "absolute",
|
|
145
|
+
top: `${hoverRect.y}px`,
|
|
146
|
+
left: `${hoverRect.x + 1}px`,
|
|
147
|
+
width: `${hoverRect.width - 1}px`,
|
|
148
|
+
height: `${hoverRect.height - 1}px`,
|
|
149
|
+
background: "purple",
|
|
150
|
+
opacity: 0.3,
|
|
151
|
+
}
|
|
152
|
+
: {}
|
|
153
|
+
}
|
|
154
|
+
></div>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import DropTarget from "./DropTarget";
|
|
2
|
+
import DropPreview from "./DropPreview";
|
|
3
|
+
import { format } from "date-fns";
|
|
4
|
+
|
|
5
|
+
import { MouseEvent, ReactElement, useState } from "react";
|
|
6
|
+
import { addDays, setHours } from "date-fns";
|
|
7
|
+
import {
|
|
8
|
+
getItemRectangle,
|
|
9
|
+
getDropTargetDimensions as getSwimlaneDimensions,
|
|
10
|
+
getUpdatedItem as getUpdatedItem,
|
|
11
|
+
} from "../utils";
|
|
12
|
+
import {
|
|
13
|
+
getAvailableSpace,
|
|
14
|
+
getDropPreviewRectangle,
|
|
15
|
+
getOverlap,
|
|
16
|
+
} from "./utils";
|
|
17
|
+
import CoreItemComponent from "../CoreItem/CoreItemComponent";
|
|
18
|
+
import OverlapIndicator from "./OverlapIndicator";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
AvailableSpace,
|
|
22
|
+
CoreItem,
|
|
23
|
+
Dimensions,
|
|
24
|
+
GrabInfo,
|
|
25
|
+
Grid,
|
|
26
|
+
Position,
|
|
27
|
+
Rectangle,
|
|
28
|
+
SwimlaneT,
|
|
29
|
+
isCoreItem,
|
|
30
|
+
} from "../../../types";
|
|
31
|
+
import { useTimelineContext } from "../../../hooks/useTimelineContext";
|
|
32
|
+
|
|
33
|
+
interface CoreSwimlaneProps<T> {
|
|
34
|
+
swimlane: SwimlaneT;
|
|
35
|
+
items: CoreItem<T>[];
|
|
36
|
+
focused?: boolean;
|
|
37
|
+
onItemUpdate?: (item: CoreItem<T>) => void;
|
|
38
|
+
onMouseUp?: (e: MouseEvent) => void;
|
|
39
|
+
onClick?: (
|
|
40
|
+
when: Date,
|
|
41
|
+
availableSpace: AvailableSpace | null,
|
|
42
|
+
e: MouseEvent
|
|
43
|
+
) => void;
|
|
44
|
+
onDoubleClick?: (
|
|
45
|
+
when: Date,
|
|
46
|
+
availableSpace: AvailableSpace | null,
|
|
47
|
+
e: MouseEvent
|
|
48
|
+
) => void;
|
|
49
|
+
onContextMenu?: (when: Date, e: MouseEvent) => void;
|
|
50
|
+
renderItem?: (item: CoreItem<T>, isDragged: boolean) => ReactElement;
|
|
51
|
+
onResizeStart?: (data: T) => void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export default function CoreSwimlane<T>({
|
|
55
|
+
swimlane,
|
|
56
|
+
items,
|
|
57
|
+
focused = false,
|
|
58
|
+
onItemUpdate = () => {},
|
|
59
|
+
onMouseUp = () => {},
|
|
60
|
+
onClick = () => {},
|
|
61
|
+
onDoubleClick = () => {},
|
|
62
|
+
onContextMenu = () => {},
|
|
63
|
+
renderItem = defaultRenderItem,
|
|
64
|
+
onResizeStart = () => {},
|
|
65
|
+
}: CoreSwimlaneProps<T>) {
|
|
66
|
+
const { settings } = useTimelineContext();
|
|
67
|
+
|
|
68
|
+
const grid: Grid = {
|
|
69
|
+
x: settings.pixelsPerDay,
|
|
70
|
+
offsetX: settings.pixelsPerDay / 2,
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const dimensions: Dimensions = getSwimlaneDimensions(
|
|
74
|
+
swimlane,
|
|
75
|
+
settings,
|
|
76
|
+
settings
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const [draggedItem, setDraggedItem] = useState<CoreItem<T> | null>(null);
|
|
80
|
+
|
|
81
|
+
const [dropPreviewRect, setDropPreviewRect] = useState<Rectangle | null>(
|
|
82
|
+
null
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
function getMouseEventDate(e: React.MouseEvent) {
|
|
86
|
+
const clientRect = e.currentTarget.getBoundingClientRect();
|
|
87
|
+
return setHours(
|
|
88
|
+
addDays(
|
|
89
|
+
settings.start,
|
|
90
|
+
(e.clientX - clientRect.left) / settings.pixelsPerDay
|
|
91
|
+
),
|
|
92
|
+
12
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getMouseEventOffset(e: React.MouseEvent) {
|
|
97
|
+
const clientRect = e.currentTarget.getBoundingClientRect();
|
|
98
|
+
const relativePxOffsetY = (e.pageY - clientRect.top) / clientRect.height;
|
|
99
|
+
|
|
100
|
+
return relativePxOffsetY * swimlane.capacity;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function handleDrag(mousePos: Position, grabInfo: GrabInfo, data: object) {
|
|
104
|
+
if (!isCoreItem(data)) return;
|
|
105
|
+
|
|
106
|
+
const newDropPreviewRect: Rectangle | null = getDropPreviewRectangle(
|
|
107
|
+
swimlane,
|
|
108
|
+
items,
|
|
109
|
+
data,
|
|
110
|
+
mousePos,
|
|
111
|
+
grabInfo,
|
|
112
|
+
settings,
|
|
113
|
+
grid,
|
|
114
|
+
settings,
|
|
115
|
+
settings.allowOverlaps
|
|
116
|
+
);
|
|
117
|
+
|
|
118
|
+
setDropPreviewRect(newDropPreviewRect);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function handleDrop(
|
|
122
|
+
mousePos: Position,
|
|
123
|
+
grabInfo: GrabInfo,
|
|
124
|
+
data: CoreItem<T>
|
|
125
|
+
) {
|
|
126
|
+
if (!isCoreItem(data) || dropPreviewRect === null) return;
|
|
127
|
+
|
|
128
|
+
const updatedItem: CoreItem<T> = getUpdatedItem<T>(
|
|
129
|
+
data,
|
|
130
|
+
swimlane,
|
|
131
|
+
dropPreviewRect,
|
|
132
|
+
settings,
|
|
133
|
+
settings
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
onItemUpdate(updatedItem);
|
|
137
|
+
setDropPreviewRect(null);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function handleDragLeave() {
|
|
141
|
+
setDropPreviewRect(null);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function handleClick(e: React.MouseEvent, type: "single" | "double") {
|
|
145
|
+
const clickedDate = getMouseEventDate(e);
|
|
146
|
+
const clickedOffset = getMouseEventOffset(e);
|
|
147
|
+
|
|
148
|
+
const availableSpace: AvailableSpace | null = getAvailableSpace(
|
|
149
|
+
clickedDate,
|
|
150
|
+
clickedOffset,
|
|
151
|
+
swimlane,
|
|
152
|
+
items,
|
|
153
|
+
settings
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
if (type == "single") {
|
|
157
|
+
onClick(clickedDate, availableSpace, e);
|
|
158
|
+
} else {
|
|
159
|
+
onDoubleClick(clickedDate, availableSpace, e);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const rects = items.map((x) => ({
|
|
164
|
+
id: x.id,
|
|
165
|
+
...getItemRectangle(x, swimlane, settings, settings),
|
|
166
|
+
}));
|
|
167
|
+
|
|
168
|
+
const overlaps: Rectangle[] = rects
|
|
169
|
+
.flatMap((x) =>
|
|
170
|
+
rects.map((y) => {
|
|
171
|
+
if (x.id < y.id) {
|
|
172
|
+
const overlap = getOverlap(x, y);
|
|
173
|
+
return overlap;
|
|
174
|
+
} else {
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
)
|
|
179
|
+
.filter((x) => x !== null);
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<div
|
|
183
|
+
id={`timeline-swimlane-${swimlane.id}`}
|
|
184
|
+
className={`timeline-swimlane ${
|
|
185
|
+
focused ? "timeline-swimlane-focused" : ""
|
|
186
|
+
}`}
|
|
187
|
+
data-timeline-swimlane-id={swimlane.id}
|
|
188
|
+
style={dimensions}
|
|
189
|
+
onMouseUp={onMouseUp}
|
|
190
|
+
onClick={(e) => handleClick(e, "single")}
|
|
191
|
+
onDoubleClick={(e) => handleClick(e, "double")}
|
|
192
|
+
onContextMenu={(e) => {
|
|
193
|
+
onContextMenu(getMouseEventDate(e), e);
|
|
194
|
+
}}
|
|
195
|
+
>
|
|
196
|
+
<DropTarget
|
|
197
|
+
onDrag={handleDrag}
|
|
198
|
+
onDragLeave={handleDragLeave}
|
|
199
|
+
onDrop={handleDrop}
|
|
200
|
+
>
|
|
201
|
+
{items.map((item, index) => (
|
|
202
|
+
<CoreItemComponent<T>
|
|
203
|
+
key={index}
|
|
204
|
+
item={item}
|
|
205
|
+
settings={settings}
|
|
206
|
+
swimlane={swimlane}
|
|
207
|
+
onDragStart={() => {
|
|
208
|
+
setDraggedItem(item);
|
|
209
|
+
}}
|
|
210
|
+
onDrag={() => {}}
|
|
211
|
+
onDrop={() => {
|
|
212
|
+
setDraggedItem(null);
|
|
213
|
+
}}
|
|
214
|
+
onUpdate={onItemUpdate}
|
|
215
|
+
onResizeStart={() => {
|
|
216
|
+
onResizeStart(item.payload);
|
|
217
|
+
}}
|
|
218
|
+
>
|
|
219
|
+
{renderItem(
|
|
220
|
+
item,
|
|
221
|
+
draggedItem !== null && draggedItem.id === item.id
|
|
222
|
+
)}
|
|
223
|
+
</CoreItemComponent>
|
|
224
|
+
))}
|
|
225
|
+
{dropPreviewRect && <DropPreview {...dropPreviewRect}></DropPreview>}
|
|
226
|
+
|
|
227
|
+
{overlaps.map((overlap, index) => (
|
|
228
|
+
<OverlapIndicator overlap={overlap} key={index} />
|
|
229
|
+
))}
|
|
230
|
+
</DropTarget>
|
|
231
|
+
</div>
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function defaultRenderItem<T>(
|
|
236
|
+
item: CoreItem<T>,
|
|
237
|
+
isDragged: boolean
|
|
238
|
+
): ReactElement {
|
|
239
|
+
return (
|
|
240
|
+
<div>
|
|
241
|
+
{item.id} ({format(item.start, "yyyy-mm-dd")} -{" "}
|
|
242
|
+
{format(item.end, "yyyy-mm-dd")}){isDragged ? "dragging" : ""}
|
|
243
|
+
</div>
|
|
244
|
+
);
|
|
245
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PropsWithChildren } from "react";
|
|
2
|
+
|
|
3
|
+
interface DropPreviewProps {
|
|
4
|
+
x: number;
|
|
5
|
+
y: number;
|
|
6
|
+
width: number;
|
|
7
|
+
height: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default function DropPreview({
|
|
11
|
+
x,
|
|
12
|
+
y,
|
|
13
|
+
width,
|
|
14
|
+
height,
|
|
15
|
+
children,
|
|
16
|
+
}: PropsWithChildren<DropPreviewProps>) {
|
|
17
|
+
return (
|
|
18
|
+
<div
|
|
19
|
+
className="timeline-drop-preview"
|
|
20
|
+
style={{
|
|
21
|
+
left: `${x}px`,
|
|
22
|
+
top: `${y}px`,
|
|
23
|
+
width: `${width}px`,
|
|
24
|
+
height: `${height}px`,
|
|
25
|
+
}}
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { dropTargetForElements } from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
2
|
+
import { DragLocationHistory } from "@atlaskit/pragmatic-drag-and-drop/dist/types/internal-types";
|
|
3
|
+
import { PropsWithChildren, useEffect, useRef } from "react";
|
|
4
|
+
import invariant from "tiny-invariant";
|
|
5
|
+
import { getGrabPosition } from "../utils";
|
|
6
|
+
import { GrabInfo, Position } from "../../../types";
|
|
7
|
+
|
|
8
|
+
interface DropTargetProps {
|
|
9
|
+
onDragStart?: (mousePos: Position, grabInfo: GrabInfo, data: any) => void;
|
|
10
|
+
onDrag?: (mousePos: Position, grabInfo: GrabInfo, data: any) => void;
|
|
11
|
+
onDrop?: (mousePos: Position, grabInfo: GrabInfo, data: any) => void;
|
|
12
|
+
onDragEnter?: (mousePos: Position, grabInfo: GrabInfo, data: any) => void;
|
|
13
|
+
onDragLeave?: (grabInfo: GrabInfo, data: any) => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default function DropTarget({
|
|
17
|
+
children,
|
|
18
|
+
onDragStart = () => {},
|
|
19
|
+
onDrag = () => {},
|
|
20
|
+
onDrop = () => {},
|
|
21
|
+
onDragEnter = () => {},
|
|
22
|
+
onDragLeave = () => {},
|
|
23
|
+
}: PropsWithChildren<DropTargetProps>) {
|
|
24
|
+
const ref = useRef<HTMLDivElement>(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const el = ref.current;
|
|
28
|
+
invariant(el);
|
|
29
|
+
|
|
30
|
+
return dropTargetForElements({
|
|
31
|
+
element: el,
|
|
32
|
+
getData: () => ({ drop: "drop" }),
|
|
33
|
+
onDragStart: ({ source, location }) => {
|
|
34
|
+
const relativeMousePos = getMousePositionRelativeToDropTarget(location);
|
|
35
|
+
|
|
36
|
+
const grabPosition = getGrabPosition(source, location);
|
|
37
|
+
|
|
38
|
+
onDragStart(relativeMousePos, grabPosition, source.data);
|
|
39
|
+
},
|
|
40
|
+
onDragEnter: ({ source, location }) => {
|
|
41
|
+
const relativeMousePos = getMousePositionRelativeToDropTarget(location);
|
|
42
|
+
|
|
43
|
+
const grabPosition = getGrabPosition(source, location);
|
|
44
|
+
onDragEnter(relativeMousePos, grabPosition, source.data);
|
|
45
|
+
},
|
|
46
|
+
onDrag: ({ source, location }) => {
|
|
47
|
+
const relativeMousePos = getMousePositionRelativeToDropTarget(location);
|
|
48
|
+
|
|
49
|
+
const grabPosition = getGrabPosition(source, location);
|
|
50
|
+
|
|
51
|
+
onDrag(relativeMousePos, grabPosition, source.data);
|
|
52
|
+
},
|
|
53
|
+
onDrop: ({ source, location }) => {
|
|
54
|
+
const relativeMousePos = getMousePositionRelativeToDropTarget(location);
|
|
55
|
+
|
|
56
|
+
const grabPosition = getGrabPosition(source, location);
|
|
57
|
+
|
|
58
|
+
onDrop(relativeMousePos, grabPosition, source.data);
|
|
59
|
+
},
|
|
60
|
+
onDragLeave: ({ source, location }) => {
|
|
61
|
+
const grabPosition = getGrabPosition(source, location);
|
|
62
|
+
onDragLeave(grabPosition, source.data);
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
}, [onDrag, onDragLeave, onDragStart, onDrop, onDragEnter]);
|
|
66
|
+
return (
|
|
67
|
+
<div className="timeline-drop-target" ref={ref}>
|
|
68
|
+
{children}
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getMousePositionRelativeToDropTarget(
|
|
74
|
+
location: DragLocationHistory
|
|
75
|
+
): Position {
|
|
76
|
+
const targetRect =
|
|
77
|
+
location.current.dropTargets[0].element.getBoundingClientRect();
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
x: location.current.input.pageX - targetRect.x,
|
|
81
|
+
y: location.current.input.pageY - targetRect.y,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Rectangle } from "../../../types";
|
|
2
|
+
|
|
3
|
+
interface OverlapIndicatorProps {
|
|
4
|
+
overlap: Rectangle;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default function OverlapIndicator({ overlap }: OverlapIndicatorProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
style={{
|
|
11
|
+
position: "absolute",
|
|
12
|
+
left: `${overlap.x}px`,
|
|
13
|
+
top: `${overlap.y}px`,
|
|
14
|
+
width: `${overlap.width - 1}px`,
|
|
15
|
+
height: `${overlap.height - 1}px`,
|
|
16
|
+
background: `rgba(255,0,0, 0.3)`,
|
|
17
|
+
marginLeft: "1px",
|
|
18
|
+
pointerEvents: "none",
|
|
19
|
+
}}
|
|
20
|
+
></div>
|
|
21
|
+
);
|
|
22
|
+
}
|