funda-ui 4.7.723 → 4.7.735
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/DragDropList/index.d.ts +1 -0
- package/DragDropList/index.js +143 -52
- package/EventCalendarTimeline/index.js +270 -196
- package/MultipleSelect/index.js +162 -71
- package/Table/index.css +5 -1
- package/Table/index.js +410 -90
- package/Utils/useBoundedDrag.d.ts +1 -0
- package/Utils/useBoundedDrag.js +124 -39
- package/lib/cjs/DragDropList/index.d.ts +1 -0
- package/lib/cjs/DragDropList/index.js +143 -52
- package/lib/cjs/EventCalendarTimeline/index.js +270 -196
- package/lib/cjs/MultipleSelect/index.js +162 -71
- package/lib/cjs/Table/index.js +410 -90
- package/lib/cjs/Utils/useBoundedDrag.d.ts +1 -0
- package/lib/cjs/Utils/useBoundedDrag.js +124 -39
- package/lib/css/Table/index.css +5 -1
- package/lib/esm/DragDropList/index.tsx +23 -16
- package/lib/esm/EventCalendarTimeline/index.tsx +290 -198
- package/lib/esm/Table/Table.tsx +9 -7
- package/lib/esm/Table/TableRow.tsx +9 -3
- package/lib/esm/Table/index.scss +8 -2
- package/lib/esm/Table/utils/DragHandleSprite.tsx +6 -2
- package/lib/esm/Table/utils/func.ts +12 -1
- package/lib/esm/Table/utils/hooks/useTableDraggable.tsx +401 -93
- package/lib/esm/Utils/hooks/useBoundedDrag.tsx +142 -39
- package/package.json +1 -1
|
@@ -116,6 +116,7 @@ export interface BoundedDragOptions {
|
|
|
116
116
|
dragHandleSelector?: string;
|
|
117
117
|
onDragStart?: (index: number) => void;
|
|
118
118
|
onDragOver?: (dragIndex: number | null, dropIndex: number | null) => void;
|
|
119
|
+
onDragUpdate?: (dragIndex: number | null, dropIndex: number | null) => void;
|
|
119
120
|
onDragEnd?: (dragIndex: number | null, dropIndex: number | null) => void;
|
|
120
121
|
}
|
|
121
122
|
|
|
@@ -127,6 +128,7 @@ export const useBoundedDrag = (options: BoundedDragOptions = {}) => {
|
|
|
127
128
|
dragHandleSelector = '.custom-draggable-list__handle',
|
|
128
129
|
onDragStart,
|
|
129
130
|
onDragOver,
|
|
131
|
+
onDragUpdate,
|
|
130
132
|
onDragEnd
|
|
131
133
|
} = options;
|
|
132
134
|
|
|
@@ -134,8 +136,31 @@ export const useBoundedDrag = (options: BoundedDragOptions = {}) => {
|
|
|
134
136
|
const dragItem = useRef<number | null>(null);
|
|
135
137
|
const dragOverItem = useRef<number | null>(null);
|
|
136
138
|
const dragNode = useRef<HTMLElement | null>(null);
|
|
139
|
+
const draggedElement = useRef<HTMLElement | null>(null);
|
|
140
|
+
const boundaryElement = useRef<HTMLElement | null>(null);
|
|
137
141
|
const touchOffset = useRef<TouchOffset>({ x: 0, y: 0 });
|
|
138
142
|
const currentHoverItem = useRef<HTMLElement | null>(null);
|
|
143
|
+
const rafId = useRef<number | null>(null);
|
|
144
|
+
const lastUpdateDragIndex = useRef<number | null>(null);
|
|
145
|
+
const lastUpdateDropIndex = useRef<number | null>(null);
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Performance Note:
|
|
149
|
+
*
|
|
150
|
+
* Drag-over events can fire at a very high frequency, especially on touch devices
|
|
151
|
+
* or when dragging quickly. Directly performing DOM read/write operations in the
|
|
152
|
+
* event handler (e.g. `getBoundingClientRect`, `classList` changes, style updates)
|
|
153
|
+
* can easily cause layout thrashing and frame drops when there are many items.
|
|
154
|
+
*
|
|
155
|
+
* To mitigate this, we:
|
|
156
|
+
* - Collect the pointer coordinates synchronously in the event handler.
|
|
157
|
+
* - Schedule all DOM-intensive work inside `requestAnimationFrame`, so the browser
|
|
158
|
+
* batches these operations before the next paint.
|
|
159
|
+
* - Cancel any pending frame (`cancelAnimationFrame`) before scheduling a new one,
|
|
160
|
+
* ensuring there is at most one pending DOM update per frame.
|
|
161
|
+
*
|
|
162
|
+
* This keeps drag interactions smooth even with large lists.
|
|
163
|
+
*/
|
|
139
164
|
|
|
140
165
|
const handleDragStart = (e: React.DragEvent | React.TouchEvent, position: number) => {
|
|
141
166
|
const isTouch = 'touches' in e;
|
|
@@ -192,14 +217,31 @@ export const useBoundedDrag = (options: BoundedDragOptions = {}) => {
|
|
|
192
217
|
});
|
|
193
218
|
|
|
194
219
|
document.body.appendChild(dragNode.current);
|
|
220
|
+
|
|
221
|
+
// Keep track of the original element (acts as a placeholder inside the list)
|
|
222
|
+
draggedElement.current = listItem;
|
|
223
|
+
boundaryElement.current = boundary as HTMLElement;
|
|
195
224
|
setIsDragging(true);
|
|
196
225
|
listItem.classList.add('dragging-placeholder');
|
|
197
226
|
} else {
|
|
198
|
-
//
|
|
227
|
+
// Desktop: use native drag image, but still record dragged element / boundary
|
|
228
|
+
draggedElement.current = listItem;
|
|
229
|
+
boundaryElement.current = boundary as HTMLElement;
|
|
230
|
+
setIsDragging(true);
|
|
231
|
+
|
|
232
|
+
const dragEvent = e as React.DragEvent;
|
|
233
|
+
if (dragEvent.dataTransfer) {
|
|
234
|
+
dragEvent.dataTransfer.effectAllowed = 'move';
|
|
235
|
+
// Optional: customize drag preview if needed
|
|
236
|
+
dragEvent.dataTransfer.setData('text/plain', '');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
listItem.classList.add('dragging-placeholder');
|
|
199
240
|
}
|
|
200
241
|
};
|
|
201
242
|
|
|
202
243
|
const handleDragOver = (e: React.DragEvent | React.TouchEvent) => {
|
|
244
|
+
// Always prevent default synchronously
|
|
203
245
|
e.preventDefault();
|
|
204
246
|
const isTouch = 'touches' in e;
|
|
205
247
|
|
|
@@ -207,56 +249,109 @@ export const useBoundedDrag = (options: BoundedDragOptions = {}) => {
|
|
|
207
249
|
(e as React.DragEvent).dataTransfer.dropEffect = 'move';
|
|
208
250
|
}
|
|
209
251
|
|
|
210
|
-
//
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
{ clientX: (e as React.DragEvent).clientX, clientY: (e as React.DragEvent).clientY };
|
|
252
|
+
// Extract primitive coordinates synchronously to avoid using pooled events in async callbacks
|
|
253
|
+
let clientX: number;
|
|
254
|
+
let clientY: number;
|
|
214
255
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
256
|
+
if (isTouch) {
|
|
257
|
+
const touch = (e as React.TouchEvent).touches[0];
|
|
258
|
+
clientX = touch.clientX;
|
|
259
|
+
clientY = touch.clientY;
|
|
260
|
+
} else {
|
|
261
|
+
clientX = (e as React.DragEvent).clientX;
|
|
262
|
+
clientY = (e as React.DragEvent).clientY;
|
|
219
263
|
}
|
|
220
264
|
|
|
221
|
-
//
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
);
|
|
265
|
+
// Cancel any pending frame to avoid stacking DOM operations
|
|
266
|
+
if (rafId.current !== null) {
|
|
267
|
+
cancelAnimationFrame(rafId.current);
|
|
268
|
+
}
|
|
226
269
|
|
|
227
|
-
|
|
270
|
+
rafId.current = requestAnimationFrame(() => {
|
|
271
|
+
// Update dragged element position for touch events
|
|
272
|
+
if (isTouch && isDragging && dragNode.current) {
|
|
273
|
+
dragNode.current.style.left = `${clientX - touchOffset.current.x}px`;
|
|
274
|
+
dragNode.current.style.top = `${clientY - touchOffset.current.y}px`;
|
|
275
|
+
}
|
|
228
276
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
if (!listItem || listItem === currentHoverItem.current) return;
|
|
277
|
+
// Find the element below the pointer/touch
|
|
278
|
+
const elemBelow = document.elementFromPoint(clientX, clientY);
|
|
232
279
|
|
|
233
|
-
|
|
234
|
-
const boundary = listItem.closest(boundarySelector);
|
|
235
|
-
if (!boundary) return;
|
|
280
|
+
if (!elemBelow) return;
|
|
236
281
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
}
|
|
282
|
+
// Find the closest list item
|
|
283
|
+
const listItem = elemBelow.closest(itemSelector) as HTMLElement;
|
|
284
|
+
if (!listItem) return;
|
|
241
285
|
|
|
242
|
-
|
|
243
|
-
|
|
286
|
+
// Check boundary
|
|
287
|
+
const boundary =
|
|
288
|
+
(boundaryElement.current as HTMLElement | null) ||
|
|
289
|
+
(listItem.closest(boundarySelector) as HTMLElement | null);
|
|
290
|
+
if (!boundary) return;
|
|
244
291
|
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
292
|
+
// Update hover states
|
|
293
|
+
if (currentHoverItem.current && currentHoverItem.current !== listItem) {
|
|
294
|
+
currentHoverItem.current.classList.remove('drag-over', 'drag-over-top', 'drag-over-bottom');
|
|
295
|
+
}
|
|
248
296
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const middleY = rect.top + rect.height / 2;
|
|
297
|
+
currentHoverItem.current = listItem;
|
|
298
|
+
listItem.classList.add('drag-over');
|
|
252
299
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
} else {
|
|
256
|
-
listItem.classList.add('drag-over-bottom');
|
|
257
|
-
}
|
|
300
|
+
const dragEl = draggedElement.current;
|
|
301
|
+
if (!dragEl || !dragEl.parentNode) return;
|
|
258
302
|
|
|
259
|
-
|
|
303
|
+
const container = boundary;
|
|
304
|
+
|
|
305
|
+
// Collect current ordered items in the container
|
|
306
|
+
const children = Array.from(container.querySelectorAll<HTMLElement>(itemSelector));
|
|
307
|
+
|
|
308
|
+
const currentIndex = children.indexOf(dragEl);
|
|
309
|
+
let targetIndex = children.indexOf(listItem);
|
|
310
|
+
|
|
311
|
+
if (currentIndex === -1 || targetIndex === -1) return;
|
|
312
|
+
|
|
313
|
+
// Determine drop position (top/bottom)
|
|
314
|
+
const rect = listItem.getBoundingClientRect();
|
|
315
|
+
const middleY = rect.top + rect.height / 2;
|
|
316
|
+
|
|
317
|
+
listItem.classList.remove('drag-over-top', 'drag-over-bottom');
|
|
318
|
+
|
|
319
|
+
const insertBefore =
|
|
320
|
+
clientY < middleY
|
|
321
|
+
? listItem
|
|
322
|
+
: (listItem.nextElementSibling as HTMLElement | null);
|
|
323
|
+
|
|
324
|
+
if (clientY < middleY) {
|
|
325
|
+
listItem.classList.add('drag-over-top');
|
|
326
|
+
} else {
|
|
327
|
+
listItem.classList.add('drag-over-bottom');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Only move in DOM when the effective position changes
|
|
331
|
+
if (insertBefore !== dragEl && container.contains(dragEl)) {
|
|
332
|
+
container.insertBefore(dragEl, insertBefore);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Recompute index after DOM move
|
|
336
|
+
const reorderedChildren = Array.from(container.querySelectorAll<HTMLElement>(itemSelector));
|
|
337
|
+
const newIndex = reorderedChildren.indexOf(dragEl);
|
|
338
|
+
dragOverItem.current = newIndex;
|
|
339
|
+
|
|
340
|
+
onDragOver?.(dragItem.current, dragOverItem.current);
|
|
341
|
+
|
|
342
|
+
// Only fire onDragUpdate when the (dragIndex, dropIndex) pair actually changes.
|
|
343
|
+
if (
|
|
344
|
+
onDragUpdate &&
|
|
345
|
+
(dragItem.current !== lastUpdateDragIndex.current ||
|
|
346
|
+
dragOverItem.current !== lastUpdateDropIndex.current)
|
|
347
|
+
) {
|
|
348
|
+
lastUpdateDragIndex.current = dragItem.current;
|
|
349
|
+
lastUpdateDropIndex.current = dragOverItem.current;
|
|
350
|
+
onDragUpdate(dragItem.current, dragOverItem.current);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
rafId.current = null;
|
|
354
|
+
});
|
|
260
355
|
};
|
|
261
356
|
|
|
262
357
|
const handleDragEnd = (e: React.DragEvent | React.TouchEvent) => {
|
|
@@ -265,6 +360,12 @@ export const useBoundedDrag = (options: BoundedDragOptions = {}) => {
|
|
|
265
360
|
|
|
266
361
|
onDragEnd?.(dragItem.current, dragOverItem.current);
|
|
267
362
|
|
|
363
|
+
// Cancel any pending animation frame
|
|
364
|
+
if (rafId.current !== null) {
|
|
365
|
+
cancelAnimationFrame(rafId.current);
|
|
366
|
+
rafId.current = null;
|
|
367
|
+
}
|
|
368
|
+
|
|
268
369
|
// Cleanup
|
|
269
370
|
if (dragNode.current) {
|
|
270
371
|
dragNode.current.remove();
|
|
@@ -286,6 +387,8 @@ export const useBoundedDrag = (options: BoundedDragOptions = {}) => {
|
|
|
286
387
|
currentHoverItem.current = null;
|
|
287
388
|
dragItem.current = null;
|
|
288
389
|
dragOverItem.current = null;
|
|
390
|
+
draggedElement.current = null;
|
|
391
|
+
boundaryElement.current = null;
|
|
289
392
|
};
|
|
290
393
|
|
|
291
394
|
return {
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"author":"UIUX Lab","email":"uiuxlab@gmail.com","name":"funda-ui","version":"4.7.
|
|
1
|
+
{"author":"UIUX Lab","email":"uiuxlab@gmail.com","name":"funda-ui","version":"4.7.735","description":"React components using pure Bootstrap 5+ which does not contain any external style and script libraries.","repository":{"type":"git","url":"git+https://github.com/xizon/funda-ui.git"},"scripts":{"test":"echo \"Error: no test specified\" && exit 1"},"keywords":["bootstrap","react-bootstrap","react-components","components","components-react","react-bootstrap-components","react","funda-ui","fundaui","uikit","ui-kit","ui-components"],"bugs":{"url":"https://github.com/xizon/funda-ui/issues"},"homepage":"https://github.com/xizon/funda-ui#readme","main":"all.js","license":"MIT","dependencies":{"react":"^18.2.0","react-dom":"^18.2.0"},"types":"all.d.ts","publishConfig":{"directory":"lib"},"directories":{"lib":"lib"}}
|