playbook_ui 15.5.0 → 15.6.0.pre.alpha.draggableask12906
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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_advanced_table/_advanced_table.scss +96 -6
- data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_table_props.html.erb +1 -1
- data/app/pb_kits/playbook/pb_background/_background.tsx +6 -6
- data/app/pb_kits/playbook/pb_background/background.test.js +5 -1
- data/app/pb_kits/playbook/pb_background/docs/_background_light.html.erb +1 -1
- data/app/pb_kits/playbook/pb_background/docs/_background_light.jsx +0 -1
- data/app/pb_kits/playbook/pb_background/docs/_background_light.md +1 -0
- data/app/pb_kits/playbook/pb_background/docs/example.yml +2 -2
- data/app/pb_kits/playbook/pb_dialog/docs/_dialog_compound_components.html.erb +31 -0
- data/app/pb_kits/playbook/pb_draggable/context/index.tsx +399 -7
- data/app/pb_kits/playbook/pb_draggable/context/types.ts +8 -3
- data/app/pb_kits/playbook/pb_draggable/docs/_draggable_multiple_containers_dropzone.jsx +180 -0
- data/app/pb_kits/playbook/pb_draggable/docs/_draggable_multiple_containers_dropzone.md +22 -0
- data/app/pb_kits/playbook/pb_draggable/docs/example.yml +3 -2
- data/app/pb_kits/playbook/pb_draggable/docs/index.js +2 -1
- data/app/pb_kits/playbook/pb_draggable/draggable.test.jsx +77 -1
- data/app/pb_kits/playbook/pb_file_upload/_file_upload.scss +4 -4
- data/app/pb_kits/playbook/pb_home_address_street/_home_address_street.tsx +34 -22
- data/app/pb_kits/playbook/pb_home_address_street/city_emphasis.html.erb +16 -12
- data/app/pb_kits/playbook/pb_home_address_street/docs/_home_address_street_default.html.erb +1 -1
- data/app/pb_kits/playbook/pb_home_address_street/none_emphasis.html.erb +16 -12
- data/app/pb_kits/playbook/pb_home_address_street/street_emphasis.html.erb +16 -12
- data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.scss +10 -0
- data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.tsx +66 -15
- data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.jsx +42 -0
- data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.md +1 -0
- data/app/pb_kits/playbook/pb_multiple_users/docs/example.yml +1 -0
- data/app/pb_kits/playbook/pb_multiple_users/docs/index.js +1 -0
- data/app/pb_kits/playbook/pb_multiple_users/multiple_users.test.js +25 -0
- data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +44 -10
- data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.html.erb +34 -4
- data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.jsx +16 -7
- data/app/pb_kits/playbook/pb_radio/docs/_radio_error.md +1 -1
- data/app/pb_kits/playbook/pb_select/docs/_select_error.md +1 -1
- data/app/pb_kits/playbook/pb_table/styles/_vertical_border.scss +49 -0
- data/app/pb_kits/playbook/pb_text_input/docs/_text_input_error.md +1 -1
- data/app/pb_kits/playbook/pb_textarea/docs/_textarea_error.md +1 -1
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +15 -0
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +3 -0
- data/app/pb_kits/playbook/pb_typeahead/components/ClearIndicator.tsx +13 -2
- data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +6 -1
- data/app/pb_kits/playbook/pb_typeahead/components/ValueContainer.tsx +34 -7
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.html.erb +30 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.jsx +37 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.md +3 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +2 -0
- data/app/pb_kits/playbook/pb_typeahead/docs/index.js +2 -1
- data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +6 -1
- data/dist/chunks/_typeahead-wpGumTwA.js +6 -0
- data/dist/chunks/{lib-Dk4GKPut.js → lib-CgpqUb6l.js} +2 -2
- data/dist/chunks/vendor.js +2 -2
- data/dist/playbook-rails-react-bindings.js +1 -1
- data/dist/playbook-rails.js +1 -1
- data/dist/playbook.css +1 -1
- data/lib/playbook/version.rb +2 -2
- metadata +12 -5
- data/app/pb_kits/playbook/pb_bar_graph/BarGraphStyles.scss +0 -58
- data/dist/chunks/_typeahead-Bx4QsIEU.js +0 -6
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext, useReducer, useContext, useEffect, useMemo } from "react";
|
|
1
|
+
import React, { createContext, useReducer, useContext, useEffect, useMemo, useRef } from "react";
|
|
2
2
|
import { InitialStateType, ActionType, DraggableProviderType } from "./types";
|
|
3
3
|
|
|
4
4
|
const initialState: InitialStateType = {
|
|
@@ -39,6 +39,72 @@ const reducer = (state: InitialStateType, action: ActionType) => {
|
|
|
39
39
|
|
|
40
40
|
return { ...state, items: newItems };
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
// Used only when enableCrossContainerPreview is true
|
|
44
|
+
case "REORDER_ITEMS_CROSS_CONTAINER": {
|
|
45
|
+
const { dragId, targetId, newContainer } = action.payload;
|
|
46
|
+
const newItems = [...state.items];
|
|
47
|
+
const draggedItem = newItems.find((item) => item && item.id === dragId);
|
|
48
|
+
|
|
49
|
+
if (!draggedItem) return state;
|
|
50
|
+
|
|
51
|
+
const draggedIndex = newItems.indexOf(draggedItem);
|
|
52
|
+
const targetIndex = newItems.findIndex(
|
|
53
|
+
(item) => item && item.id === targetId
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
if (draggedIndex === -1 || targetIndex === -1) return state;
|
|
57
|
+
|
|
58
|
+
const updatedItem = { ...draggedItem, container: newContainer };
|
|
59
|
+
newItems.splice(draggedIndex, 1);
|
|
60
|
+
newItems.splice(targetIndex, 0, updatedItem);
|
|
61
|
+
|
|
62
|
+
return { ...state, items: newItems };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Used only when enableCrossContainerPreview is true
|
|
66
|
+
case "MOVE_TO_CONTAINER_END": {
|
|
67
|
+
const { dragId, newContainer } = action.payload;
|
|
68
|
+
const newItems = [...state.items];
|
|
69
|
+
const draggedItem = newItems.find((item) => item && item.id === dragId);
|
|
70
|
+
|
|
71
|
+
if (!draggedItem) return state;
|
|
72
|
+
|
|
73
|
+
const draggedIndex = newItems.indexOf(draggedItem);
|
|
74
|
+
if (draggedIndex === -1) return state;
|
|
75
|
+
|
|
76
|
+
const updatedItem = { ...draggedItem, container: newContainer };
|
|
77
|
+
|
|
78
|
+
// Remove from current position
|
|
79
|
+
newItems.splice(draggedIndex, 1);
|
|
80
|
+
|
|
81
|
+
// Insert at end of target container
|
|
82
|
+
const lastIndexInContainer = newItems
|
|
83
|
+
.map((item) => item && item.container)
|
|
84
|
+
.lastIndexOf(newContainer);
|
|
85
|
+
|
|
86
|
+
if (lastIndexInContainer === -1) {
|
|
87
|
+
newItems.push(updatedItem);
|
|
88
|
+
} else {
|
|
89
|
+
newItems.splice(lastIndexInContainer + 1, 0, updatedItem);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { ...state, items: newItems };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Reset item back to its original container (e.g., when drag ends without valid drop)
|
|
96
|
+
case "RESET_DRAG_CONTAINER": {
|
|
97
|
+
const { itemId, originalContainer } = action.payload;
|
|
98
|
+
return {
|
|
99
|
+
...state,
|
|
100
|
+
items: state.items.map(item =>
|
|
101
|
+
item.id === itemId
|
|
102
|
+
? { ...item, container: originalContainer }
|
|
103
|
+
: item
|
|
104
|
+
)
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
42
108
|
default:
|
|
43
109
|
return state;
|
|
44
110
|
}
|
|
@@ -61,9 +127,32 @@ export const DraggableProvider = ({
|
|
|
61
127
|
onDrop,
|
|
62
128
|
onDragOver,
|
|
63
129
|
dropZone = { type: 'ghost', color: 'neutral', direction: 'vertical' },
|
|
64
|
-
providerId = 'default', // fallback provided for backward compatibility
|
|
130
|
+
providerId = 'default', // fallback provided for backward compatibility
|
|
131
|
+
// Opt-in flag for cross-container preview
|
|
132
|
+
enableCrossContainerPreview = false,
|
|
65
133
|
}: DraggableProviderType) => {
|
|
66
134
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
135
|
+
|
|
136
|
+
// Track drag state for global listener
|
|
137
|
+
const dragStateRef = useRef<{
|
|
138
|
+
isDragging: boolean;
|
|
139
|
+
draggedItemId: string;
|
|
140
|
+
originalContainer: string;
|
|
141
|
+
currentContainer: string;
|
|
142
|
+
dropOccurred: boolean;
|
|
143
|
+
}>({
|
|
144
|
+
isDragging: false,
|
|
145
|
+
draggedItemId: '',
|
|
146
|
+
originalContainer: '',
|
|
147
|
+
currentContainer: '',
|
|
148
|
+
dropOccurred: false,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// Track current state for use in gated event listeners (avoid stale closures)
|
|
152
|
+
const stateRef = useRef(state);
|
|
153
|
+
useEffect(() => {
|
|
154
|
+
stateRef.current = state;
|
|
155
|
+
}, [state]);
|
|
67
156
|
|
|
68
157
|
// Parse dropZone prop - handle both string format (backward compatibility) and object format
|
|
69
158
|
let dropZoneType = 'ghost';
|
|
@@ -93,7 +182,168 @@ export const DraggableProvider = ({
|
|
|
93
182
|
onReorder(state.items);
|
|
94
183
|
}, [state.items]);
|
|
95
184
|
|
|
185
|
+
// Monitor for failed drops by detecting mouse/pointer release during drag (this is needed for cross container preview)
|
|
186
|
+
useEffect(() => {
|
|
187
|
+
if (!enableCrossContainerPreview) return;
|
|
188
|
+
|
|
189
|
+
// Allow drops anywhere on the document by preventing default dragover
|
|
190
|
+
const handleGlobalDragOver = (e: DragEvent) => {
|
|
191
|
+
if (dragStateRef.current.isDragging) {
|
|
192
|
+
e.preventDefault();
|
|
193
|
+
}
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
// Handle drops anywhere on the document (including non-container areas)
|
|
197
|
+
const handleGlobalDrop = (e: DragEvent) => {
|
|
198
|
+
if (!dragStateRef.current.isDragging) return;
|
|
199
|
+
|
|
200
|
+
// If a container already handled the drop, don't process again
|
|
201
|
+
if (dragStateRef.current.dropOccurred) return;
|
|
202
|
+
|
|
203
|
+
e.preventDefault();
|
|
204
|
+
|
|
205
|
+
// If we reach here, it means the drop was NOT on a valid container
|
|
206
|
+
// (otherwise the container's handleDrop would have set dropOccurred = true)
|
|
207
|
+
// So we should ALWAYS reset to original container for invalid drops
|
|
208
|
+
const originalContainer = dragStateRef.current.originalContainer;
|
|
209
|
+
|
|
210
|
+
dispatch({
|
|
211
|
+
type: 'RESET_DRAG_CONTAINER',
|
|
212
|
+
payload: {
|
|
213
|
+
itemId: dragStateRef.current.draggedItemId,
|
|
214
|
+
originalContainer: originalContainer,
|
|
215
|
+
},
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
|
|
219
|
+
dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
|
|
220
|
+
dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
|
|
221
|
+
|
|
222
|
+
// Clear drag state
|
|
223
|
+
dragStateRef.current = {
|
|
224
|
+
isDragging: false,
|
|
225
|
+
draggedItemId: '',
|
|
226
|
+
originalContainer: '',
|
|
227
|
+
currentContainer: '',
|
|
228
|
+
dropOccurred: false,
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const handleGlobalMouseUp = () => {
|
|
233
|
+
// If we're dragging and mouse is released, wait a bit to see if drop occurs
|
|
234
|
+
if (dragStateRef.current.isDragging) {
|
|
235
|
+
setTimeout(() => {
|
|
236
|
+
const currentContainer = dragStateRef.current.currentContainer;
|
|
237
|
+
|
|
238
|
+
// If drop still hasn't occurred, check if item is in a different container
|
|
239
|
+
if (dragStateRef.current.isDragging && !dragStateRef.current.dropOccurred) {
|
|
240
|
+
// If item is in a different container than original, treat it as a successful drop
|
|
241
|
+
if (currentContainer && currentContainer !== dragStateRef.current.originalContainer) {
|
|
242
|
+
// Trigger onDrop callback with the current container
|
|
243
|
+
if (onDrop) {
|
|
244
|
+
const draggedItem = stateRef.current.items.find(item => item && item.id === dragStateRef.current.draggedItemId);
|
|
245
|
+
const updatedItem = draggedItem ? { ...draggedItem, container: currentContainer } : null;
|
|
246
|
+
const itemsInContainer = stateRef.current.items.filter(item => item && item.container === currentContainer);
|
|
247
|
+
const indexInContainer = itemsInContainer.findIndex(item => item && item.id === dragStateRef.current.draggedItemId);
|
|
248
|
+
const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
|
|
249
|
+
const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
|
|
250
|
+
|
|
251
|
+
onDrop(
|
|
252
|
+
dragStateRef.current.draggedItemId,
|
|
253
|
+
currentContainer,
|
|
254
|
+
dragStateRef.current.originalContainer,
|
|
255
|
+
updatedItem,
|
|
256
|
+
itemAbove,
|
|
257
|
+
itemBelow
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
dispatch({
|
|
262
|
+
type: 'RESET_DRAG_CONTAINER',
|
|
263
|
+
payload: {
|
|
264
|
+
itemId: dragStateRef.current.draggedItemId,
|
|
265
|
+
originalContainer: dragStateRef.current.originalContainer,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
|
|
270
|
+
dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
|
|
271
|
+
dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
|
|
272
|
+
|
|
273
|
+
// Clear drag state
|
|
274
|
+
dragStateRef.current = {
|
|
275
|
+
isDragging: false,
|
|
276
|
+
draggedItemId: '',
|
|
277
|
+
originalContainer: '',
|
|
278
|
+
currentContainer: '',
|
|
279
|
+
dropOccurred: false,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}, 50); // Small delay to let drop event fire if it's going to
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
document.addEventListener('dragover', handleGlobalDragOver);
|
|
287
|
+
document.addEventListener('drop', handleGlobalDrop);
|
|
288
|
+
document.addEventListener('mouseup', handleGlobalMouseUp);
|
|
289
|
+
document.addEventListener('pointerup', handleGlobalMouseUp);
|
|
290
|
+
|
|
291
|
+
return () => {
|
|
292
|
+
document.removeEventListener('dragover', handleGlobalDragOver);
|
|
293
|
+
document.removeEventListener('drop', handleGlobalDrop);
|
|
294
|
+
document.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
295
|
+
document.removeEventListener('pointerup', handleGlobalMouseUp);
|
|
296
|
+
};
|
|
297
|
+
}, [enableCrossContainerPreview]);
|
|
298
|
+
|
|
299
|
+
// Detect when dragging stops (isDragging goes from truthy to empty)
|
|
300
|
+
const prevIsDraggingRef = useRef(state.isDragging);
|
|
301
|
+
|
|
302
|
+
useEffect(() => {
|
|
303
|
+
if (!enableCrossContainerPreview) return;
|
|
304
|
+
|
|
305
|
+
const wasDragging = prevIsDraggingRef.current;
|
|
306
|
+
const isNowDragging = state.isDragging;
|
|
307
|
+
|
|
308
|
+
// Drag just ended (was dragging, now not)
|
|
309
|
+
if (wasDragging && !isNowDragging) {
|
|
310
|
+
|
|
311
|
+
// If drop didn't occur, reset to original container
|
|
312
|
+
if (!dragStateRef.current.dropOccurred && dragStateRef.current.draggedItemId) {
|
|
313
|
+
dispatch({
|
|
314
|
+
type: 'RESET_DRAG_CONTAINER',
|
|
315
|
+
payload: {
|
|
316
|
+
itemId: dragStateRef.current.draggedItemId,
|
|
317
|
+
originalContainer: dragStateRef.current.originalContainer,
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Clear drag state
|
|
323
|
+
dragStateRef.current = {
|
|
324
|
+
isDragging: false,
|
|
325
|
+
draggedItemId: '',
|
|
326
|
+
originalContainer: '',
|
|
327
|
+
currentContainer: '',
|
|
328
|
+
dropOccurred: false,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
prevIsDraggingRef.current = isNowDragging;
|
|
333
|
+
}, [state.isDragging, enableCrossContainerPreview]);
|
|
334
|
+
|
|
96
335
|
const handleDragStart = (id: string, container: string) => {
|
|
336
|
+
// Track drag in ref for global listener
|
|
337
|
+
if (enableCrossContainerPreview) {
|
|
338
|
+
dragStateRef.current = {
|
|
339
|
+
isDragging: true,
|
|
340
|
+
draggedItemId: id,
|
|
341
|
+
originalContainer: container,
|
|
342
|
+
currentContainer: container,
|
|
343
|
+
dropOccurred: false,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
97
347
|
dispatch({ type: 'SET_DRAG_DATA', payload: { id: id, initialGroup: container, originId: providerId } });
|
|
98
348
|
dispatch({ type: 'SET_IS_DRAGGING', payload: id });
|
|
99
349
|
if (onDragStart) onDragStart(id, container);
|
|
@@ -103,17 +353,93 @@ export const DraggableProvider = ({
|
|
|
103
353
|
if (state.dragData.originId !== providerId) return; // Ignore drag events from other providers
|
|
104
354
|
|
|
105
355
|
if (state.dragData.id !== id) {
|
|
106
|
-
|
|
107
|
-
|
|
356
|
+
if (enableCrossContainerPreview) {
|
|
357
|
+
// Used only when enableCrossContainerPreview is true
|
|
358
|
+
const draggedItem = state.items.find(
|
|
359
|
+
(item) => item && item.id === state.dragData.id
|
|
360
|
+
);
|
|
361
|
+
const currentContainer =
|
|
362
|
+
draggedItem && draggedItem.container
|
|
363
|
+
? draggedItem.container
|
|
364
|
+
: state.dragData.initialGroup;
|
|
365
|
+
|
|
366
|
+
const isCrossContainer =
|
|
367
|
+
currentContainer !== container &&
|
|
368
|
+
(currentContainer !== undefined || container !== undefined);
|
|
369
|
+
|
|
370
|
+
if (isCrossContainer) {
|
|
371
|
+
dispatch({
|
|
372
|
+
type: "REORDER_ITEMS_CROSS_CONTAINER",
|
|
373
|
+
payload: {
|
|
374
|
+
dragId: state.dragData.id,
|
|
375
|
+
targetId: id,
|
|
376
|
+
newContainer: container,
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
// Update current container in ref
|
|
380
|
+
if (enableCrossContainerPreview) {
|
|
381
|
+
dragStateRef.current.currentContainer = container;
|
|
382
|
+
}
|
|
383
|
+
} else {
|
|
384
|
+
// Same container: keep original behavior
|
|
385
|
+
dispatch({
|
|
386
|
+
type: "REORDER_ITEMS",
|
|
387
|
+
payload: { dragId: state.dragData.id, targetId: id },
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
} else {
|
|
391
|
+
// Original behavior (no preview across containers)
|
|
392
|
+
dispatch({type: "REORDER_ITEMS", payload: { dragId: state.dragData.id, targetId: id }});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// When enableCrossContainerPreview is true, preserve the original initialGroup
|
|
396
|
+
// Otherwise, update it to track the current container
|
|
397
|
+
const newInitialGroup = enableCrossContainerPreview ? state.dragData.initialGroup : container;
|
|
398
|
+
dispatch({type: "SET_DRAG_DATA",payload: {id: state.dragData.id, initialGroup: newInitialGroup, originId: providerId}});
|
|
108
399
|
}
|
|
109
400
|
if (onDragEnter) onDragEnter(id, container);
|
|
110
401
|
};
|
|
111
402
|
|
|
112
403
|
const handleDragEnd = () => {
|
|
404
|
+
const draggedItemId = state.dragData.id;
|
|
405
|
+
const originalContainer = state.dragData.initialGroup;
|
|
406
|
+
|
|
407
|
+
// If enableCrossContainerPreview is true and no drop occurred, reset item to original container
|
|
408
|
+
if (enableCrossContainerPreview && !dragStateRef.current.dropOccurred && draggedItemId && originalContainer) {
|
|
409
|
+
dispatch({ type: 'RESET_DRAG_CONTAINER', payload: { itemId: draggedItemId, originalContainer } });
|
|
410
|
+
}
|
|
411
|
+
|
|
113
412
|
dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
|
|
114
413
|
dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
|
|
115
414
|
dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
|
|
116
|
-
|
|
415
|
+
|
|
416
|
+
// Only call onDragEnd if drop didn't already occur (for enableCrossContainerPreview)
|
|
417
|
+
// If drop occurred, handleDrop or global drop handler already called onDragEnd
|
|
418
|
+
if (enableCrossContainerPreview && dragStateRef.current.dropOccurred) {
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (onDragEnd) {
|
|
423
|
+
if (!enableCrossContainerPreview) {
|
|
424
|
+
onDragEnd();
|
|
425
|
+
} else {
|
|
426
|
+
const draggedItem = stateRef.current.items.find(item => item && item.id === draggedItemId);
|
|
427
|
+
const finalContainer = draggedItem ? draggedItem.container : originalContainer;
|
|
428
|
+
|
|
429
|
+
const itemsInContainer = stateRef.current.items.filter(item => item && item.container === finalContainer);
|
|
430
|
+
const indexInContainer = itemsInContainer.findIndex(item => item && item.id === draggedItemId);
|
|
431
|
+
const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
|
|
432
|
+
const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
|
|
433
|
+
|
|
434
|
+
onDragEnd(
|
|
435
|
+
draggedItemId,
|
|
436
|
+
finalContainer,
|
|
437
|
+
originalContainer,
|
|
438
|
+
itemAbove,
|
|
439
|
+
itemBelow
|
|
440
|
+
);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
117
443
|
};
|
|
118
444
|
|
|
119
445
|
const changeCategory = (itemId: string, container: string) => {
|
|
@@ -123,10 +449,60 @@ export const DraggableProvider = ({
|
|
|
123
449
|
const handleDrop = (container: string) => {
|
|
124
450
|
if (state.dragData.originId !== providerId) return; // Ignore drop events from other providers
|
|
125
451
|
|
|
452
|
+
const draggedItemId = state.dragData.id;
|
|
453
|
+
const originalContainer = state.dragData.initialGroup;
|
|
454
|
+
|
|
455
|
+
// Mark drop as successful in ref for global listener
|
|
456
|
+
if (enableCrossContainerPreview) {
|
|
457
|
+
dragStateRef.current.dropOccurred = true;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Gather data for callbacks BEFORE clearing state
|
|
461
|
+
const isCrossContainer = container !== originalContainer;
|
|
462
|
+
let callbackData = null;
|
|
463
|
+
|
|
464
|
+
if (enableCrossContainerPreview) {
|
|
465
|
+
const draggedItem = stateRef.current.items.find(item => item && item.id === draggedItemId);
|
|
466
|
+
const updatedItem = draggedItem ? { ...draggedItem, container } : null;
|
|
467
|
+
const itemsInContainer = stateRef.current.items.filter(item => item && item.container === container);
|
|
468
|
+
const indexInContainer = itemsInContainer.findIndex(item => item && item.id === draggedItemId);
|
|
469
|
+
const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
|
|
470
|
+
const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
|
|
471
|
+
|
|
472
|
+
callbackData = { updatedItem, itemAbove, itemBelow };
|
|
473
|
+
}
|
|
474
|
+
|
|
126
475
|
dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
|
|
127
476
|
dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
|
|
477
|
+
dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
|
|
128
478
|
changeCategory(state.dragData.id, container);
|
|
129
|
-
|
|
479
|
+
|
|
480
|
+
if (onDrop) {
|
|
481
|
+
if (!enableCrossContainerPreview) {
|
|
482
|
+
onDrop(container);
|
|
483
|
+
} else {
|
|
484
|
+
onDrop(
|
|
485
|
+
draggedItemId,
|
|
486
|
+
container,
|
|
487
|
+
originalContainer,
|
|
488
|
+
callbackData.updatedItem,
|
|
489
|
+
callbackData.itemAbove,
|
|
490
|
+
callbackData.itemBelow
|
|
491
|
+
);
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Trigger onDragEnd ONLY for cross-container drops (dragend doesn't fire reliably in that case)
|
|
496
|
+
// For same-container drops, handleDragEnd will be called normally
|
|
497
|
+
if (enableCrossContainerPreview && isCrossContainer && onDragEnd && callbackData) {
|
|
498
|
+
onDragEnd(
|
|
499
|
+
draggedItemId,
|
|
500
|
+
container,
|
|
501
|
+
originalContainer,
|
|
502
|
+
callbackData.itemAbove,
|
|
503
|
+
callbackData.itemBelow
|
|
504
|
+
);
|
|
505
|
+
}
|
|
130
506
|
};
|
|
131
507
|
|
|
132
508
|
const handleDragOver = (e: Event, container: string) => {
|
|
@@ -134,6 +510,22 @@ export const DraggableProvider = ({
|
|
|
134
510
|
|
|
135
511
|
e.preventDefault();
|
|
136
512
|
dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: container });
|
|
513
|
+
|
|
514
|
+
if (enableCrossContainerPreview && state.dragData.id) {
|
|
515
|
+
// Only when enableCrossContainerPreview is true: when hovering over a different container, move item to end
|
|
516
|
+
const draggedItem = state.items.find(
|
|
517
|
+
(item) => item && item.id === state.dragData.id
|
|
518
|
+
);
|
|
519
|
+
if (draggedItem && draggedItem.container !== container) {
|
|
520
|
+
dispatch({
|
|
521
|
+
type: "MOVE_TO_CONTAINER_END",
|
|
522
|
+
payload: { dragId: state.dragData.id, newContainer: container },
|
|
523
|
+
});
|
|
524
|
+
// Update current container in ref
|
|
525
|
+
dragStateRef.current.currentContainer = container;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
137
529
|
if (onDragOver) onDragOver(e, container);
|
|
138
530
|
};
|
|
139
531
|
|
|
@@ -157,4 +549,4 @@ export const DraggableProvider = ({
|
|
|
157
549
|
return (
|
|
158
550
|
<DragContext.Provider value={contextValue}>{children}</DragContext.Provider>
|
|
159
551
|
);
|
|
160
|
-
};
|
|
552
|
+
};
|
|
@@ -18,8 +18,12 @@ export type ActionType =
|
|
|
18
18
|
} }
|
|
19
19
|
| { type: 'SET_IS_DRAGGING'; payload: string }
|
|
20
20
|
| { type: 'SET_ACTIVE_CONTAINER'; payload: string }
|
|
21
|
+
| { type: 'SET_CROSS_CONTAINER_PREVIEW'; payload: boolean }
|
|
21
22
|
| { type: 'CHANGE_CATEGORY'; payload: { itemId: string; container: string } }
|
|
22
|
-
| { type: 'REORDER_ITEMS'; payload: { dragId: string; targetId: string } }
|
|
23
|
+
| { type: 'REORDER_ITEMS'; payload: { dragId: string; targetId: string } }
|
|
24
|
+
| { type: 'REORDER_ITEMS_CROSS_CONTAINER'; payload: { dragId: string; targetId: string; newContainer: string } }
|
|
25
|
+
| { type: 'MOVE_TO_CONTAINER_END'; payload: { dragId: string; newContainer: string } }
|
|
26
|
+
| { type: 'RESET_DRAG_CONTAINER'; payload: { itemId: string; originalContainer: string } };
|
|
23
27
|
|
|
24
28
|
export interface DropZoneConfig {
|
|
25
29
|
type?: 'ghost' | 'outline' | 'shadow' | 'line';
|
|
@@ -33,9 +37,10 @@ export type ActionType =
|
|
|
33
37
|
onReorder: (items: ItemType[]) => void;
|
|
34
38
|
onDragStart?: (id: string, container: string) => void;
|
|
35
39
|
onDragEnter?: (id: string, container: string) => void;
|
|
36
|
-
onDragEnd?: () => void;
|
|
37
|
-
onDrop?: (
|
|
40
|
+
onDragEnd?: (...args: any[]) => void;
|
|
41
|
+
onDrop?: (...args: any[]) => void;
|
|
38
42
|
onDragOver?: (e: Event, container: string) => void;
|
|
39
43
|
dropZone?: DropZoneConfig | string; // Can accept string for backward compatibility
|
|
40
44
|
providerId?: string;
|
|
45
|
+
enableCrossContainerPreview?: boolean;
|
|
41
46
|
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
|
|
3
|
+
import Flex from '../../pb_flex/_flex'
|
|
4
|
+
import Draggable from '../../pb_draggable/_draggable'
|
|
5
|
+
import { DraggableProvider } from '../../pb_draggable/context'
|
|
6
|
+
import Badge from '../../pb_badge/_badge'
|
|
7
|
+
import Title from '../../pb_title/_title'
|
|
8
|
+
import Caption from '../../pb_caption/_caption'
|
|
9
|
+
import Card from '../../pb_card/_card'
|
|
10
|
+
import FlexItem from '../../pb_flex/_flex_item'
|
|
11
|
+
import Avatar from '../../pb_avatar/_avatar'
|
|
12
|
+
import Body from '../../pb_body/_body'
|
|
13
|
+
|
|
14
|
+
// Initial groups to drag between
|
|
15
|
+
const containers = ["To Do", "In Progress", "Done"];
|
|
16
|
+
|
|
17
|
+
// Initial items to be dragged
|
|
18
|
+
const data = [
|
|
19
|
+
{
|
|
20
|
+
id: "11",
|
|
21
|
+
container: "To Do",
|
|
22
|
+
title: "Task 1",
|
|
23
|
+
description: "Bug fixes",
|
|
24
|
+
assignee_name: "Terry Miles",
|
|
25
|
+
assignee_img: "https://randomuser.me/api/portraits/men/44.jpg",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "12",
|
|
29
|
+
container: "To Do",
|
|
30
|
+
title: "Task 2",
|
|
31
|
+
description: "Documentation",
|
|
32
|
+
assignee_name: "Sophia Miles",
|
|
33
|
+
assignee_img: "https://randomuser.me/api/portraits/women/8.jpg",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: "13",
|
|
37
|
+
container: "In Progress",
|
|
38
|
+
title: "Task 3",
|
|
39
|
+
description: "Add a variant",
|
|
40
|
+
assignee_name: "Alice Jones",
|
|
41
|
+
assignee_img: "https://randomuser.me/api/portraits/women/10.jpg",
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: "14",
|
|
45
|
+
container: "To Do",
|
|
46
|
+
title: "Task 4",
|
|
47
|
+
description: "Add jest tests",
|
|
48
|
+
assignee_name: "Mike James",
|
|
49
|
+
assignee_img: "https://randomuser.me/api/portraits/men/8.jpg",
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: "15",
|
|
53
|
+
container: "Done",
|
|
54
|
+
title: "Task 5",
|
|
55
|
+
description: "Alpha testing",
|
|
56
|
+
assignee_name: "James Guy",
|
|
57
|
+
assignee_img: "https://randomuser.me/api/portraits/men/18.jpg",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "16",
|
|
61
|
+
container: "In Progress",
|
|
62
|
+
title: "Task 6",
|
|
63
|
+
description: "Release",
|
|
64
|
+
assignee_name: "Sally Jones",
|
|
65
|
+
assignee_img: "https://randomuser.me/api/portraits/women/28.jpg",
|
|
66
|
+
},
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
const DraggableMultipleContainersDropzone = (props) => {
|
|
70
|
+
const [initialState, setInitialState] = useState(data);
|
|
71
|
+
|
|
72
|
+
const badgeProperties = (container) => {
|
|
73
|
+
switch (container) {
|
|
74
|
+
case "To Do":
|
|
75
|
+
return { text: "queue", color: "warning" };
|
|
76
|
+
case "In Progress":
|
|
77
|
+
return { text: "progress", color: "primary" };
|
|
78
|
+
default:
|
|
79
|
+
return { text: "done", color: "success" };
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<DraggableProvider
|
|
85
|
+
dropZone={{type: "outline"}}
|
|
86
|
+
enableCrossContainerPreview
|
|
87
|
+
initialItems={data}
|
|
88
|
+
onDragEnd={(draggedItemId, finalContainer, originalContainer, itemAbove, itemBelow) => {
|
|
89
|
+
console.log(`Dragged Item ID: ${draggedItemId}`);
|
|
90
|
+
console.log(`Final Container: ${finalContainer}`);
|
|
91
|
+
console.log(`Original Container: ${originalContainer}`);
|
|
92
|
+
console.log('Item Above:', itemAbove);
|
|
93
|
+
console.log('Item Below:', itemBelow);
|
|
94
|
+
}}
|
|
95
|
+
onDrop={(draggedItemId, droppedContainer, originalContainer, item, itemAbove, itemBelow) => {
|
|
96
|
+
console.log(`Dragged Item ID: ${draggedItemId}`);
|
|
97
|
+
console.log(`Dropped Container: ${droppedContainer}`);
|
|
98
|
+
console.log(`Original Container: ${originalContainer}`);
|
|
99
|
+
console.log('Dropped Item:', item);
|
|
100
|
+
console.log('Item Above:', itemAbove);
|
|
101
|
+
console.log('Item Below:', itemBelow);
|
|
102
|
+
}}
|
|
103
|
+
onReorder={(items) => setInitialState(items)}
|
|
104
|
+
>
|
|
105
|
+
<Flex
|
|
106
|
+
justifyContent="center"
|
|
107
|
+
{...props}
|
|
108
|
+
>
|
|
109
|
+
{containers?.map((container) => (
|
|
110
|
+
<Draggable.Container
|
|
111
|
+
container={container}
|
|
112
|
+
htmlOptions={{style:{ width: "200px", height: "70vh"}}}
|
|
113
|
+
key={container}
|
|
114
|
+
padding="sm"
|
|
115
|
+
>
|
|
116
|
+
<Caption textAlign="center">{container}</Caption>
|
|
117
|
+
<Flex
|
|
118
|
+
alignItems="stretch"
|
|
119
|
+
gap="sm"
|
|
120
|
+
orientation="column"
|
|
121
|
+
>
|
|
122
|
+
{initialState
|
|
123
|
+
.filter((item) => item.container === container)
|
|
124
|
+
.map(
|
|
125
|
+
({
|
|
126
|
+
assignee_img,
|
|
127
|
+
assignee_name,
|
|
128
|
+
description,
|
|
129
|
+
id,
|
|
130
|
+
title,
|
|
131
|
+
}) => (
|
|
132
|
+
<Draggable.Item
|
|
133
|
+
container={container}
|
|
134
|
+
dragId={id}
|
|
135
|
+
key={id}
|
|
136
|
+
>
|
|
137
|
+
<Card
|
|
138
|
+
padding="sm"
|
|
139
|
+
{...props}
|
|
140
|
+
>
|
|
141
|
+
<Flex justify="between">
|
|
142
|
+
<FlexItem>
|
|
143
|
+
<Flex>
|
|
144
|
+
<Avatar
|
|
145
|
+
imageUrl={assignee_img}
|
|
146
|
+
name={assignee_name}
|
|
147
|
+
size="xxs"
|
|
148
|
+
/>
|
|
149
|
+
<Title paddingLeft="xs"
|
|
150
|
+
size={4}
|
|
151
|
+
text={title}
|
|
152
|
+
{...props}
|
|
153
|
+
/>
|
|
154
|
+
</Flex>
|
|
155
|
+
</FlexItem>
|
|
156
|
+
<Badge
|
|
157
|
+
marginLeft="sm"
|
|
158
|
+
rounded
|
|
159
|
+
text={badgeProperties(container).text}
|
|
160
|
+
variant={badgeProperties(container).color}
|
|
161
|
+
{...props}
|
|
162
|
+
/>
|
|
163
|
+
</Flex>
|
|
164
|
+
<Body paddingTop="xs"
|
|
165
|
+
text={description}
|
|
166
|
+
{...props}
|
|
167
|
+
/>
|
|
168
|
+
</Card>
|
|
169
|
+
</Draggable.Item>
|
|
170
|
+
)
|
|
171
|
+
)}
|
|
172
|
+
</Flex>
|
|
173
|
+
</Draggable.Container>
|
|
174
|
+
))}
|
|
175
|
+
</Flex>
|
|
176
|
+
</DraggableProvider>
|
|
177
|
+
);
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
export default DraggableMultipleContainersDropzone;
|