@archbase/kanban 4.0.37
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/LICENSE +13 -0
- package/README.md +630 -0
- package/dist/archbase-kanban-4.0.37.tgz +0 -0
- package/dist/components/Card/Card.d.ts +26 -0
- package/dist/components/Card/index.d.ts +1 -0
- package/dist/components/CardSkeleton/CardSkeleton.d.ts +9 -0
- package/dist/components/CardSkeleton/index.d.ts +2 -0
- package/dist/components/Column/Column.d.ts +47 -0
- package/dist/components/Column/index.d.ts +1 -0
- package/dist/components/ColumnAdder/ColumnAdder.d.ts +6 -0
- package/dist/components/ColumnAdder/index.d.ts +1 -0
- package/dist/components/ColumnContent/ColumnContent.d.ts +24 -0
- package/dist/components/ColumnContent/index.d.ts +1 -0
- package/dist/components/ColumnHeader/ColumnHeader.d.ts +10 -0
- package/dist/components/ColumnHeader/index.d.ts +1 -0
- package/dist/components/DefaultCard/DefaultCard.d.ts +3 -0
- package/dist/components/DefaultCard/index.d.ts +1 -0
- package/dist/components/GenericItem/GenericItem.d.ts +26 -0
- package/dist/components/GenericItem/index.d.ts +1 -0
- package/dist/components/Kanban.d.ts +3 -0
- package/dist/components/index.d.ts +2 -0
- package/dist/components/types.d.ts +124 -0
- package/dist/context/KanbanContext.d.ts +5 -0
- package/dist/global/dnd/dropManager.d.ts +16 -0
- package/dist/global/dnd/useCardDnd.d.ts +22 -0
- package/dist/global/dnd/useColumnDnd.d.ts +29 -0
- package/dist/index.css +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1041 -0
- package/dist/utils/columnsUtils.d.ts +4 -0
- package/dist/utils/getPrefix.d.ts +2 -0
- package/dist/utils/getSharedProps.d.ts +8 -0
- package/dist/utils/infinite-scroll.d.ts +4 -0
- package/dist/utils/mergeRefs.d.ts +5 -0
- package/dist/utils/scroll.d.ts +2 -0
- package/package.json +66 -0
- package/src/common/_index.scss +0 -0
- package/src/components/Card/Card.tsx +215 -0
- package/src/components/Card/_Card.scss +11 -0
- package/src/components/Card/_index.scss +1 -0
- package/src/components/Card/index.ts +1 -0
- package/src/components/CardSkeleton/CardSkeleton.tsx +45 -0
- package/src/components/CardSkeleton/_CardSkeleton.scss +245 -0
- package/src/components/CardSkeleton/_index.scss +1 -0
- package/src/components/CardSkeleton/index.ts +2 -0
- package/src/components/Column/Column.tsx +246 -0
- package/src/components/Column/_Column.scss +44 -0
- package/src/components/Column/_index.scss +1 -0
- package/src/components/Column/index.ts +1 -0
- package/src/components/ColumnAdder/ColumnAdder.tsx +18 -0
- package/src/components/ColumnAdder/_ColumnAdder.scss +0 -0
- package/src/components/ColumnAdder/_index.scss +1 -0
- package/src/components/ColumnAdder/index.ts +1 -0
- package/src/components/ColumnContent/ColumnContent.tsx +238 -0
- package/src/components/ColumnContent/_ColumnContent.scss +23 -0
- package/src/components/ColumnContent/_index.scss +1 -0
- package/src/components/ColumnContent/index.ts +1 -0
- package/src/components/ColumnHeader/ColumnHeader.tsx +43 -0
- package/src/components/ColumnHeader/_ColumnHeader.scss +26 -0
- package/src/components/ColumnHeader/_index.scss +1 -0
- package/src/components/ColumnHeader/index.ts +1 -0
- package/src/components/DefaultCard/DefaultCard.tsx +9 -0
- package/src/components/DefaultCard/_DefaultCard.scss +14 -0
- package/src/components/DefaultCard/_index.scss +1 -0
- package/src/components/DefaultCard/index.ts +1 -0
- package/src/components/GenericItem/GenericItem.tsx +132 -0
- package/src/components/GenericItem/_GenericItem.scss +4 -0
- package/src/components/GenericItem/_index.scss +1 -0
- package/src/components/GenericItem/index.ts +1 -0
- package/src/components/Kanban.tsx +102 -0
- package/src/components/_Kanban.scss +8 -0
- package/src/components/_index.scss +9 -0
- package/src/components/index.ts +2 -0
- package/src/components/types.ts +155 -0
- package/src/context/KanbanContext.tsx +19 -0
- package/src/global/_index.scss +3 -0
- package/src/global/assets/styles/abstracts/_breakpoints.scss +71 -0
- package/src/global/assets/styles/abstracts/_colors.scss +85 -0
- package/src/global/assets/styles/abstracts/_functions.scss +3 -0
- package/src/global/assets/styles/abstracts/_index.scss +5 -0
- package/src/global/assets/styles/abstracts/_mixins.scss +49 -0
- package/src/global/assets/styles/abstracts/_padding.scss +1 -0
- package/src/global/assets/styles/abstracts/_variables.scss +8 -0
- package/src/global/assets/styles/base/_index.scss +34 -0
- package/src/global/dnd/dropManager.ts +301 -0
- package/src/global/dnd/useCardDnd.tsx +225 -0
- package/src/global/dnd/useColumnDnd.tsx +231 -0
- package/src/global/theme-default.scss +137 -0
- package/src/index.ts +6 -0
- package/src/utils/columnsUtils.ts +19 -0
- package/src/utils/getPrefix.ts +7 -0
- package/src/utils/getSharedProps.ts +18 -0
- package/src/utils/infinite-scroll.ts +18 -0
- package/src/utils/mergeRefs.ts +23 -0
- package/src/utils/scroll.ts +34 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
@use "../abstracts" as *;
|
|
2
|
+
|
|
3
|
+
@import url("https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;500;600;700;800;900&family=Poppins:ital,wght@0,300;0,400;0,500;0,600;0,700;1,300;1,400;1,500;1,600;1,700,wght@0,100..900;1,100..900&display=swap");
|
|
4
|
+
|
|
5
|
+
* {
|
|
6
|
+
font-family: "Raleway", sans-serif;
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: 0;
|
|
9
|
+
box-sizing: border-box;
|
|
10
|
+
outline: none;
|
|
11
|
+
border: none;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
body {
|
|
15
|
+
margin: 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
html {
|
|
19
|
+
scroll-behavior: smooth;
|
|
20
|
+
scroll-padding-top: 5rem;
|
|
21
|
+
text-size-adjust: none;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
a,
|
|
25
|
+
button {
|
|
26
|
+
cursor: pointer;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
a {
|
|
30
|
+
@include transition(opacity, 0.2s, ease-in-out);
|
|
31
|
+
&:active {
|
|
32
|
+
opacity: 0.8;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BoardData,
|
|
3
|
+
BoardItem,
|
|
4
|
+
DropColumnParams,
|
|
5
|
+
DropParams,
|
|
6
|
+
} from "@/components";
|
|
7
|
+
import { extractClosestEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
8
|
+
import { reorderWithEdge } from "@atlaskit/pragmatic-drag-and-drop-hitbox/util/reorder-with-edge";
|
|
9
|
+
|
|
10
|
+
const isCardData = (data: Record<string, unknown>) => data?.type === "card";
|
|
11
|
+
const isColumnData = (data: Record<string, unknown>) => data?.type === "column";
|
|
12
|
+
|
|
13
|
+
const getTasksByColumnId = (columnId: string, dataSource: BoardData) => {
|
|
14
|
+
const tasks: BoardItem[] = [];
|
|
15
|
+
const column = dataSource[columnId];
|
|
16
|
+
if (!column) return tasks;
|
|
17
|
+
const parentTasks = column.children;
|
|
18
|
+
parentTasks?.forEach((taskId) => {
|
|
19
|
+
const task = dataSource[taskId];
|
|
20
|
+
if (task) tasks.push(task);
|
|
21
|
+
});
|
|
22
|
+
return tasks;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const handleCardDrop = ({
|
|
26
|
+
source,
|
|
27
|
+
location,
|
|
28
|
+
columns,
|
|
29
|
+
dataSource,
|
|
30
|
+
onCardMove,
|
|
31
|
+
onColumnMove,
|
|
32
|
+
}: DropParams) => {
|
|
33
|
+
// ── Column drop ───────────────────────────────────────────────────
|
|
34
|
+
if (isColumnData(source.data)) {
|
|
35
|
+
if (!onColumnMove) return;
|
|
36
|
+
|
|
37
|
+
const innerMost = location.current.dropTargets[0];
|
|
38
|
+
if (!innerMost) return;
|
|
39
|
+
|
|
40
|
+
const dropTargetData = innerMost.data;
|
|
41
|
+
if (!isColumnData(dropTargetData)) return;
|
|
42
|
+
|
|
43
|
+
const sourceColumnId = source.data.columnId as string;
|
|
44
|
+
const targetColumnId = dropTargetData.columnId as string;
|
|
45
|
+
if (sourceColumnId === targetColumnId) return;
|
|
46
|
+
|
|
47
|
+
const sourceIndex = columns.findIndex((c) => c.id === sourceColumnId);
|
|
48
|
+
const targetIndex = columns.findIndex((c) => c.id === targetColumnId);
|
|
49
|
+
if (sourceIndex === -1 || targetIndex === -1) return;
|
|
50
|
+
|
|
51
|
+
const closestEdge = extractClosestEdge(dropTargetData);
|
|
52
|
+
const reordered = reorderWithEdge({
|
|
53
|
+
axis: "horizontal",
|
|
54
|
+
list: columns,
|
|
55
|
+
startIndex: sourceIndex,
|
|
56
|
+
indexOfTarget: targetIndex,
|
|
57
|
+
closestEdgeOfTarget: closestEdge,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
onColumnMove({
|
|
61
|
+
columnId: sourceColumnId,
|
|
62
|
+
fromIndex: sourceIndex,
|
|
63
|
+
toIndex: reordered.findIndex((c) => c.id === sourceColumnId),
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!isCardData(source.data)) return;
|
|
70
|
+
|
|
71
|
+
const draggingCardId = source.data.itemId;
|
|
72
|
+
const sourceColumnId = source.data.columnId;
|
|
73
|
+
|
|
74
|
+
const innerMost = location.current.dropTargets[0];
|
|
75
|
+
if (!innerMost) return;
|
|
76
|
+
|
|
77
|
+
const dropTargetData = innerMost.data;
|
|
78
|
+
|
|
79
|
+
// Case 1: Dropping on another card
|
|
80
|
+
if (dropTargetData["card-drop-target"]) {
|
|
81
|
+
const targetColumnId = dropTargetData.columnId;
|
|
82
|
+
const targetCardId = dropTargetData.itemId;
|
|
83
|
+
const closestEdge = extractClosestEdge(dropTargetData);
|
|
84
|
+
|
|
85
|
+
// Find the cards involved
|
|
86
|
+
const sourceColumn = dataSource[sourceColumnId];
|
|
87
|
+
const targetColumn = dataSource[targetColumnId];
|
|
88
|
+
|
|
89
|
+
if (!sourceColumn || !targetColumn) return;
|
|
90
|
+
|
|
91
|
+
// For same column reordering
|
|
92
|
+
if (sourceColumnId === targetColumnId) {
|
|
93
|
+
// Get all tasks in the column
|
|
94
|
+
const allTasks = getTasksByColumnId(targetColumnId, dataSource);
|
|
95
|
+
|
|
96
|
+
// Find source and target indices
|
|
97
|
+
const sourceIndex = allTasks.findIndex(
|
|
98
|
+
(task) => task.id === draggingCardId,
|
|
99
|
+
);
|
|
100
|
+
const targetIndex = allTasks.findIndex(
|
|
101
|
+
(task) => task.id === targetCardId,
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
if (sourceIndex === -1 || targetIndex === -1) return;
|
|
105
|
+
|
|
106
|
+
// No change needed if same position
|
|
107
|
+
if (sourceIndex === targetIndex) return;
|
|
108
|
+
|
|
109
|
+
// Use reorderWithEdge to get the correct new order
|
|
110
|
+
const reordered = reorderWithEdge({
|
|
111
|
+
axis: "vertical",
|
|
112
|
+
list: allTasks,
|
|
113
|
+
startIndex: sourceIndex,
|
|
114
|
+
indexOfTarget: targetIndex,
|
|
115
|
+
closestEdgeOfTarget: closestEdge,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// Calculate the new position based on the reordered array
|
|
119
|
+
const newPosition = reordered.findIndex(
|
|
120
|
+
(task) => task.id === draggingCardId,
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
// Find tasks above and below the insertion point
|
|
124
|
+
const taskAbove = newPosition > 0 ? reordered[newPosition - 1] : null;
|
|
125
|
+
const taskBelow =
|
|
126
|
+
newPosition < reordered.length - 1 ? reordered[newPosition + 1] : null;
|
|
127
|
+
|
|
128
|
+
// Call the provided onCardMove handler
|
|
129
|
+
if (onCardMove) {
|
|
130
|
+
onCardMove({
|
|
131
|
+
cardId: draggingCardId,
|
|
132
|
+
fromColumnId: sourceColumnId,
|
|
133
|
+
toColumnId: targetColumnId,
|
|
134
|
+
taskAbove: taskAbove?.id,
|
|
135
|
+
taskBelow: taskBelow?.id,
|
|
136
|
+
position: newPosition,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Moving between columns
|
|
141
|
+
else {
|
|
142
|
+
// Get all tasks in the target column
|
|
143
|
+
const allTargetTasks = getTasksByColumnId(targetColumnId, dataSource);
|
|
144
|
+
|
|
145
|
+
// Find the target task index
|
|
146
|
+
const targetIndex = allTargetTasks.findIndex(
|
|
147
|
+
(task) => task.id === targetCardId,
|
|
148
|
+
);
|
|
149
|
+
if (targetIndex === -1) return;
|
|
150
|
+
const finalIndex =
|
|
151
|
+
closestEdge === "bottom" ? targetIndex + 1 : targetIndex;
|
|
152
|
+
|
|
153
|
+
// Get the dragging task from source column
|
|
154
|
+
const draggingTask = getTasksByColumnId(sourceColumnId, dataSource).find(
|
|
155
|
+
(task) => task.id === draggingCardId,
|
|
156
|
+
);
|
|
157
|
+
if (!draggingTask) return;
|
|
158
|
+
|
|
159
|
+
// Create a temporary array with the dragging task inserted at the target position
|
|
160
|
+
const tempArray = [...allTargetTasks];
|
|
161
|
+
tempArray.splice(finalIndex, 0, draggingTask);
|
|
162
|
+
|
|
163
|
+
// Use reorderWithEdge to get the correct final position
|
|
164
|
+
const reordered = reorderWithEdge({
|
|
165
|
+
axis: "vertical",
|
|
166
|
+
list: tempArray,
|
|
167
|
+
startIndex: finalIndex,
|
|
168
|
+
indexOfTarget: finalIndex,
|
|
169
|
+
closestEdgeOfTarget: closestEdge,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Calculate the new position based on the reordered array
|
|
173
|
+
const newPosition = reordered.findIndex(
|
|
174
|
+
(task) => task.id === draggingCardId,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const taskAbove = newPosition > 0 ? reordered[newPosition - 1] : null;
|
|
178
|
+
const taskBelow =
|
|
179
|
+
newPosition < reordered.length - 1 ? reordered[newPosition + 1] : null;
|
|
180
|
+
|
|
181
|
+
if (onCardMove) {
|
|
182
|
+
onCardMove({
|
|
183
|
+
cardId: draggingCardId,
|
|
184
|
+
fromColumnId: sourceColumnId,
|
|
185
|
+
toColumnId: targetColumnId,
|
|
186
|
+
taskAbove: taskAbove?.id,
|
|
187
|
+
taskBelow: taskBelow?.id,
|
|
188
|
+
position: newPosition,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// Case 2: Dropping directly on a column (not on a card)
|
|
194
|
+
else if (isColumnData(dropTargetData)) {
|
|
195
|
+
const targetColumnId = dropTargetData.columnId;
|
|
196
|
+
const targetTasks = getTasksByColumnId(targetColumnId, dataSource);
|
|
197
|
+
const taskAbove =
|
|
198
|
+
targetTasks.length > 0 ? targetTasks[targetTasks.length - 1] : null;
|
|
199
|
+
|
|
200
|
+
if (onCardMove) {
|
|
201
|
+
onCardMove({
|
|
202
|
+
cardId: draggingCardId,
|
|
203
|
+
fromColumnId: sourceColumnId,
|
|
204
|
+
toColumnId: targetColumnId,
|
|
205
|
+
taskAbove: taskAbove?.id,
|
|
206
|
+
taskBelow: null,
|
|
207
|
+
position: targetTasks.length,
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
export const dropColumnHandler = (
|
|
214
|
+
drop: DropColumnParams,
|
|
215
|
+
dataSource: BoardData,
|
|
216
|
+
) => {
|
|
217
|
+
const { columnId, toIndex } = drop;
|
|
218
|
+
const newDataSource = { ...dataSource };
|
|
219
|
+
const rootChildren = [...(newDataSource["root"]?.children || [])];
|
|
220
|
+
const currentIndex = rootChildren.indexOf(columnId);
|
|
221
|
+
|
|
222
|
+
if (currentIndex === -1) return newDataSource;
|
|
223
|
+
|
|
224
|
+
rootChildren.splice(currentIndex, 1);
|
|
225
|
+
rootChildren.splice(toIndex, 0, columnId);
|
|
226
|
+
if (newDataSource["root"]) {
|
|
227
|
+
newDataSource["root"].children = rootChildren;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return newDataSource;
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
export const dropHandler = (
|
|
234
|
+
drop: {
|
|
235
|
+
cardId: string;
|
|
236
|
+
fromColumnId: string;
|
|
237
|
+
toColumnId: string;
|
|
238
|
+
taskAbove: string | null;
|
|
239
|
+
taskBelow: string | null;
|
|
240
|
+
},
|
|
241
|
+
dataSource: BoardData,
|
|
242
|
+
updateDroppedItem?: (targetColumn: BoardItem, droppedItem: any) => any,
|
|
243
|
+
updateDestinationColumn?: (targetColumn: BoardItem) => any,
|
|
244
|
+
updateSourceColumn?: (sourceColumn: BoardItem) => any,
|
|
245
|
+
) => {
|
|
246
|
+
const { cardId, fromColumnId, toColumnId, taskAbove, taskBelow } = drop;
|
|
247
|
+
|
|
248
|
+
const newDataSource = { ...dataSource };
|
|
249
|
+
|
|
250
|
+
if (newDataSource[fromColumnId]?.children) {
|
|
251
|
+
newDataSource[fromColumnId].children = newDataSource[
|
|
252
|
+
fromColumnId
|
|
253
|
+
].children.filter((id: string) => id !== cardId);
|
|
254
|
+
if (
|
|
255
|
+
newDataSource[fromColumnId]?.totalItems !== undefined &&
|
|
256
|
+
newDataSource[fromColumnId].totalItems > 0
|
|
257
|
+
)
|
|
258
|
+
newDataSource[fromColumnId].totalItems--;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (updateSourceColumn && newDataSource[fromColumnId])
|
|
262
|
+
newDataSource[fromColumnId] = updateSourceColumn(
|
|
263
|
+
newDataSource[fromColumnId],
|
|
264
|
+
);
|
|
265
|
+
|
|
266
|
+
const targetChildren = newDataSource[toColumnId]?.children || [];
|
|
267
|
+
let insertIndex = 0;
|
|
268
|
+
|
|
269
|
+
if (taskAbove) insertIndex = targetChildren.indexOf(taskAbove) + 1;
|
|
270
|
+
else if (taskBelow) insertIndex = targetChildren.indexOf(taskBelow);
|
|
271
|
+
|
|
272
|
+
const alreadyExistInTarget = targetChildren.includes(cardId);
|
|
273
|
+
|
|
274
|
+
if (
|
|
275
|
+
newDataSource[toColumnId]?.totalItems !== undefined &&
|
|
276
|
+
!alreadyExistInTarget
|
|
277
|
+
)
|
|
278
|
+
newDataSource[toColumnId].totalItems++;
|
|
279
|
+
|
|
280
|
+
if (updateDroppedItem && newDataSource[cardId]) {
|
|
281
|
+
const updatedItem = updateDroppedItem?.(
|
|
282
|
+
newDataSource[toColumnId],
|
|
283
|
+
newDataSource[cardId],
|
|
284
|
+
);
|
|
285
|
+
newDataSource[cardId] = updatedItem || newDataSource[cardId];
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (newDataSource[toColumnId]?.children) {
|
|
289
|
+
newDataSource[toColumnId].children = newDataSource[
|
|
290
|
+
toColumnId
|
|
291
|
+
]?.children?.filter((id: string) => id !== cardId);
|
|
292
|
+
newDataSource[toColumnId].children.splice(insertIndex, 0, cardId);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (updateDestinationColumn && newDataSource[toColumnId])
|
|
296
|
+
newDataSource[toColumnId] = updateDestinationColumn(
|
|
297
|
+
newDataSource[toColumnId],
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
return newDataSource;
|
|
301
|
+
};
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
2
|
+
import { BoardItem, DndState } from "@/components/types";
|
|
3
|
+
import {
|
|
4
|
+
draggable,
|
|
5
|
+
dropTargetForElements,
|
|
6
|
+
} from "@atlaskit/pragmatic-drag-and-drop/element/adapter";
|
|
7
|
+
import {
|
|
8
|
+
attachClosestEdge,
|
|
9
|
+
extractClosestEdge,
|
|
10
|
+
Edge,
|
|
11
|
+
} from "@atlaskit/pragmatic-drag-and-drop-hitbox/closest-edge";
|
|
12
|
+
import { combine } from "@atlaskit/pragmatic-drag-and-drop/combine";
|
|
13
|
+
import { setCustomNativeDragPreview } from "@atlaskit/pragmatic-drag-and-drop/element/set-custom-native-drag-preview";
|
|
14
|
+
import { preserveOffsetOnSource } from "@atlaskit/pragmatic-drag-and-drop/element/preserve-offset-on-source";
|
|
15
|
+
import { useKanbanContext } from "@/context/KanbanContext";
|
|
16
|
+
|
|
17
|
+
export type TaskCardState =
|
|
18
|
+
| {
|
|
19
|
+
type: "idle";
|
|
20
|
+
}
|
|
21
|
+
| {
|
|
22
|
+
type: "is-dragging";
|
|
23
|
+
}
|
|
24
|
+
| {
|
|
25
|
+
type: "is-dragging-and-left-self";
|
|
26
|
+
}
|
|
27
|
+
| {
|
|
28
|
+
type: "is-over";
|
|
29
|
+
dragging: DOMRect;
|
|
30
|
+
closestEdge: Edge;
|
|
31
|
+
}
|
|
32
|
+
| {
|
|
33
|
+
type: "preview";
|
|
34
|
+
container: HTMLElement;
|
|
35
|
+
dragging: DOMRect;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const idle: TaskCardState = { type: "idle" };
|
|
39
|
+
|
|
40
|
+
// Custom hook to handle all drag and drop logic
|
|
41
|
+
export const useCardDnd = (
|
|
42
|
+
data: BoardItem = {} as BoardItem,
|
|
43
|
+
column: BoardItem = {} as BoardItem,
|
|
44
|
+
index: number,
|
|
45
|
+
isDraggable: boolean,
|
|
46
|
+
onCardDndStateChange?: (info: DndState) => void
|
|
47
|
+
) => {
|
|
48
|
+
const { viewOnly } = useKanbanContext();
|
|
49
|
+
const outerRef = useRef<HTMLDivElement>(null);
|
|
50
|
+
const innerRef = useRef<HTMLDivElement>(null);
|
|
51
|
+
const [state, setState] = useState<TaskCardState>(idle);
|
|
52
|
+
|
|
53
|
+
// Memoize initial data to prevent recreating on each render
|
|
54
|
+
const getInitialData = useCallback(
|
|
55
|
+
() => ({
|
|
56
|
+
type: "card",
|
|
57
|
+
itemId: data?.id,
|
|
58
|
+
columnId: column?.id,
|
|
59
|
+
index,
|
|
60
|
+
isDraggable,
|
|
61
|
+
parentId: data.parentId,
|
|
62
|
+
rect: innerRef.current?.getBoundingClientRect() || null,
|
|
63
|
+
}),
|
|
64
|
+
[data?.id, column?.id, index, isDraggable, data?.parentId]
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const getDropTargetData = useCallback(
|
|
68
|
+
({ input, element }) => {
|
|
69
|
+
const cardData = {
|
|
70
|
+
type: "card",
|
|
71
|
+
"card-drop-target": true,
|
|
72
|
+
itemId: data.id,
|
|
73
|
+
columnId: column.id,
|
|
74
|
+
index,
|
|
75
|
+
isDraggable,
|
|
76
|
+
parentId: data.parentId,
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
return attachClosestEdge(cardData, {
|
|
80
|
+
input,
|
|
81
|
+
element,
|
|
82
|
+
allowedEdges: ["top", "bottom"],
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
[data?.id, column?.id, index, isDraggable, data?.parentId]
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
// Optimize the drop check to avoid recalculating on every drag move
|
|
89
|
+
const canDrop = useCallback(
|
|
90
|
+
(args) => {
|
|
91
|
+
const sourceData = args.source.data;
|
|
92
|
+
if (sourceData.itemId === data.parentId) return false;
|
|
93
|
+
return sourceData.isDraggable;
|
|
94
|
+
},
|
|
95
|
+
[data?.id, data?.parentId]
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
// Drag and drop event handlers
|
|
99
|
+
const handleGenerateDragPreview = useCallback(
|
|
100
|
+
({ nativeSetDragImage, location }) => {
|
|
101
|
+
setCustomNativeDragPreview({
|
|
102
|
+
nativeSetDragImage,
|
|
103
|
+
getOffset: preserveOffsetOnSource({
|
|
104
|
+
element: innerRef.current!,
|
|
105
|
+
input: location.current.input,
|
|
106
|
+
}),
|
|
107
|
+
render({ container }) {
|
|
108
|
+
const rect = innerRef.current!.getBoundingClientRect();
|
|
109
|
+
setState({
|
|
110
|
+
type: "preview",
|
|
111
|
+
container,
|
|
112
|
+
dragging: rect,
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
},
|
|
117
|
+
[]
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const handleDragStart = useCallback(() => {
|
|
121
|
+
setState({ type: "is-dragging" });
|
|
122
|
+
}, []);
|
|
123
|
+
|
|
124
|
+
const handleDrop = useCallback(() => {
|
|
125
|
+
setState(idle);
|
|
126
|
+
}, []);
|
|
127
|
+
|
|
128
|
+
const handleDragEnter = useCallback(
|
|
129
|
+
({ source, self }) => {
|
|
130
|
+
if (source.data.type !== "card") return;
|
|
131
|
+
if (source.data.itemId === data.id) return;
|
|
132
|
+
|
|
133
|
+
const closestEdge = extractClosestEdge(self.data);
|
|
134
|
+
if (!closestEdge) return;
|
|
135
|
+
|
|
136
|
+
setState({
|
|
137
|
+
type: "is-over",
|
|
138
|
+
dragging: source.data.rect as DOMRect,
|
|
139
|
+
closestEdge,
|
|
140
|
+
});
|
|
141
|
+
},
|
|
142
|
+
[data?.id]
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
const handleDrag = useCallback(
|
|
146
|
+
({ source, self }) => {
|
|
147
|
+
if (source.data.type !== "card") return;
|
|
148
|
+
if (source.data.itemId === data.id) return;
|
|
149
|
+
|
|
150
|
+
const closestEdge = extractClosestEdge(self.data);
|
|
151
|
+
if (!closestEdge) return;
|
|
152
|
+
|
|
153
|
+
setState({
|
|
154
|
+
type: "is-over",
|
|
155
|
+
dragging: source.data.rect as DOMRect,
|
|
156
|
+
closestEdge,
|
|
157
|
+
});
|
|
158
|
+
},
|
|
159
|
+
[data?.id]
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const handleDragLeave = useCallback(
|
|
163
|
+
({ source }) => {
|
|
164
|
+
if (source.data.type !== "card") return;
|
|
165
|
+
|
|
166
|
+
if (source.data.itemId === data?.id) {
|
|
167
|
+
setState({ type: "is-dragging-and-left-self" });
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
setState(idle);
|
|
172
|
+
},
|
|
173
|
+
[data.id]
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
// Setup drag and drop effects
|
|
177
|
+
useEffect(() => {
|
|
178
|
+
const outer = outerRef.current;
|
|
179
|
+
const inner = innerRef.current;
|
|
180
|
+
|
|
181
|
+
if (!outer || !inner) return;
|
|
182
|
+
|
|
183
|
+
return combine(
|
|
184
|
+
draggable({
|
|
185
|
+
element: inner,
|
|
186
|
+
getInitialData,
|
|
187
|
+
onGenerateDragPreview: handleGenerateDragPreview,
|
|
188
|
+
onDragStart: handleDragStart,
|
|
189
|
+
onDrop: handleDrop,
|
|
190
|
+
canDrag: () => isDraggable && !viewOnly,
|
|
191
|
+
}),
|
|
192
|
+
dropTargetForElements({
|
|
193
|
+
element: outer,
|
|
194
|
+
canDrop,
|
|
195
|
+
getIsSticky: () => true,
|
|
196
|
+
getData: getDropTargetData,
|
|
197
|
+
onDragEnter: handleDragEnter,
|
|
198
|
+
onDrag: handleDrag,
|
|
199
|
+
onDragLeave: handleDragLeave,
|
|
200
|
+
onDrop: handleDrop,
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
}, [
|
|
204
|
+
getInitialData,
|
|
205
|
+
handleGenerateDragPreview,
|
|
206
|
+
handleDragStart,
|
|
207
|
+
handleDrop,
|
|
208
|
+
isDraggable,
|
|
209
|
+
canDrop,
|
|
210
|
+
getDropTargetData,
|
|
211
|
+
handleDragEnter,
|
|
212
|
+
handleDrag,
|
|
213
|
+
handleDragLeave,
|
|
214
|
+
]);
|
|
215
|
+
|
|
216
|
+
useEffect(() => {
|
|
217
|
+
onCardDndStateChange?.({ state, card: data, column });
|
|
218
|
+
}, [state, onCardDndStateChange]);
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
outerRef,
|
|
222
|
+
innerRef,
|
|
223
|
+
state,
|
|
224
|
+
};
|
|
225
|
+
};
|