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.
@@ -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
- // ... desktop drag logic remains the same ...
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
- // Get the current pointer/touch position
211
- const point = isTouch ?
212
- (e as React.TouchEvent).touches[0] :
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
- // Update dragged element position for touch events
216
- if (isTouch && isDragging && dragNode.current) {
217
- dragNode.current.style.left = `${point.clientX - touchOffset.current.x}px`;
218
- dragNode.current.style.top = `${point.clientY - touchOffset.current.y}px`;
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
- // Find the element below the pointer/touch
222
- const elemBelow = document.elementFromPoint(
223
- point.clientX,
224
- point.clientY
225
- );
265
+ // Cancel any pending frame to avoid stacking DOM operations
266
+ if (rafId.current !== null) {
267
+ cancelAnimationFrame(rafId.current);
268
+ }
226
269
 
227
- if (!elemBelow) return;
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
- // Find the closest list item
230
- const listItem = elemBelow.closest(itemSelector) as HTMLElement;
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
- // Check boundary
234
- const boundary = listItem.closest(boundarySelector);
235
- if (!boundary) return;
280
+ if (!elemBelow) return;
236
281
 
237
- // Update hover states
238
- if (currentHoverItem.current) {
239
- currentHoverItem.current.classList.remove('drag-over', 'drag-over-top', 'drag-over-bottom');
240
- }
282
+ // Find the closest list item
283
+ const listItem = elemBelow.closest(itemSelector) as HTMLElement;
284
+ if (!listItem) return;
241
285
 
242
- currentHoverItem.current = listItem;
243
- listItem.classList.add('drag-over');
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
- // Calculate position in list
246
- const position = Array.from(listItem.parentNode!.children).indexOf(listItem);
247
- dragOverItem.current = position;
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
- // Determine drop position (top/bottom)
250
- const rect = listItem.getBoundingClientRect();
251
- const middleY = rect.top + rect.height / 2;
297
+ currentHoverItem.current = listItem;
298
+ listItem.classList.add('drag-over');
252
299
 
253
- if (point.clientY < middleY) {
254
- listItem.classList.add('drag-over-top');
255
- } else {
256
- listItem.classList.add('drag-over-bottom');
257
- }
300
+ const dragEl = draggedElement.current;
301
+ if (!dragEl || !dragEl.parentNode) return;
258
302
 
259
- onDragOver?.(dragItem.current, dragOverItem.current);
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.723","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"}}
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"}}