@noya-app/noya-designsystem 0.1.55 → 0.1.56
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/.turbo/turbo-build.log +10 -10
- package/CHANGELOG.md +8 -0
- package/dist/index.d.mts +111 -40
- package/dist/index.d.ts +111 -40
- package/dist/index.js +981 -651
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +936 -611
- package/dist/index.mjs.map +1 -1
- package/package.json +2 -2
- package/src/__tests__/validateDropIndicator.test.ts +35 -9
- package/src/components/ListView.tsx +5 -8
- package/src/components/Toolbar.tsx +15 -2
- package/src/components/sorting/DragRegistration.tsx +110 -0
- package/src/components/sorting/SharedDragProvider.tsx +307 -0
- package/src/components/sorting/Sortable.tsx +404 -0
- package/src/components/sorting/createSharedDrag.tsx +46 -0
- package/src/components/sorting/sorting.ts +180 -0
- package/src/index.tsx +4 -2
- package/src/utils/moveTreeItem.ts +4 -2
- package/src/components/Sortable.tsx +0 -468
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noya-app/noya-designsystem",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.56",
|
|
4
4
|
"main": "./dist/index.js",
|
|
5
5
|
"module": "./dist/index.mjs",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"@noya-app/noya-colorpicker": "0.1.21",
|
|
26
26
|
"@noya-app/noya-utils": "0.1.5",
|
|
27
27
|
"@noya-app/noya-geometry": "0.1.11",
|
|
28
|
-
"@noya-app/noya-icons": "0.1.
|
|
28
|
+
"@noya-app/noya-icons": "0.1.10",
|
|
29
29
|
"@noya-app/noya-keymap": "0.1.3",
|
|
30
30
|
"@noya-app/noya-tailwind-config": "0.1.3",
|
|
31
31
|
"@radix-ui/primitive": "^1.0.0",
|
|
@@ -2,46 +2,46 @@ import { describe, expect, it } from "bun:test";
|
|
|
2
2
|
import {
|
|
3
3
|
defaultAcceptsDrop,
|
|
4
4
|
validateDropIndicator,
|
|
5
|
-
} from "../components/
|
|
5
|
+
} from "../components/sorting/sorting";
|
|
6
6
|
|
|
7
7
|
describe("defaultAcceptsDrop", () => {
|
|
8
8
|
it("should return false if the active item is the same as the over item", () => {
|
|
9
|
-
const result = defaultAcceptsDrop(0, 0, "inside");
|
|
9
|
+
const result = defaultAcceptsDrop(0, 0, "inside", "", "");
|
|
10
10
|
expect(result).toBe(false);
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it("should return false for 'inside' position", () => {
|
|
14
|
-
const result = defaultAcceptsDrop(0, 1, "inside");
|
|
14
|
+
const result = defaultAcceptsDrop(0, 1, "inside", "", "");
|
|
15
15
|
expect(result).toBe(false);
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
it("should return false when dropping above the same item", () => {
|
|
19
|
-
const result = defaultAcceptsDrop(0, 0, "above");
|
|
19
|
+
const result = defaultAcceptsDrop(0, 0, "above", "", "");
|
|
20
20
|
expect(result).toBe(false);
|
|
21
21
|
});
|
|
22
22
|
|
|
23
23
|
it("should return false when dropping below the same item", () => {
|
|
24
|
-
const result = defaultAcceptsDrop(0, 0, "below");
|
|
24
|
+
const result = defaultAcceptsDrop(0, 0, "below", "", "");
|
|
25
25
|
expect(result).toBe(false);
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
it("should return false when dropping above the item below the source", () => {
|
|
29
|
-
const result = defaultAcceptsDrop(0, 1, "above");
|
|
29
|
+
const result = defaultAcceptsDrop(0, 1, "above", "", "");
|
|
30
30
|
expect(result).toBe(false);
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
it("should return false when dropping below the item above the source", () => {
|
|
34
|
-
const result = defaultAcceptsDrop(1, 0, "below");
|
|
34
|
+
const result = defaultAcceptsDrop(1, 0, "below", "", "");
|
|
35
35
|
expect(result).toBe(false);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
it("should return true when dropping above a valid target", () => {
|
|
39
|
-
const result = defaultAcceptsDrop(0, 2, "above");
|
|
39
|
+
const result = defaultAcceptsDrop(0, 2, "above", "", "");
|
|
40
40
|
expect(result).toBe(true);
|
|
41
41
|
});
|
|
42
42
|
|
|
43
43
|
it("should return true when dropping below a valid target", () => {
|
|
44
|
-
const result = defaultAcceptsDrop(2, 0, "below");
|
|
44
|
+
const result = defaultAcceptsDrop(2, 0, "below", "", "");
|
|
45
45
|
expect(result).toBe(true);
|
|
46
46
|
});
|
|
47
47
|
});
|
|
@@ -97,6 +97,8 @@ describe("validateDropIndicator", () => {
|
|
|
97
97
|
offsetStart: 50, // offsetStart in middle third
|
|
98
98
|
elementStart: 0, // elementStart
|
|
99
99
|
elementSize: 100, // elementSize
|
|
100
|
+
sourceListId: "",
|
|
101
|
+
targetListId: "",
|
|
100
102
|
});
|
|
101
103
|
expect(result).toBe("inside");
|
|
102
104
|
});
|
|
@@ -110,6 +112,8 @@ describe("validateDropIndicator", () => {
|
|
|
110
112
|
offsetStart: 20, // offsetStart in top third
|
|
111
113
|
elementStart: 0, // elementStart
|
|
112
114
|
elementSize: 100, // elementSize
|
|
115
|
+
sourceListId: "",
|
|
116
|
+
targetListId: "",
|
|
113
117
|
});
|
|
114
118
|
expect(result).toBe("above");
|
|
115
119
|
});
|
|
@@ -123,6 +127,8 @@ describe("validateDropIndicator", () => {
|
|
|
123
127
|
offsetStart: 80, // offsetStart in bottom third
|
|
124
128
|
elementStart: 0, // elementStart
|
|
125
129
|
elementSize: 100, // elementSize
|
|
130
|
+
sourceListId: "",
|
|
131
|
+
targetListId: "",
|
|
126
132
|
});
|
|
127
133
|
expect(result).toBe("inside");
|
|
128
134
|
});
|
|
@@ -138,6 +144,8 @@ describe("validateDropIndicator", () => {
|
|
|
138
144
|
offsetStart: 50, // offsetStart in middle third
|
|
139
145
|
elementStart: 0, // elementStart
|
|
140
146
|
elementSize: 100, // elementSize
|
|
147
|
+
sourceListId: "",
|
|
148
|
+
targetListId: "",
|
|
141
149
|
});
|
|
142
150
|
expect(result).toBe("inside");
|
|
143
151
|
});
|
|
@@ -151,6 +159,8 @@ describe("validateDropIndicator", () => {
|
|
|
151
159
|
offsetStart: 20, // offsetStart in top third
|
|
152
160
|
elementStart: 0, // elementStart
|
|
153
161
|
elementSize: 100, // elementSize
|
|
162
|
+
sourceListId: "",
|
|
163
|
+
targetListId: "",
|
|
154
164
|
});
|
|
155
165
|
expect(result).toBe("above");
|
|
156
166
|
});
|
|
@@ -164,6 +174,8 @@ describe("validateDropIndicator", () => {
|
|
|
164
174
|
offsetStart: 80, // offsetStart in bottom third
|
|
165
175
|
elementStart: 0, // elementStart
|
|
166
176
|
elementSize: 100, // elementSize
|
|
177
|
+
sourceListId: "",
|
|
178
|
+
targetListId: "",
|
|
167
179
|
});
|
|
168
180
|
expect(result).toBe("below");
|
|
169
181
|
});
|
|
@@ -177,6 +189,8 @@ describe("validateDropIndicator", () => {
|
|
|
177
189
|
offsetStart: 120, // offsetStart in middle third relative to elementStart
|
|
178
190
|
elementStart: 100, // elementStart
|
|
179
191
|
elementSize: 60, // elementSize
|
|
192
|
+
sourceListId: "",
|
|
193
|
+
targetListId: "",
|
|
180
194
|
});
|
|
181
195
|
expect(result).toBe("inside");
|
|
182
196
|
});
|
|
@@ -191,6 +205,8 @@ describe("validateDropIndicator", () => {
|
|
|
191
205
|
offsetStart: 50, // offsetStart in middle third
|
|
192
206
|
elementStart: 0, // elementStart
|
|
193
207
|
elementSize: 100, // elementSize
|
|
208
|
+
sourceListId: "",
|
|
209
|
+
targetListId: "",
|
|
194
210
|
});
|
|
195
211
|
expect(result).toBe("inside");
|
|
196
212
|
});
|
|
@@ -204,6 +220,8 @@ describe("validateDropIndicator", () => {
|
|
|
204
220
|
offsetStart: 20, // offsetStart in top half
|
|
205
221
|
elementStart: 0, // elementStart
|
|
206
222
|
elementSize: 100, // elementSize
|
|
223
|
+
sourceListId: "",
|
|
224
|
+
targetListId: "",
|
|
207
225
|
});
|
|
208
226
|
expect(result).toBe("above");
|
|
209
227
|
});
|
|
@@ -217,6 +235,8 @@ describe("validateDropIndicator", () => {
|
|
|
217
235
|
offsetStart: 70, // offsetStart in bottom half
|
|
218
236
|
elementStart: 0, // elementStart
|
|
219
237
|
elementSize: 100, // elementSize
|
|
238
|
+
sourceListId: "",
|
|
239
|
+
targetListId: "",
|
|
220
240
|
});
|
|
221
241
|
expect(result).toBe("below");
|
|
222
242
|
});
|
|
@@ -230,6 +250,8 @@ describe("validateDropIndicator", () => {
|
|
|
230
250
|
offsetStart: 20, // offsetStart in top half
|
|
231
251
|
elementStart: 0, // elementStart
|
|
232
252
|
elementSize: 100, // elementSize
|
|
253
|
+
sourceListId: "",
|
|
254
|
+
targetListId: "",
|
|
233
255
|
});
|
|
234
256
|
expect(result).toBe("inside");
|
|
235
257
|
});
|
|
@@ -244,6 +266,8 @@ describe("validateDropIndicator", () => {
|
|
|
244
266
|
offsetStart: 50,
|
|
245
267
|
elementStart: 0,
|
|
246
268
|
elementSize: 100,
|
|
269
|
+
sourceListId: "",
|
|
270
|
+
targetListId: "",
|
|
247
271
|
});
|
|
248
272
|
expect(result).toBeUndefined();
|
|
249
273
|
});
|
|
@@ -257,6 +281,8 @@ describe("validateDropIndicator", () => {
|
|
|
257
281
|
offsetStart: 50,
|
|
258
282
|
elementStart: 0,
|
|
259
283
|
elementSize: 100,
|
|
284
|
+
sourceListId: "",
|
|
285
|
+
targetListId: "",
|
|
260
286
|
});
|
|
261
287
|
expect(result).toBeUndefined();
|
|
262
288
|
});
|
|
@@ -31,13 +31,14 @@ import { ContextMenu } from "./ContextMenu";
|
|
|
31
31
|
import { InputField } from "./InputField";
|
|
32
32
|
import { MenuItem } from "./internal/Menu";
|
|
33
33
|
import { ScrollArea } from "./ScrollArea";
|
|
34
|
+
import { Sortable } from "./sorting/Sortable";
|
|
34
35
|
import {
|
|
35
36
|
DropValidator,
|
|
37
|
+
MoveDragItemHandler,
|
|
36
38
|
normalizeListTargetIndex,
|
|
37
39
|
RelativeDropPosition,
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
} from "./Sortable";
|
|
40
|
+
SortableItemContextProps,
|
|
41
|
+
} from "./sorting/sorting";
|
|
41
42
|
import { Spacer } from "./Spacer";
|
|
42
43
|
|
|
43
44
|
export type ListRowMarginType = "none" | "top" | "bottom" | "vertical";
|
|
@@ -776,11 +777,7 @@ export type ListViewRootProps = {
|
|
|
776
777
|
onPress?: () => void;
|
|
777
778
|
scrollable?: boolean;
|
|
778
779
|
expandable?: boolean;
|
|
779
|
-
onMoveItem?:
|
|
780
|
-
sourceIndex: number,
|
|
781
|
-
targetIndex: number,
|
|
782
|
-
position: RelativeDropPosition
|
|
783
|
-
) => void;
|
|
780
|
+
onMoveItem?: MoveDragItemHandler;
|
|
784
781
|
indentation?: number;
|
|
785
782
|
acceptsDrop?: DropValidator;
|
|
786
783
|
pressEventName?: PressEventName;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useKeyboardShortcuts } from "@noya-app/noya-keymap";
|
|
2
|
-
import React from "react";
|
|
2
|
+
import React, { ComponentProps } from "react";
|
|
3
3
|
|
|
4
4
|
import { memoGeneric } from "@noya-app/react-utils";
|
|
5
5
|
import { BaseToolbar } from "./BaseToolbar";
|
|
@@ -60,16 +60,20 @@ export const ToolbarMenuButton = memoGeneric(function ToolbarMenuButton<
|
|
|
60
60
|
>({
|
|
61
61
|
item,
|
|
62
62
|
onSelectMenuItem,
|
|
63
|
+
as,
|
|
63
64
|
}: {
|
|
64
65
|
item: SelectableMenuItem<T>;
|
|
65
66
|
onSelectMenuItem?: (value: T) => void;
|
|
67
|
+
as?: ComponentProps<typeof Button>["as"];
|
|
66
68
|
}) {
|
|
67
69
|
const content = (
|
|
68
70
|
<Button
|
|
71
|
+
as={as}
|
|
69
72
|
disabled={item.disabled}
|
|
70
73
|
active={item.checked}
|
|
71
74
|
icon={item.icon}
|
|
72
75
|
style={item.icon && !item.title ? iconButtonStyle : undefined}
|
|
76
|
+
{...(as === "a" && { href: item.value })}
|
|
73
77
|
onClick={() => {
|
|
74
78
|
if (onSelectMenuItem && item.value) {
|
|
75
79
|
onSelectMenuItem(item.value);
|
|
@@ -94,9 +98,11 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
|
|
|
94
98
|
>({
|
|
95
99
|
item,
|
|
96
100
|
onSelectMenuItem,
|
|
101
|
+
buttonAs,
|
|
97
102
|
}: {
|
|
98
103
|
item: MenuItem<T>;
|
|
99
104
|
onSelectMenuItem?: (value: T) => void;
|
|
105
|
+
buttonAs?: ComponentProps<typeof Button>["as"];
|
|
100
106
|
}) {
|
|
101
107
|
if (item.type === "sectionHeader") return null;
|
|
102
108
|
|
|
@@ -106,7 +112,11 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
|
|
|
106
112
|
|
|
107
113
|
if (isSelectableMenuItem(item)) {
|
|
108
114
|
return (
|
|
109
|
-
<ToolbarMenuButton
|
|
115
|
+
<ToolbarMenuButton
|
|
116
|
+
item={item}
|
|
117
|
+
onSelectMenuItem={onSelectMenuItem}
|
|
118
|
+
as={buttonAs}
|
|
119
|
+
/>
|
|
110
120
|
);
|
|
111
121
|
}
|
|
112
122
|
|
|
@@ -118,9 +128,11 @@ export const ToolbarMenuItem = memoGeneric(function ToolbarMenuItem<
|
|
|
118
128
|
export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
|
|
119
129
|
items,
|
|
120
130
|
onSelectMenuItem,
|
|
131
|
+
buttonAs,
|
|
121
132
|
}: {
|
|
122
133
|
items: MenuItem<T>[];
|
|
123
134
|
onSelectMenuItem?: (value: T) => void;
|
|
135
|
+
buttonAs?: ComponentProps<typeof Button>["as"];
|
|
124
136
|
}) {
|
|
125
137
|
return (
|
|
126
138
|
<>
|
|
@@ -130,6 +142,7 @@ export const ToolbarMenu = memoGeneric(function ToolbarMenu<T extends string>({
|
|
|
130
142
|
key={i}
|
|
131
143
|
item={item}
|
|
132
144
|
onSelectMenuItem={onSelectMenuItem}
|
|
145
|
+
buttonAs={buttonAs}
|
|
133
146
|
/>
|
|
134
147
|
);
|
|
135
148
|
})}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AcceptsDrop,
|
|
3
|
+
MoveDragItemHandler,
|
|
4
|
+
RelativeDropPosition,
|
|
5
|
+
} from "./sorting";
|
|
6
|
+
|
|
7
|
+
import { Active, Over, UniqueIdentifier } from "@dnd-kit/core";
|
|
8
|
+
import { AcceptsFromList } from "./sorting";
|
|
9
|
+
|
|
10
|
+
import { Point } from "@noya-app/noya-geometry";
|
|
11
|
+
import React from "react";
|
|
12
|
+
import { DragItem } from "./sorting";
|
|
13
|
+
|
|
14
|
+
export type GetDropIndicatorParameters = {
|
|
15
|
+
absolutePosition: Point;
|
|
16
|
+
active: Active;
|
|
17
|
+
over: Over;
|
|
18
|
+
sourceListId: string;
|
|
19
|
+
targetListId: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export interface RegisteredList {
|
|
23
|
+
id: string;
|
|
24
|
+
keys: UniqueIdentifier[];
|
|
25
|
+
onMoveItem: MoveDragItemHandler;
|
|
26
|
+
renderOverlay?: (id: UniqueIdentifier) => React.ReactNode;
|
|
27
|
+
acceptsFromList: AcceptsFromList;
|
|
28
|
+
acceptsDrop: AcceptsDrop;
|
|
29
|
+
getDropIndicator: (
|
|
30
|
+
parameters: GetDropIndicatorParameters
|
|
31
|
+
) => RelativeDropPosition | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface DragRegistrationContextType {
|
|
35
|
+
registerList: (list: RegisteredList) => void;
|
|
36
|
+
unregisterList: (listId: string) => void;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export const DragRegistrationContext =
|
|
40
|
+
React.createContext<DragRegistrationContextType | null>(null);
|
|
41
|
+
|
|
42
|
+
export function useDragRegistration(
|
|
43
|
+
context: React.Context<DragRegistrationContextType | null>
|
|
44
|
+
): DragRegistrationContextType {
|
|
45
|
+
const value = React.useContext(context);
|
|
46
|
+
|
|
47
|
+
if (!value) {
|
|
48
|
+
throw new Error(
|
|
49
|
+
"useDragRegistration must be used within a DragRegistrationProvider"
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return value;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function useDragRegistrationManager() {
|
|
57
|
+
const [registeredLists, setRegisteredLists] = React.useState<
|
|
58
|
+
Map<string, RegisteredList>
|
|
59
|
+
>(new Map());
|
|
60
|
+
|
|
61
|
+
const registerList = React.useCallback((list: RegisteredList) => {
|
|
62
|
+
setRegisteredLists((prev) => new Map(prev).set(list.id, list));
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const unregisterList = React.useCallback((listId: string) => {
|
|
66
|
+
setRegisteredLists((prev) => {
|
|
67
|
+
const newMap = new Map(prev);
|
|
68
|
+
newMap.delete(listId);
|
|
69
|
+
return newMap;
|
|
70
|
+
});
|
|
71
|
+
}, []);
|
|
72
|
+
|
|
73
|
+
return { registerList, unregisterList, registeredLists };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export type ActiveDragContextType = {
|
|
77
|
+
activeItem: DragItem | null;
|
|
78
|
+
delta: Point;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
export const ActiveDragContext =
|
|
82
|
+
React.createContext<ActiveDragContextType | null>(null);
|
|
83
|
+
|
|
84
|
+
export function useActiveDrag(
|
|
85
|
+
context: React.Context<ActiveDragContextType | null>
|
|
86
|
+
): ActiveDragContextType {
|
|
87
|
+
const value = React.useContext(context);
|
|
88
|
+
|
|
89
|
+
if (!value) {
|
|
90
|
+
throw new Error("useActiveDrag must be used within a ActiveDragProvider");
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return value;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* We use this context to automatically create a drag context if not exists.
|
|
98
|
+
* We do this separately from the contexts that actually store data since for those
|
|
99
|
+
* we want to allow a custom context to be passed and not get overwritten by nested contexts.
|
|
100
|
+
*/
|
|
101
|
+
export const AnyDragContext = React.createContext<boolean>(false);
|
|
102
|
+
|
|
103
|
+
export function useAnyDragContext() {
|
|
104
|
+
return React.useContext(AnyDragContext);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type SharedDragProviderContexts = {
|
|
108
|
+
dragRegistrationContext: React.Context<DragRegistrationContextType | null>;
|
|
109
|
+
activeDragContext: React.Context<ActiveDragContextType | null>;
|
|
110
|
+
};
|
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import {
|
|
2
|
+
closestCenter,
|
|
3
|
+
CollisionDetection,
|
|
4
|
+
DndContext,
|
|
5
|
+
DragEndEvent,
|
|
6
|
+
DragMoveEvent,
|
|
7
|
+
DragOverlay,
|
|
8
|
+
DragStartEvent,
|
|
9
|
+
PointerSensor,
|
|
10
|
+
pointerWithin,
|
|
11
|
+
useSensor,
|
|
12
|
+
useSensors,
|
|
13
|
+
} from "@dnd-kit/core";
|
|
14
|
+
import * as React from "react";
|
|
15
|
+
import { useMemo } from "react";
|
|
16
|
+
import { createPortal } from "react-dom";
|
|
17
|
+
import {
|
|
18
|
+
ActiveDragContext,
|
|
19
|
+
ActiveDragContextType,
|
|
20
|
+
AnyDragContext,
|
|
21
|
+
DragRegistrationContext,
|
|
22
|
+
DragRegistrationContextType,
|
|
23
|
+
SharedDragProviderContexts,
|
|
24
|
+
useDragRegistrationManager,
|
|
25
|
+
} from "./DragRegistration";
|
|
26
|
+
import { DragItem } from "./sorting";
|
|
27
|
+
|
|
28
|
+
type SharedDragProviderProps = {
|
|
29
|
+
children: React.ReactNode;
|
|
30
|
+
contexts?: SharedDragProviderContexts;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export function SharedDragProvider({
|
|
34
|
+
children,
|
|
35
|
+
contexts = {
|
|
36
|
+
dragRegistrationContext: DragRegistrationContext,
|
|
37
|
+
activeDragContext: ActiveDragContext,
|
|
38
|
+
},
|
|
39
|
+
}: SharedDragProviderProps) {
|
|
40
|
+
const sensors = useSensors(
|
|
41
|
+
useSensor(PointerSensor, {
|
|
42
|
+
activationConstraint: {
|
|
43
|
+
distance: 4,
|
|
44
|
+
},
|
|
45
|
+
})
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
const mounted = useMounted();
|
|
49
|
+
const [activeItem, setActiveItem] = React.useState<DragItem | null>(null);
|
|
50
|
+
const [delta, setDelta] = React.useState({ x: 0, y: 0 });
|
|
51
|
+
const { registerList, unregisterList, registeredLists } =
|
|
52
|
+
useDragRegistrationManager();
|
|
53
|
+
|
|
54
|
+
const activatorEventRef = React.useRef<PointerEvent | null>(null);
|
|
55
|
+
|
|
56
|
+
const acceptsListDrop = React.useCallback(
|
|
57
|
+
(sourceListId: string, targetListId: string) => {
|
|
58
|
+
const sourceList = registeredLists.get(sourceListId);
|
|
59
|
+
const targetList = registeredLists.get(targetListId);
|
|
60
|
+
|
|
61
|
+
if (!sourceList || !targetList) return false;
|
|
62
|
+
|
|
63
|
+
return targetList.acceptsFromList({
|
|
64
|
+
sourceListId,
|
|
65
|
+
targetListId,
|
|
66
|
+
});
|
|
67
|
+
},
|
|
68
|
+
[registeredLists]
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const handleDragStart = React.useCallback(
|
|
72
|
+
(event: DragStartEvent) => {
|
|
73
|
+
// Capture the activator event for position calculations
|
|
74
|
+
activatorEventRef.current = event.activatorEvent as PointerEvent;
|
|
75
|
+
|
|
76
|
+
// Find which list contains this item
|
|
77
|
+
for (const [listId, list] of registeredLists) {
|
|
78
|
+
const itemIndex = list.keys.findIndex((key) => key === event.active.id);
|
|
79
|
+
|
|
80
|
+
if (itemIndex >= 0) {
|
|
81
|
+
const dragItem: DragItem = {
|
|
82
|
+
id: event.active.id,
|
|
83
|
+
listId,
|
|
84
|
+
index: itemIndex,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
setActiveItem(dragItem);
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
[registeredLists]
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
const handleDragMove = React.useCallback((event: DragMoveEvent) => {
|
|
96
|
+
setDelta({ x: event.delta.x, y: event.delta.y });
|
|
97
|
+
}, []);
|
|
98
|
+
|
|
99
|
+
const handleDragEnd = React.useCallback(
|
|
100
|
+
(event: DragEndEvent) => {
|
|
101
|
+
const { active, over } = event;
|
|
102
|
+
const activatorEventPoint = activatorEventRef.current
|
|
103
|
+
? {
|
|
104
|
+
x: activatorEventRef.current.clientX,
|
|
105
|
+
y: activatorEventRef.current.clientY,
|
|
106
|
+
}
|
|
107
|
+
: null;
|
|
108
|
+
|
|
109
|
+
setActiveItem(null);
|
|
110
|
+
activatorEventRef.current = null;
|
|
111
|
+
|
|
112
|
+
if (!activeItem || !over || active.id === over.id) {
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Find target list and item
|
|
117
|
+
let targetListId: string | null = null;
|
|
118
|
+
let targetIndex: number = -1;
|
|
119
|
+
|
|
120
|
+
for (const [listId, list] of registeredLists) {
|
|
121
|
+
const itemIndex = list.keys.findIndex((key) => key === over.id);
|
|
122
|
+
|
|
123
|
+
if (itemIndex >= 0) {
|
|
124
|
+
targetListId = listId;
|
|
125
|
+
targetIndex = itemIndex;
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Also check if dropping on the list container itself
|
|
130
|
+
if (over.id === listId) {
|
|
131
|
+
targetListId = listId;
|
|
132
|
+
targetIndex = list.keys.length; // Append to end
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!targetListId || !acceptsListDrop(activeItem.listId, targetListId)) {
|
|
138
|
+
setActiveItem(null);
|
|
139
|
+
activatorEventRef.current = null;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const targetList = registeredLists.get(targetListId);
|
|
144
|
+
const sourceList = registeredLists.get(activeItem.listId);
|
|
145
|
+
|
|
146
|
+
if (!targetList || !sourceList) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Handle drops into items within the list
|
|
151
|
+
if (targetIndex < targetList.keys.length && activatorEventPoint) {
|
|
152
|
+
// Calculate absolute cursor position
|
|
153
|
+
const eventX = activatorEventPoint.x;
|
|
154
|
+
const eventY = activatorEventPoint.y;
|
|
155
|
+
const currentX = eventX + delta.x;
|
|
156
|
+
const currentY = eventY + delta.y;
|
|
157
|
+
const absolutePosition = { x: currentX, y: currentY };
|
|
158
|
+
|
|
159
|
+
const indicator = targetList.getDropIndicator({
|
|
160
|
+
absolutePosition,
|
|
161
|
+
active,
|
|
162
|
+
over,
|
|
163
|
+
sourceListId: activeItem.listId,
|
|
164
|
+
targetListId,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (!indicator) return;
|
|
168
|
+
|
|
169
|
+
// Notify the source list to handle the move
|
|
170
|
+
sourceList.onMoveItem(
|
|
171
|
+
activeItem.index,
|
|
172
|
+
targetIndex,
|
|
173
|
+
indicator,
|
|
174
|
+
activeItem.listId,
|
|
175
|
+
targetListId
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
// Handle drops into empty lists or at the end of lists
|
|
179
|
+
else if (targetIndex >= targetList.keys.length) {
|
|
180
|
+
// For empty lists or drops at the end, we don't need detailed position validation
|
|
181
|
+
// Just check if the drop is acceptable with a "below" position
|
|
182
|
+
const canDrop = targetList.acceptsDrop(
|
|
183
|
+
activeItem.index,
|
|
184
|
+
Math.max(0, targetList.keys.length - 1), // Use last valid index or 0 for empty lists
|
|
185
|
+
"below",
|
|
186
|
+
activeItem.listId,
|
|
187
|
+
targetListId
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
if (canDrop) {
|
|
191
|
+
sourceList.onMoveItem(
|
|
192
|
+
activeItem.index,
|
|
193
|
+
targetIndex,
|
|
194
|
+
"below",
|
|
195
|
+
activeItem.listId,
|
|
196
|
+
targetListId
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
[activeItem, registeredLists, acceptsListDrop, delta]
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const contextValue: DragRegistrationContextType = React.useMemo(
|
|
205
|
+
() => ({
|
|
206
|
+
activeItem,
|
|
207
|
+
delta,
|
|
208
|
+
registerList,
|
|
209
|
+
unregisterList,
|
|
210
|
+
}),
|
|
211
|
+
[activeItem, delta, registerList, unregisterList]
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
const activeList = activeItem ? registeredLists.get(activeItem.listId) : null;
|
|
215
|
+
const activeDragContextValue: ActiveDragContextType = React.useMemo(
|
|
216
|
+
() => ({ activeItem, delta }),
|
|
217
|
+
[activeItem, delta]
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
const collisionDetection = useMemo(() => {
|
|
221
|
+
return getItemFirstCollisionDetection(Array.from(registeredLists.keys()));
|
|
222
|
+
}, [registeredLists]);
|
|
223
|
+
|
|
224
|
+
return (
|
|
225
|
+
<AnyDragContext.Provider value={true}>
|
|
226
|
+
<contexts.dragRegistrationContext.Provider value={contextValue}>
|
|
227
|
+
<contexts.activeDragContext.Provider value={activeDragContextValue}>
|
|
228
|
+
<DndContext
|
|
229
|
+
sensors={sensors}
|
|
230
|
+
collisionDetection={collisionDetection}
|
|
231
|
+
onDragStart={handleDragStart}
|
|
232
|
+
onDragMove={handleDragMove}
|
|
233
|
+
onDragEnd={handleDragEnd}
|
|
234
|
+
>
|
|
235
|
+
{children}
|
|
236
|
+
{mounted &&
|
|
237
|
+
activeList?.renderOverlay &&
|
|
238
|
+
activeItem &&
|
|
239
|
+
createPortal(
|
|
240
|
+
<DragOverlay dropAnimation={null}>
|
|
241
|
+
{activeList.renderOverlay(activeItem.id)}
|
|
242
|
+
</DragOverlay>,
|
|
243
|
+
document.body
|
|
244
|
+
)}
|
|
245
|
+
</DndContext>
|
|
246
|
+
</contexts.activeDragContext.Provider>
|
|
247
|
+
</contexts.dragRegistrationContext.Provider>
|
|
248
|
+
</AnyDragContext.Provider>
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function useMounted() {
|
|
253
|
+
const [mounted, setMounted] = React.useState(false);
|
|
254
|
+
|
|
255
|
+
React.useEffect(() => {
|
|
256
|
+
setMounted(true);
|
|
257
|
+
}, []);
|
|
258
|
+
|
|
259
|
+
return mounted;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function withDragProvider<T extends object>(
|
|
263
|
+
Component: React.ComponentType<T>
|
|
264
|
+
) {
|
|
265
|
+
return function WithDragProvider(props: T) {
|
|
266
|
+
const anyDragContext = React.useContext(AnyDragContext);
|
|
267
|
+
|
|
268
|
+
if (!anyDragContext) {
|
|
269
|
+
return (
|
|
270
|
+
<SharedDragProvider>
|
|
271
|
+
<Component {...props} />
|
|
272
|
+
</SharedDragProvider>
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
return <Component {...props} />;
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Custom collision detection that prioritizes items over containers
|
|
281
|
+
export const getItemFirstCollisionDetection =
|
|
282
|
+
(registeredListIds: string[]): CollisionDetection =>
|
|
283
|
+
(parameters) => {
|
|
284
|
+
const pointerCollisions = pointerWithin(parameters);
|
|
285
|
+
|
|
286
|
+
if (pointerCollisions.length > 0) {
|
|
287
|
+
const itemCollisions = pointerCollisions.filter((collision) => {
|
|
288
|
+
for (const listId of registeredListIds) {
|
|
289
|
+
if (collision.id === listId) {
|
|
290
|
+
return false;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return true;
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (itemCollisions.length > 0) {
|
|
298
|
+
return itemCollisions;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Fall back to pointer-based detection (includes containers)
|
|
303
|
+
// This handles empty lists and edge cases
|
|
304
|
+
return pointerCollisions.length > 0
|
|
305
|
+
? pointerCollisions
|
|
306
|
+
: closestCenter(parameters);
|
|
307
|
+
};
|