playbook_ui 15.6.0.pre.alpha.play266013023 → 15.6.0.pre.rc.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.
- checksums.yaml +4 -4
- data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_table_props.html.erb +163 -1
- data/app/pb_kits/playbook/pb_advanced_table/docs/_advanced_table_table_props.jsx +190 -0
- data/app/pb_kits/playbook/pb_background/_background.tsx +6 -6
- data/app/pb_kits/playbook/pb_background/background.test.js +1 -5
- 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 +1 -0
- data/app/pb_kits/playbook/pb_background/docs/example.yml +2 -2
- data/app/pb_kits/playbook/pb_card/docs/_card_header.md +1 -1
- data/app/pb_kits/playbook/pb_card/docs/_card_highlight.md +1 -1
- data/app/pb_kits/playbook/pb_collapsible/__snapshots__/collapsible.test.js.snap +2 -2
- data/app/pb_kits/playbook/pb_collapsible/child_kits/CollapsibleIcon.tsx +8 -10
- data/app/pb_kits/playbook/pb_collapsible/docs/_collapsible_icons.jsx +1 -0
- data/app/pb_kits/playbook/pb_collapsible/docs/_collapsible_state.jsx +3 -0
- data/app/pb_kits/playbook/pb_date_picker/date_picker.test.js +0 -24
- data/app/pb_kits/playbook/pb_date_picker/date_picker_helper.ts +3 -181
- data/app/pb_kits/playbook/pb_dialog/_dialog.tsx +1 -2
- data/app/pb_kits/playbook/pb_dialog/dialog.html.erb +1 -1
- data/app/pb_kits/playbook/pb_dialog/dialog.rb +0 -1
- data/app/pb_kits/playbook/pb_dialog/dialog.test.jsx +0 -14
- data/app/pb_kits/playbook/pb_dialog/dialog_header.html.erb +4 -5
- data/app/pb_kits/playbook/pb_dialog/dialog_header.rb +0 -2
- data/app/pb_kits/playbook/pb_dialog/docs/_dialog_compound_components.html.erb +0 -31
- data/app/pb_kits/playbook/pb_dialog/docs/example.yml +0 -2
- data/app/pb_kits/playbook/pb_dialog/docs/index.js +1 -2
- data/app/pb_kits/playbook/pb_distribution_bar/docs/_distribution_bar_custom_colors.md +1 -1
- data/app/pb_kits/playbook/pb_draggable/context/index.tsx +7 -458
- data/app/pb_kits/playbook/pb_draggable/context/types.ts +3 -8
- data/app/pb_kits/playbook/pb_draggable/docs/example.yml +2 -3
- data/app/pb_kits/playbook/pb_draggable/docs/index.js +1 -2
- data/app/pb_kits/playbook/pb_draggable/draggable.test.jsx +1 -77
- 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 +22 -34
- data/app/pb_kits/playbook/pb_home_address_street/city_emphasis.html.erb +12 -16
- 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 +12 -16
- data/app/pb_kits/playbook/pb_home_address_street/street_emphasis.html.erb +12 -16
- data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.scss +0 -10
- data/app/pb_kits/playbook/pb_multiple_users/_multiple_users.tsx +15 -66
- data/app/pb_kits/playbook/pb_multiple_users/docs/example.yml +0 -1
- data/app/pb_kits/playbook/pb_multiple_users/docs/index.js +0 -1
- data/app/pb_kits/playbook/pb_multiple_users/multiple_users.test.js +0 -25
- data/app/pb_kits/playbook/pb_phone_number_input/_phone_number_input.tsx +10 -44
- data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.html.erb +4 -34
- data/app/pb_kits/playbook/pb_phone_number_input/docs/_phone_number_input_validation.jsx +7 -16
- data/app/pb_kits/playbook/pb_radio/docs/_radio_error.md +1 -1
- data/app/pb_kits/playbook/pb_select/_select.tsx +3 -8
- data/app/pb_kits/playbook/pb_select/docs/_select_error.md +1 -1
- data/app/pb_kits/playbook/pb_select/docs/example.yml +0 -2
- data/app/pb_kits/playbook/pb_select/docs/index.js +0 -1
- data/app/pb_kits/playbook/pb_select/select.html.erb +2 -2
- data/app/pb_kits/playbook/pb_select/select.rb +1 -3
- data/app/pb_kits/playbook/pb_select/select.test.js +0 -23
- 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_timeline/_item.tsx +0 -3
- data/app/pb_kits/playbook/pb_timeline/docs/_timeline_with_date.md +1 -1
- data/app/pb_kits/playbook/pb_timeline/docs/example.yml +0 -2
- data/app/pb_kits/playbook/pb_timeline/docs/index.js +0 -1
- data/app/pb_kits/playbook/pb_timeline/item.html.erb +1 -1
- data/app/pb_kits/playbook/pb_timeline/item.rb +0 -2
- data/app/pb_kits/playbook/pb_timeline/label.html.erb +1 -2
- data/app/pb_kits/playbook/pb_timeline/label.rb +0 -2
- data/app/pb_kits/playbook/pb_timeline/subcomponents/Label.tsx +0 -3
- data/app/pb_kits/playbook/pb_timeline/timeline.test.js +0 -51
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.test.jsx +0 -15
- data/app/pb_kits/playbook/pb_typeahead/_typeahead.tsx +0 -3
- data/app/pb_kits/playbook/pb_typeahead/components/ClearIndicator.tsx +2 -13
- data/app/pb_kits/playbook/pb_typeahead/components/MultiValue.tsx +1 -6
- data/app/pb_kits/playbook/pb_typeahead/components/ValueContainer.tsx +7 -34
- data/app/pb_kits/playbook/pb_typeahead/docs/example.yml +0 -2
- data/app/pb_kits/playbook/pb_typeahead/docs/index.js +1 -2
- data/app/pb_kits/playbook/pb_typeahead/typeahead.rb +1 -6
- data/app/pb_kits/playbook/tokens/_colors.scss +1 -2
- data/dist/chunks/_typeahead-kRdz5zPn.js +6 -0
- data/dist/chunks/lib-CgpqUb6l.js +29 -0
- 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/forms/builder/collection_select_field.rb +1 -9
- data/lib/playbook/forms/builder/select_field.rb +1 -9
- data/lib/playbook/forms/builder/time_zone_select_field.rb +1 -9
- data/lib/playbook/pb_kit_helper.rb +0 -35
- data/lib/playbook/version.rb +2 -2
- metadata +4 -22
- data/app/pb_kits/playbook/pb_background/docs/_background_light.md +0 -1
- data/app/pb_kits/playbook/pb_dialog/docs/_dialog_closeable.html.erb +0 -24
- data/app/pb_kits/playbook/pb_dialog/docs/_dialog_closeable.jsx +0 -60
- data/app/pb_kits/playbook/pb_dialog/docs/_dialog_closeable.md +0 -3
- data/app/pb_kits/playbook/pb_draggable/docs/_draggable_multiple_containers_dropzone.jsx +0 -180
- data/app/pb_kits/playbook/pb_draggable/docs/_draggable_multiple_containers_dropzone.md +0 -22
- data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.jsx +0 -42
- data/app/pb_kits/playbook/pb_multiple_users/docs/_multiple_users_with_tooltip.md +0 -1
- data/app/pb_kits/playbook/pb_select/docs/_select_input_options.html.erb +0 -16
- data/app/pb_kits/playbook/pb_select/docs/_select_input_options.jsx +0 -30
- data/app/pb_kits/playbook/pb_select/docs/_select_input_options.md +0 -1
- data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.html.erb +0 -60
- data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.jsx +0 -118
- data/app/pb_kits/playbook/pb_timeline/docs/_timeline_show_current_year.md +0 -1
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.html.erb +0 -30
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.jsx +0 -37
- data/app/pb_kits/playbook/pb_typeahead/docs/_typeahead_input_display.md +0 -3
- data/app/pb_kits/playbook/utilities/deprecated.ts +0 -73
- data/dist/chunks/_typeahead-CYNrKU10.js +0 -6
- data/dist/chunks/lib-DDDLiZuu.js +0 -29
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext, useReducer, useContext, useEffect, useMemo
|
|
1
|
+
import React, { createContext, useReducer, useContext, useEffect, useMemo } from "react";
|
|
2
2
|
import { InitialStateType, ActionType, DraggableProviderType } from "./types";
|
|
3
3
|
|
|
4
4
|
const initialState: InitialStateType = {
|
|
@@ -39,88 +39,6 @@ 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 and position (e.g., when drag ends without valid drop)
|
|
96
|
-
case "RESET_DRAG_CONTAINER": {
|
|
97
|
-
const { itemId, originalContainer, originalIndex } = action.payload;
|
|
98
|
-
const newItems = [...state.items];
|
|
99
|
-
const draggedItem = newItems.find(item => item && item.id === itemId);
|
|
100
|
-
|
|
101
|
-
if (!draggedItem) return state;
|
|
102
|
-
|
|
103
|
-
const currentIndex = newItems.indexOf(draggedItem);
|
|
104
|
-
|
|
105
|
-
// Remove from current position
|
|
106
|
-
newItems.splice(currentIndex, 1);
|
|
107
|
-
|
|
108
|
-
// Restore container property and insert at original index
|
|
109
|
-
const restoredItem = { ...draggedItem, container: originalContainer };
|
|
110
|
-
|
|
111
|
-
// Insert at original index, or at end if index is invalid
|
|
112
|
-
if (originalIndex !== undefined && originalIndex >= 0) {
|
|
113
|
-
newItems.splice(originalIndex, 0, restoredItem);
|
|
114
|
-
} else {
|
|
115
|
-
newItems.push(restoredItem);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
return {
|
|
119
|
-
...state,
|
|
120
|
-
items: newItems
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
42
|
default:
|
|
125
43
|
return state;
|
|
126
44
|
}
|
|
@@ -143,34 +61,9 @@ export const DraggableProvider = ({
|
|
|
143
61
|
onDrop,
|
|
144
62
|
onDragOver,
|
|
145
63
|
dropZone = { type: 'ghost', color: 'neutral', direction: 'vertical' },
|
|
146
|
-
providerId = 'default', // fallback provided for backward compatibility
|
|
147
|
-
// Opt-in flag for cross-container preview
|
|
148
|
-
enableCrossContainerPreview = false,
|
|
64
|
+
providerId = 'default', // fallback provided for backward compatibility, so this does not become a required prop
|
|
149
65
|
}: DraggableProviderType) => {
|
|
150
66
|
const [state, dispatch] = useReducer(reducer, initialState);
|
|
151
|
-
|
|
152
|
-
// Track drag state for global listener
|
|
153
|
-
const dragStateRef = useRef<{
|
|
154
|
-
isDragging: boolean;
|
|
155
|
-
draggedItemId: string;
|
|
156
|
-
originalContainer: string;
|
|
157
|
-
originalIndex: number;
|
|
158
|
-
currentContainer: string;
|
|
159
|
-
dropOccurred: boolean;
|
|
160
|
-
}>({
|
|
161
|
-
isDragging: false,
|
|
162
|
-
draggedItemId: '',
|
|
163
|
-
originalContainer: '',
|
|
164
|
-
originalIndex: -1,
|
|
165
|
-
currentContainer: '',
|
|
166
|
-
dropOccurred: false,
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// Track current state for use in gated event listeners (avoid stale closures)
|
|
170
|
-
const stateRef = useRef(state);
|
|
171
|
-
useEffect(() => {
|
|
172
|
-
stateRef.current = state;
|
|
173
|
-
}, [state]);
|
|
174
67
|
|
|
175
68
|
// Parse dropZone prop - handle both string format (backward compatibility) and object format
|
|
176
69
|
let dropZoneType = 'ghost';
|
|
@@ -200,209 +93,7 @@ export const DraggableProvider = ({
|
|
|
200
93
|
onReorder(state.items);
|
|
201
94
|
}, [state.items]);
|
|
202
95
|
|
|
203
|
-
// Monitor for failed drops by detecting mouse/pointer release during drag (this is needed for cross container preview)
|
|
204
|
-
useEffect(() => {
|
|
205
|
-
if (!enableCrossContainerPreview) return;
|
|
206
|
-
|
|
207
|
-
// Allow drops anywhere on the document by preventing default dragover
|
|
208
|
-
const handleGlobalDragOver = (e: DragEvent) => {
|
|
209
|
-
if (dragStateRef.current.isDragging) {
|
|
210
|
-
e.preventDefault();
|
|
211
|
-
}
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
// Handle drops anywhere on the document (including non-container areas)
|
|
215
|
-
const handleGlobalDrop = (e: DragEvent) => {
|
|
216
|
-
if (!dragStateRef.current.isDragging) return;
|
|
217
|
-
|
|
218
|
-
// If a container already handled the drop, don't process again
|
|
219
|
-
if (dragStateRef.current.dropOccurred) return;
|
|
220
|
-
|
|
221
|
-
e.preventDefault();
|
|
222
|
-
|
|
223
|
-
// If we reach here, it means the drop was NOT on a valid container
|
|
224
|
-
// (otherwise the container's handleDrop would have set dropOccurred = true)
|
|
225
|
-
// So we should ALWAYS reset to original container for invalid drops
|
|
226
|
-
const originalContainer = dragStateRef.current.originalContainer;
|
|
227
|
-
|
|
228
|
-
dispatch({
|
|
229
|
-
type: 'RESET_DRAG_CONTAINER',
|
|
230
|
-
payload: {
|
|
231
|
-
itemId: dragStateRef.current.draggedItemId,
|
|
232
|
-
originalContainer: originalContainer,
|
|
233
|
-
originalIndex: dragStateRef.current.originalIndex,
|
|
234
|
-
},
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
|
|
238
|
-
dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
|
|
239
|
-
dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
|
|
240
|
-
|
|
241
|
-
// Clear drag state
|
|
242
|
-
dragStateRef.current = {
|
|
243
|
-
isDragging: false,
|
|
244
|
-
draggedItemId: '',
|
|
245
|
-
originalContainer: '',
|
|
246
|
-
originalIndex: -1,
|
|
247
|
-
currentContainer: '',
|
|
248
|
-
dropOccurred: false,
|
|
249
|
-
};
|
|
250
|
-
};
|
|
251
|
-
|
|
252
|
-
const handleGlobalMouseUp = () => {
|
|
253
|
-
// If we're dragging and mouse is released, wait a bit to see if drop occurs
|
|
254
|
-
if (dragStateRef.current.isDragging) {
|
|
255
|
-
setTimeout(() => {
|
|
256
|
-
const currentContainer = dragStateRef.current.currentContainer;
|
|
257
|
-
|
|
258
|
-
// If drop still hasn't occurred, check if item is in a different container
|
|
259
|
-
if (dragStateRef.current.isDragging && !dragStateRef.current.dropOccurred) {
|
|
260
|
-
// If item is in a different container than original, treat it as a successful drop
|
|
261
|
-
if (currentContainer && currentContainer !== dragStateRef.current.originalContainer) {
|
|
262
|
-
// Trigger onDrop callback with the current container
|
|
263
|
-
if (onDrop) {
|
|
264
|
-
const draggedItem = stateRef.current.items.find(item => item && item.id === dragStateRef.current.draggedItemId);
|
|
265
|
-
const updatedItem = draggedItem ? { ...draggedItem, container: currentContainer } : null;
|
|
266
|
-
const itemsInContainer = stateRef.current.items.filter(item => item && item.container === currentContainer);
|
|
267
|
-
const indexInContainer = itemsInContainer.findIndex(item => item && item.id === dragStateRef.current.draggedItemId);
|
|
268
|
-
const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
|
|
269
|
-
const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
|
|
270
|
-
|
|
271
|
-
onDrop(
|
|
272
|
-
dragStateRef.current.draggedItemId,
|
|
273
|
-
currentContainer,
|
|
274
|
-
dragStateRef.current.originalContainer,
|
|
275
|
-
updatedItem,
|
|
276
|
-
itemAbove,
|
|
277
|
-
itemBelow
|
|
278
|
-
);
|
|
279
|
-
}
|
|
280
|
-
} else {
|
|
281
|
-
dispatch({
|
|
282
|
-
type: 'RESET_DRAG_CONTAINER',
|
|
283
|
-
payload: {
|
|
284
|
-
itemId: dragStateRef.current.draggedItemId,
|
|
285
|
-
originalContainer: dragStateRef.current.originalContainer,
|
|
286
|
-
originalIndex: dragStateRef.current.originalIndex,
|
|
287
|
-
},
|
|
288
|
-
});
|
|
289
|
-
}
|
|
290
|
-
dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
|
|
291
|
-
dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
|
|
292
|
-
dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
|
|
293
|
-
|
|
294
|
-
// Clear drag state
|
|
295
|
-
dragStateRef.current = {
|
|
296
|
-
isDragging: false,
|
|
297
|
-
draggedItemId: '',
|
|
298
|
-
originalContainer: '',
|
|
299
|
-
originalIndex: -1,
|
|
300
|
-
currentContainer: '',
|
|
301
|
-
dropOccurred: false,
|
|
302
|
-
};
|
|
303
|
-
}
|
|
304
|
-
}, 50); // Small delay to let drop event fire if it's going to
|
|
305
|
-
}
|
|
306
|
-
};
|
|
307
|
-
|
|
308
|
-
// Detect when drag leaves document boundaries
|
|
309
|
-
const handleDragLeave = (e: DragEvent) => {
|
|
310
|
-
// Check if we're leaving the document (relatedTarget will be null)
|
|
311
|
-
if (!e.relatedTarget && dragStateRef.current.isDragging && !dragStateRef.current.dropOccurred) {
|
|
312
|
-
// Drag left the document: reset to original container immediately
|
|
313
|
-
dispatch({
|
|
314
|
-
type: 'RESET_DRAG_CONTAINER',
|
|
315
|
-
payload: {
|
|
316
|
-
itemId: dragStateRef.current.draggedItemId,
|
|
317
|
-
originalContainer: dragStateRef.current.originalContainer,
|
|
318
|
-
originalIndex: dragStateRef.current.originalIndex,
|
|
319
|
-
},
|
|
320
|
-
});
|
|
321
|
-
dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
|
|
322
|
-
dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
|
|
323
|
-
dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
|
|
324
|
-
|
|
325
|
-
// Clear drag state
|
|
326
|
-
dragStateRef.current = {
|
|
327
|
-
isDragging: false,
|
|
328
|
-
draggedItemId: '',
|
|
329
|
-
originalContainer: '',
|
|
330
|
-
originalIndex: -1,
|
|
331
|
-
currentContainer: '',
|
|
332
|
-
dropOccurred: false,
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
|
|
337
|
-
document.addEventListener('dragover', handleGlobalDragOver);
|
|
338
|
-
document.addEventListener('drop', handleGlobalDrop);
|
|
339
|
-
document.addEventListener('dragleave', handleDragLeave);
|
|
340
|
-
document.addEventListener('mouseup', handleGlobalMouseUp);
|
|
341
|
-
document.addEventListener('pointerup', handleGlobalMouseUp);
|
|
342
|
-
|
|
343
|
-
return () => {
|
|
344
|
-
document.removeEventListener('dragover', handleGlobalDragOver);
|
|
345
|
-
document.removeEventListener('drop', handleGlobalDrop);
|
|
346
|
-
document.removeEventListener('dragleave', handleDragLeave);
|
|
347
|
-
document.removeEventListener('mouseup', handleGlobalMouseUp);
|
|
348
|
-
document.removeEventListener('pointerup', handleGlobalMouseUp);
|
|
349
|
-
};
|
|
350
|
-
}, [enableCrossContainerPreview]);
|
|
351
|
-
|
|
352
|
-
// Detect when dragging stops (isDragging goes from truthy to empty)
|
|
353
|
-
const prevIsDraggingRef = useRef(state.isDragging);
|
|
354
|
-
|
|
355
|
-
useEffect(() => {
|
|
356
|
-
if (!enableCrossContainerPreview) return;
|
|
357
|
-
|
|
358
|
-
const wasDragging = prevIsDraggingRef.current;
|
|
359
|
-
const isNowDragging = state.isDragging;
|
|
360
|
-
|
|
361
|
-
// Drag just ended (was dragging, now not)
|
|
362
|
-
if (wasDragging && !isNowDragging) {
|
|
363
|
-
|
|
364
|
-
// If drop didn't occur, reset to original container
|
|
365
|
-
if (!dragStateRef.current.dropOccurred && dragStateRef.current.draggedItemId) {
|
|
366
|
-
dispatch({
|
|
367
|
-
type: 'RESET_DRAG_CONTAINER',
|
|
368
|
-
payload: {
|
|
369
|
-
itemId: dragStateRef.current.draggedItemId,
|
|
370
|
-
originalContainer: dragStateRef.current.originalContainer,
|
|
371
|
-
originalIndex: dragStateRef.current.originalIndex,
|
|
372
|
-
},
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Clear drag state
|
|
377
|
-
dragStateRef.current = {
|
|
378
|
-
isDragging: false,
|
|
379
|
-
draggedItemId: '',
|
|
380
|
-
originalContainer: '',
|
|
381
|
-
originalIndex: -1,
|
|
382
|
-
currentContainer: '',
|
|
383
|
-
dropOccurred: false,
|
|
384
|
-
};
|
|
385
|
-
}
|
|
386
|
-
|
|
387
|
-
prevIsDraggingRef.current = isNowDragging;
|
|
388
|
-
}, [state.isDragging, enableCrossContainerPreview]);
|
|
389
|
-
|
|
390
96
|
const handleDragStart = (id: string, container: string) => {
|
|
391
|
-
// Track drag in ref for global listener
|
|
392
|
-
if (enableCrossContainerPreview) {
|
|
393
|
-
// Find the original index of the item
|
|
394
|
-
const originalIndex = state.items.findIndex(item => item && item.id === id);
|
|
395
|
-
|
|
396
|
-
dragStateRef.current = {
|
|
397
|
-
isDragging: true,
|
|
398
|
-
draggedItemId: id,
|
|
399
|
-
originalContainer: container,
|
|
400
|
-
originalIndex: originalIndex,
|
|
401
|
-
currentContainer: container,
|
|
402
|
-
dropOccurred: false,
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
|
|
406
97
|
dispatch({ type: 'SET_DRAG_DATA', payload: { id: id, initialGroup: container, originId: providerId } });
|
|
407
98
|
dispatch({ type: 'SET_IS_DRAGGING', payload: id });
|
|
408
99
|
if (onDragStart) onDragStart(id, container);
|
|
@@ -412,93 +103,17 @@ export const DraggableProvider = ({
|
|
|
412
103
|
if (state.dragData.originId !== providerId) return; // Ignore drag events from other providers
|
|
413
104
|
|
|
414
105
|
if (state.dragData.id !== id) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const draggedItem = state.items.find(
|
|
418
|
-
(item) => item && item.id === state.dragData.id
|
|
419
|
-
);
|
|
420
|
-
const currentContainer =
|
|
421
|
-
draggedItem && draggedItem.container
|
|
422
|
-
? draggedItem.container
|
|
423
|
-
: state.dragData.initialGroup;
|
|
424
|
-
|
|
425
|
-
const isCrossContainer =
|
|
426
|
-
currentContainer !== container &&
|
|
427
|
-
(currentContainer !== undefined || container !== undefined);
|
|
428
|
-
|
|
429
|
-
if (isCrossContainer) {
|
|
430
|
-
dispatch({
|
|
431
|
-
type: "REORDER_ITEMS_CROSS_CONTAINER",
|
|
432
|
-
payload: {
|
|
433
|
-
dragId: state.dragData.id,
|
|
434
|
-
targetId: id,
|
|
435
|
-
newContainer: container,
|
|
436
|
-
},
|
|
437
|
-
});
|
|
438
|
-
// Update current container in ref
|
|
439
|
-
if (enableCrossContainerPreview) {
|
|
440
|
-
dragStateRef.current.currentContainer = container;
|
|
441
|
-
}
|
|
442
|
-
} else {
|
|
443
|
-
// Same container: keep original behavior
|
|
444
|
-
dispatch({
|
|
445
|
-
type: "REORDER_ITEMS",
|
|
446
|
-
payload: { dragId: state.dragData.id, targetId: id },
|
|
447
|
-
});
|
|
448
|
-
}
|
|
449
|
-
} else {
|
|
450
|
-
// Original behavior (no preview across containers)
|
|
451
|
-
dispatch({type: "REORDER_ITEMS", payload: { dragId: state.dragData.id, targetId: id }});
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// When enableCrossContainerPreview is true, preserve the original initialGroup
|
|
455
|
-
// Otherwise, update it to track the current container
|
|
456
|
-
const newInitialGroup = enableCrossContainerPreview ? state.dragData.initialGroup : container;
|
|
457
|
-
dispatch({type: "SET_DRAG_DATA",payload: {id: state.dragData.id, initialGroup: newInitialGroup, originId: providerId}});
|
|
106
|
+
dispatch({ type: 'REORDER_ITEMS', payload: { dragId: state.dragData.id, targetId: id } });
|
|
107
|
+
dispatch({ type: 'SET_DRAG_DATA', payload: { id: state.dragData.id, initialGroup: container, originId: providerId } });
|
|
458
108
|
}
|
|
459
109
|
if (onDragEnter) onDragEnter(id, container);
|
|
460
110
|
};
|
|
461
111
|
|
|
462
112
|
const handleDragEnd = () => {
|
|
463
|
-
const draggedItemId = state.dragData.id;
|
|
464
|
-
const originalContainer = state.dragData.initialGroup;
|
|
465
|
-
|
|
466
|
-
// If enableCrossContainerPreview is true and no drop occurred, reset item to original container
|
|
467
|
-
if (enableCrossContainerPreview && !dragStateRef.current.dropOccurred && draggedItemId && originalContainer) {
|
|
468
|
-
dispatch({ type: 'RESET_DRAG_CONTAINER', payload: { itemId: draggedItemId, originalContainer, originalIndex: dragStateRef.current.originalIndex } });
|
|
469
|
-
}
|
|
470
|
-
|
|
471
113
|
dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
|
|
472
114
|
dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
|
|
473
115
|
dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
|
|
474
|
-
|
|
475
|
-
// Only call onDragEnd if drop didn't already occur (for enableCrossContainerPreview)
|
|
476
|
-
// If drop occurred, handleDrop or global drop handler already called onDragEnd
|
|
477
|
-
if (enableCrossContainerPreview && dragStateRef.current.dropOccurred) {
|
|
478
|
-
return;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (onDragEnd) {
|
|
482
|
-
if (!enableCrossContainerPreview) {
|
|
483
|
-
onDragEnd();
|
|
484
|
-
} else {
|
|
485
|
-
const draggedItem = stateRef.current.items.find(item => item && item.id === draggedItemId);
|
|
486
|
-
const finalContainer = draggedItem ? draggedItem.container : originalContainer;
|
|
487
|
-
|
|
488
|
-
const itemsInContainer = stateRef.current.items.filter(item => item && item.container === finalContainer);
|
|
489
|
-
const indexInContainer = itemsInContainer.findIndex(item => item && item.id === draggedItemId);
|
|
490
|
-
const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
|
|
491
|
-
const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
|
|
492
|
-
|
|
493
|
-
onDragEnd(
|
|
494
|
-
draggedItemId,
|
|
495
|
-
finalContainer,
|
|
496
|
-
originalContainer,
|
|
497
|
-
itemAbove,
|
|
498
|
-
itemBelow
|
|
499
|
-
);
|
|
500
|
-
}
|
|
501
|
-
}
|
|
116
|
+
if (onDragEnd) onDragEnd();
|
|
502
117
|
};
|
|
503
118
|
|
|
504
119
|
const changeCategory = (itemId: string, container: string) => {
|
|
@@ -508,60 +123,10 @@ export const DraggableProvider = ({
|
|
|
508
123
|
const handleDrop = (container: string) => {
|
|
509
124
|
if (state.dragData.originId !== providerId) return; // Ignore drop events from other providers
|
|
510
125
|
|
|
511
|
-
const draggedItemId = state.dragData.id;
|
|
512
|
-
const originalContainer = state.dragData.initialGroup;
|
|
513
|
-
|
|
514
|
-
// Mark drop as successful in ref for global listener
|
|
515
|
-
if (enableCrossContainerPreview) {
|
|
516
|
-
dragStateRef.current.dropOccurred = true;
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Gather data for callbacks BEFORE clearing state
|
|
520
|
-
const isCrossContainer = container !== originalContainer;
|
|
521
|
-
let callbackData = null;
|
|
522
|
-
|
|
523
|
-
if (enableCrossContainerPreview) {
|
|
524
|
-
const draggedItem = stateRef.current.items.find(item => item && item.id === draggedItemId);
|
|
525
|
-
const updatedItem = draggedItem ? { ...draggedItem, container } : null;
|
|
526
|
-
const itemsInContainer = stateRef.current.items.filter(item => item && item.container === container);
|
|
527
|
-
const indexInContainer = itemsInContainer.findIndex(item => item && item.id === draggedItemId);
|
|
528
|
-
const itemAbove = indexInContainer > 0 ? itemsInContainer[indexInContainer - 1] : null;
|
|
529
|
-
const itemBelow = indexInContainer < itemsInContainer.length - 1 ? itemsInContainer[indexInContainer + 1] : null;
|
|
530
|
-
|
|
531
|
-
callbackData = { updatedItem, itemAbove, itemBelow };
|
|
532
|
-
}
|
|
533
|
-
|
|
534
126
|
dispatch({ type: 'SET_IS_DRAGGING', payload: "" });
|
|
535
127
|
dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: "" });
|
|
536
|
-
dispatch({ type: 'SET_DRAG_DATA', payload: { id: "", initialGroup: "", originId: "" } });
|
|
537
128
|
changeCategory(state.dragData.id, container);
|
|
538
|
-
|
|
539
|
-
if (onDrop) {
|
|
540
|
-
if (!enableCrossContainerPreview) {
|
|
541
|
-
onDrop(container);
|
|
542
|
-
} else {
|
|
543
|
-
onDrop(
|
|
544
|
-
draggedItemId,
|
|
545
|
-
container,
|
|
546
|
-
originalContainer,
|
|
547
|
-
callbackData.updatedItem,
|
|
548
|
-
callbackData.itemAbove,
|
|
549
|
-
callbackData.itemBelow
|
|
550
|
-
);
|
|
551
|
-
}
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
// Trigger onDragEnd ONLY for cross-container drops (dragend doesn't fire reliably in that case)
|
|
555
|
-
// For same-container drops, handleDragEnd will be called normally
|
|
556
|
-
if (enableCrossContainerPreview && isCrossContainer && onDragEnd && callbackData) {
|
|
557
|
-
onDragEnd(
|
|
558
|
-
draggedItemId,
|
|
559
|
-
container,
|
|
560
|
-
originalContainer,
|
|
561
|
-
callbackData.itemAbove,
|
|
562
|
-
callbackData.itemBelow
|
|
563
|
-
);
|
|
564
|
-
}
|
|
129
|
+
if (onDrop) onDrop(container);
|
|
565
130
|
};
|
|
566
131
|
|
|
567
132
|
const handleDragOver = (e: Event, container: string) => {
|
|
@@ -569,22 +134,6 @@ export const DraggableProvider = ({
|
|
|
569
134
|
|
|
570
135
|
e.preventDefault();
|
|
571
136
|
dispatch({ type: 'SET_ACTIVE_CONTAINER', payload: container });
|
|
572
|
-
|
|
573
|
-
if (enableCrossContainerPreview && state.dragData.id) {
|
|
574
|
-
// Only when enableCrossContainerPreview is true: when hovering over a different container, move item to end
|
|
575
|
-
const draggedItem = state.items.find(
|
|
576
|
-
(item) => item && item.id === state.dragData.id
|
|
577
|
-
);
|
|
578
|
-
if (draggedItem && draggedItem.container !== container) {
|
|
579
|
-
dispatch({
|
|
580
|
-
type: "MOVE_TO_CONTAINER_END",
|
|
581
|
-
payload: { dragId: state.dragData.id, newContainer: container },
|
|
582
|
-
});
|
|
583
|
-
// Update current container in ref
|
|
584
|
-
dragStateRef.current.currentContainer = container;
|
|
585
|
-
}
|
|
586
|
-
}
|
|
587
|
-
|
|
588
137
|
if (onDragOver) onDragOver(e, container);
|
|
589
138
|
};
|
|
590
139
|
|
|
@@ -608,4 +157,4 @@ export const DraggableProvider = ({
|
|
|
608
157
|
return (
|
|
609
158
|
<DragContext.Provider value={contextValue}>{children}</DragContext.Provider>
|
|
610
159
|
);
|
|
611
|
-
};
|
|
160
|
+
};
|
|
@@ -18,12 +18,8 @@ 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 }
|
|
22
21
|
| { type: 'CHANGE_CATEGORY'; payload: { itemId: string; container: 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, originalIndex: number } };
|
|
22
|
+
| { type: 'REORDER_ITEMS'; payload: { dragId: string; targetId: string } };
|
|
27
23
|
|
|
28
24
|
export interface DropZoneConfig {
|
|
29
25
|
type?: 'ghost' | 'outline' | 'shadow' | 'line';
|
|
@@ -37,10 +33,9 @@ export type ActionType =
|
|
|
37
33
|
onReorder: (items: ItemType[]) => void;
|
|
38
34
|
onDragStart?: (id: string, container: string) => void;
|
|
39
35
|
onDragEnter?: (id: string, container: string) => void;
|
|
40
|
-
onDragEnd?: (
|
|
41
|
-
onDrop?: (
|
|
36
|
+
onDragEnd?: () => void;
|
|
37
|
+
onDrop?: (container: string) => void;
|
|
42
38
|
onDragOver?: (e: Event, container: string) => void;
|
|
43
39
|
dropZone?: DropZoneConfig | string; // Can accept string for backward compatibility
|
|
44
40
|
providerId?: string;
|
|
45
|
-
enableCrossContainerPreview?: boolean;
|
|
46
41
|
}
|
|
@@ -5,12 +5,11 @@ examples:
|
|
|
5
5
|
- draggable_with_selectable_list: Draggable with SelectableList Kit
|
|
6
6
|
- draggable_with_cards: Draggable with Cards
|
|
7
7
|
- draggable_with_table: Draggable with Table
|
|
8
|
+
- draggable_multiple_containers: Dragging Across Multiple Containers
|
|
8
9
|
- draggable_drop_zones: Draggable Drop Zones
|
|
9
10
|
- draggable_drop_zones_colors: Draggable Drop Zones Colors
|
|
10
11
|
- draggable_drop_zones_line: Draggable Drop Zones Line
|
|
11
12
|
- draggable_event_listeners: Draggable Event Listeners
|
|
12
|
-
- draggable_multiple_containers: Dragging Across Multiple Containers
|
|
13
|
-
- draggable_multiple_containers_dropzone: Dragging Across Multiple Containers with Dropzones
|
|
14
13
|
|
|
15
14
|
rails:
|
|
16
15
|
- draggable_default: Default
|
|
@@ -18,8 +17,8 @@ examples:
|
|
|
18
17
|
- draggable_with_selectable_list: Draggable with SelectableList Kit
|
|
19
18
|
- draggable_with_cards: Draggable with Cards
|
|
20
19
|
- draggable_with_table: Draggable with Table
|
|
20
|
+
- draggable_multiple_containers: Dragging Across Multiple Containers
|
|
21
21
|
- draggable_drop_zones: Draggable Drop Zones
|
|
22
22
|
- draggable_drop_zones_colors: Draggable Drop Zones Colors
|
|
23
23
|
- draggable_drop_zones_line: Draggable Drop Zones Line
|
|
24
24
|
- draggable_event_listeners: Draggable Event Listeners
|
|
25
|
-
- draggable_multiple_containers: Dragging Across Multiple Containers
|
|
@@ -7,5 +7,4 @@ export { default as DraggableWithTable } from './_draggable_with_table.jsx'
|
|
|
7
7
|
export { default as DraggableDropZones } from './_draggable_drop_zones.jsx'
|
|
8
8
|
export { default as DraggableDropZonesColors } from './_draggable_drop_zones_colors.jsx'
|
|
9
9
|
export { default as DraggableDropZonesLine } from './_draggable_drop_zones_line.jsx'
|
|
10
|
-
export { default as DraggableEventListeners } from './_draggable_event_listeners.jsx'
|
|
11
|
-
export { default as DraggableMultipleContainersDropzone } from './_draggable_multiple_containers_dropzone.jsx'
|
|
10
|
+
export { default as DraggableEventListeners } from './_draggable_event_listeners.jsx'
|
|
@@ -255,80 +255,4 @@ test("line dropZone with horizontal direction applies 'line_horizontal' class to
|
|
|
255
255
|
const container = kit.querySelector(".pb_draggable_container");
|
|
256
256
|
|
|
257
257
|
expect(container).toHaveClass("line_horizontal");
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
// Cross-container drag tests
|
|
261
|
-
const multiContainerData = [
|
|
262
|
-
{ id: "1", container: "To Do", text: "Task 1" },
|
|
263
|
-
{ id: "2", container: "To Do", text: "Task 2" },
|
|
264
|
-
{ id: "3", container: "In Progress", text: "Task 3" },
|
|
265
|
-
{ id: "4", container: "Done", text: "Task 4" },
|
|
266
|
-
];
|
|
267
|
-
|
|
268
|
-
const containers = ["To Do", "In Progress", "Done"];
|
|
269
|
-
|
|
270
|
-
const DraggableMultipleContainers = () => {
|
|
271
|
-
const [initialState, setInitialState] = useState(multiContainerData);
|
|
272
|
-
|
|
273
|
-
return (
|
|
274
|
-
<div data-testid={testId}>
|
|
275
|
-
<DraggableProvider
|
|
276
|
-
dropZone={{ type: "outline" }}
|
|
277
|
-
initialItems={multiContainerData}
|
|
278
|
-
onReorder={(items) => setInitialState(items)}
|
|
279
|
-
>
|
|
280
|
-
{containers.map((container) => (
|
|
281
|
-
<Draggable.Container
|
|
282
|
-
container={container}
|
|
283
|
-
data={{testid:`container-${container}`}}
|
|
284
|
-
key={container}
|
|
285
|
-
>
|
|
286
|
-
{initialState
|
|
287
|
-
.filter((item) => item.container === container)
|
|
288
|
-
.map(({ id, text }) => (
|
|
289
|
-
<Draggable.Item
|
|
290
|
-
container={container}
|
|
291
|
-
data-testid={`item-${id}`}
|
|
292
|
-
dragId={id}
|
|
293
|
-
key={id}
|
|
294
|
-
>
|
|
295
|
-
{text}
|
|
296
|
-
</Draggable.Item>
|
|
297
|
-
))}
|
|
298
|
-
</Draggable.Container>
|
|
299
|
-
))}
|
|
300
|
-
</DraggableProvider>
|
|
301
|
-
</div>
|
|
302
|
-
);
|
|
303
|
-
};
|
|
304
|
-
|
|
305
|
-
test("renders multiple containers with correct items", () => {
|
|
306
|
-
render(<DraggableMultipleContainers />);
|
|
307
|
-
|
|
308
|
-
const kit = screen.getByTestId(testId);
|
|
309
|
-
expect(kit).toBeInTheDocument();
|
|
310
|
-
|
|
311
|
-
containers.forEach((container) => {
|
|
312
|
-
const containerEl = kit.querySelector(`[data-testid="container-${container}"]`);
|
|
313
|
-
expect(containerEl).toBeInTheDocument();
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
// Check items are in correct containers
|
|
317
|
-
expect(screen.getByText("Task 1")).toBeInTheDocument();
|
|
318
|
-
expect(screen.getByText("Task 2")).toBeInTheDocument();
|
|
319
|
-
expect(screen.getByText("Task 3")).toBeInTheDocument();
|
|
320
|
-
expect(screen.getByText("Task 4")).toBeInTheDocument();
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
test("items have correct container association", () => {
|
|
324
|
-
const { container } = render(<DraggableMultipleContainers />);
|
|
325
|
-
|
|
326
|
-
// items rendered within their respective containers
|
|
327
|
-
const todoContainer = container.querySelector('[data-testid="container-To Do"]');
|
|
328
|
-
const inProgressContainer = container.querySelector('[data-testid="container-In Progress"]');
|
|
329
|
-
const doneContainer = container.querySelector('[data-testid="container-Done"]');
|
|
330
|
-
|
|
331
|
-
expect(todoContainer.querySelectorAll('.pb_draggable_item')).toHaveLength(2);
|
|
332
|
-
expect(inProgressContainer.querySelectorAll('.pb_draggable_item')).toHaveLength(1);
|
|
333
|
-
expect(doneContainer.querySelectorAll('.pb_draggable_item')).toHaveLength(1);
|
|
334
|
-
})
|
|
258
|
+
});
|