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.
Files changed (72) hide show
  1. package/.github/workflows/static.yml +55 -0
  2. package/README.md +54 -0
  3. package/docs/README.md +54 -0
  4. package/docs/eslint.config.js +28 -0
  5. package/docs/index.html +12 -0
  6. package/docs/package-lock.json +5101 -0
  7. package/docs/package.json +35 -0
  8. package/docs/src/App.css +5 -0
  9. package/docs/src/App.tsx +59 -0
  10. package/docs/src/assets/react.svg +1 -0
  11. package/docs/src/components/AllocationComponent.tsx +82 -0
  12. package/docs/src/components/Timeline.tsx +183 -0
  13. package/docs/src/constants.ts +42 -0
  14. package/docs/src/hooks/useLocalStorage.ts +21 -0
  15. package/docs/src/main.tsx +9 -0
  16. package/docs/src/models/Allocation.ts +11 -0
  17. package/docs/src/models/AllocationId.ts +1 -0
  18. package/docs/src/models/Resource.ts +8 -0
  19. package/docs/src/models/ResourceId.ts +1 -0
  20. package/docs/src/vite-env.d.ts +1 -0
  21. package/docs/tsconfig.json +27 -0
  22. package/docs/vite.config.ts +8 -0
  23. package/eslint.config.js +28 -0
  24. package/index.html +13 -0
  25. package/package.json +43 -0
  26. package/src/components/Timeline.scss +297 -0
  27. package/src/components/TimelineAside.tsx +81 -0
  28. package/src/components/TimelineBackground.tsx +53 -0
  29. package/src/components/TimelineBody.tsx +54 -0
  30. package/src/components/TimelineHeader/DaysHeader.tsx +44 -0
  31. package/src/components/TimelineHeader/MonthsHeader.tsx +62 -0
  32. package/src/components/TimelineHeader/TimelineHeader.tsx +64 -0
  33. package/src/components/TimelineHeader/WeeksHeader.tsx +63 -0
  34. package/src/components/TimelineHeader/index.ts +9 -0
  35. package/src/components/TimelineHeader/renderingUtils.tsx +57 -0
  36. package/src/components/TimelineSelectionLayer.tsx +179 -0
  37. package/src/components/TimelineSettingsContext.tsx +27 -0
  38. package/src/components/TimelineSettingsProvider.tsx +24 -0
  39. package/src/components/TimelineWrapper.tsx +51 -0
  40. package/src/components/core/CoreItem/CoreItemComponent.tsx +69 -0
  41. package/src/components/core/CoreItem/DragResizeComponent.tsx +180 -0
  42. package/src/components/core/CoreSwimlane/AvailableSpaceIndicator.tsx +156 -0
  43. package/src/components/core/CoreSwimlane/CoreSwimlane.tsx +245 -0
  44. package/src/components/core/CoreSwimlane/DropPreview.tsx +30 -0
  45. package/src/components/core/CoreSwimlane/DropTarget.tsx +83 -0
  46. package/src/components/core/CoreSwimlane/OverlapIndicator.tsx +22 -0
  47. package/src/components/core/CoreSwimlane/utils.ts +375 -0
  48. package/src/components/core/utils.ts +154 -0
  49. package/src/components/layout/TimelineLayout.tsx +93 -0
  50. package/src/components/layout/layout.scss +107 -0
  51. package/src/global.d.ts +9 -0
  52. package/src/hooks/useScroll.tsx +71 -0
  53. package/src/hooks/useTimelineContext.tsx +6 -0
  54. package/src/index.ts +15 -0
  55. package/src/types/AvailableSpace.ts +6 -0
  56. package/src/types/CoreItem.ts +23 -0
  57. package/src/types/DateBounds.ts +4 -0
  58. package/src/types/Dimensions.ts +4 -0
  59. package/src/types/GrabInfo.ts +6 -0
  60. package/src/types/Grid.ts +6 -0
  61. package/src/types/ItemId.ts +1 -0
  62. package/src/types/OffsetBounds.ts +4 -0
  63. package/src/types/Pixels.ts +4 -0
  64. package/src/types/Position.ts +4 -0
  65. package/src/types/Rectangle.ts +6 -0
  66. package/src/types/SwimlaneId.ts +1 -0
  67. package/src/types/SwimlaneT.ts +6 -0
  68. package/src/types/TimeRange.ts +4 -0
  69. package/src/types/TimelineSettings.ts +11 -0
  70. package/src/types/index.ts +15 -0
  71. package/tsconfig.json +32 -0
  72. 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
+ }