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.
@@ -31,7 +31,34 @@ const App = () => {
31
31
 
32
32
  */
33
33
 
34
- import { useState, useEffect, useCallback } from 'react';
34
+
35
+ /**
36
+ * Performance Optimizations for Large Data Sets:
37
+ *
38
+ * This hook has been optimized to handle large datasets (1000+ rows) efficiently.
39
+ * Key optimizations include:
40
+ *
41
+ * 1. RequestAnimationFrame for DOM Updates
42
+ * - DOM operations are batched within requestAnimationFrame callbacks
43
+ * - Browser executes updates before next frame render, reducing visual lag
44
+ * - Pending RAF callbacks are cancelled to prevent accumulation
45
+ *
46
+ * 2. Caching Strategy
47
+ * - tbodyRef: Cached to avoid repeated DOM queries
48
+ * - colCount: Cached to eliminate repeated queries in placeholderGenerator
49
+ * - allRowsCache: Cached with time-based invalidation (100ms)
50
+ *
51
+ * 3. Redundant Operation Prevention
52
+ * - Tracks last hovered row order (lastOverOrder)
53
+ * - Skips placeholder operations when hovering over the same row
54
+ * - Reduces unnecessary DOM manipulations during drag
55
+ *
56
+ * 4. Batch DOM Operations
57
+ * - removePlaceholder: Uses cached tbodyRef and batch removal
58
+ * - handleDragEnd: Uses DocumentFragment for batch DOM updates
59
+ * - Map-based lookups instead of repeated querySelector calls
60
+ */
61
+ import { useState, useEffect, useCallback, useRef } from 'react';
35
62
 
36
63
 
37
64
  import { getTbody, allRows, insertAfter, sortDataByIndex, initOrderProps, initRowColProps } from '../func';
@@ -54,6 +81,28 @@ function useTableDraggable({
54
81
 
55
82
  // drag & drop
56
83
  const [sortData, setSortData] = useState<any[] | undefined>([]);
84
+ const [isDragging, setIsDragging] = useState<boolean>(false);
85
+
86
+ // Performance optimization: cache for drag operations
87
+ const dragCacheRef = useRef<{
88
+ draggedObj: any;
89
+ overObj: any;
90
+ allRowsCache: any[] | null;
91
+ lastUpdateTime: number;
92
+ tbodyRef: any;
93
+ colCount: number;
94
+ lastOverOrder: number | null;
95
+ rafId: number | null;
96
+ }>({
97
+ draggedObj: null,
98
+ overObj: null,
99
+ allRowsCache: null,
100
+ lastUpdateTime: 0,
101
+ tbodyRef: null,
102
+ colCount: 0,
103
+ lastOverOrder: null,
104
+ rafId: null
105
+ });
57
106
 
58
107
 
59
108
  // ================================================================
@@ -62,33 +111,107 @@ function useTableDraggable({
62
111
  let draggedObj: any = null;
63
112
  let overObj: any = null;
64
113
 
114
+ // Helper function to filter out cloned elements and get only real rows
115
+ const getRealRows = (rows: any[]) => {
116
+ return rows.filter((row: any) => !row.classList.contains('row-obj-clonelast') && !row.classList.contains('row-obj-lastplaceholder'));
117
+ };
118
+
65
119
  const placeholderGenerator = (trHeight: number) => {
66
- const tbodyRef: any = getTbody(spyElement);
67
- if (tbodyRef === null || tbodyRef.querySelector('tr') === null) return;
120
+ // Use cached tbodyRef and colCount for better performance
121
+ let tbodyRef = dragCacheRef.current.tbodyRef;
122
+ if (!tbodyRef) {
123
+ tbodyRef = getTbody(spyElement);
124
+ dragCacheRef.current.tbodyRef = tbodyRef;
125
+ }
126
+
127
+ if (tbodyRef === null) return null;
128
+
129
+
130
+ // Cache colCount to avoid repeated queries
131
+ let colCount = dragCacheRef.current.colCount;
132
+ if (colCount === 0) {
133
+ const firstRow = tbodyRef.querySelector('tr');
134
+ if (firstRow === null) return null;
135
+ colCount = firstRow.children.length;
136
+ dragCacheRef.current.colCount = colCount;
137
+ }
68
138
 
69
139
  // Insert a row at the "index" of the table
70
140
  const newRow = document.createElement('tr');
71
141
  newRow.className = 'row-placeholder';
72
142
  newRow.dataset.placeholder = 'true';
73
143
  newRow.style.height = trHeight + 'px';
144
+ newRow.style.minHeight = trHeight + 'px'; // Ensure minimum height
74
145
 
75
146
  // Insert a cell in the row at index
76
147
  const newCell = newRow.insertCell(0);
77
- newCell.colSpan = tbodyRef.querySelector('tr').children.length;
148
+ newCell.colSpan = colCount;
149
+ newCell.style.minHeight = trHeight + 'px'; // Ensure cell has minimum height
150
+
151
+ // Use non-breaking space to ensure proper height rendering
152
+ // Multiple spaces or a placeholder element helps maintain consistent height
153
+ newCell.innerHTML = '&nbsp;'; // Use &nbsp; instead of regular space for better height consistency
78
154
 
79
- // Append a text node to the cell
80
- const newText = document.createTextNode(' ');
81
- newCell.appendChild(newText);
155
+ return newRow;
156
+ };
157
+
158
+
159
+ const lastPlaceholderGenerator = (trHeight: number) => {
160
+ // Use cached tbodyRef and colCount for better performance
161
+ let tbodyRef = dragCacheRef.current.tbodyRef;
162
+ if (!tbodyRef) {
163
+ tbodyRef = getTbody(spyElement);
164
+ dragCacheRef.current.tbodyRef = tbodyRef;
165
+ }
166
+
167
+ if (tbodyRef === null) return null;
168
+
169
+ const curEl = tbodyRef.querySelector('.row-obj-lastplaceholder');
170
+ if (curEl !== null) return;
171
+
172
+
173
+ // Cache colCount to avoid repeated queries
174
+ let colCount = dragCacheRef.current.colCount;
175
+ if (colCount === 0) {
176
+ const firstRow = tbodyRef.querySelector('tr');
177
+ if (firstRow === null) return null;
178
+ colCount = firstRow.children.length;
179
+ dragCacheRef.current.colCount = colCount;
180
+ }
181
+
182
+ // Create a dedicated last placeholder row that is kept in DOM but hidden by default
183
+ const newRow = document.createElement('tr');
184
+ newRow.className = 'row-obj row-obj-lastplaceholder';
185
+ // NOTE: Do NOT set data-placeholder here, otherwise it will be removed by removePlaceholder
186
+ newRow.style.height = trHeight + 'px';
187
+ newRow.style.minHeight = trHeight + 'px';
188
+ newRow.style.display = 'none';
189
+
190
+ const newCell = newRow.insertCell(0);
191
+ newCell.colSpan = colCount;
192
+ newCell.style.minHeight = trHeight + 'px';
193
+ newCell.innerHTML = '&nbsp;';
194
+
195
+ // Insert after the last real row (excluding cloned rows)
196
+ const rows = getRealRows(allRows(spyElement));
197
+ const lastRealRow = rows.length > 0 ? rows[rows.length - 1] : null;
198
+ if (lastRealRow && lastRealRow.parentNode === tbodyRef) {
199
+ insertAfter(newRow, lastRealRow);
200
+ } else {
201
+ tbodyRef.appendChild(newRow);
202
+ }
82
203
 
83
204
  return newRow;
84
205
  };
85
206
 
207
+
208
+ // An invisible HELPER element used to trigger the touch of the last element
86
209
  const lastRowGenerator = (trHeight: number) => {
87
210
  const tbodyRef: any = getTbody(spyElement);
88
211
  if (tbodyRef === null || tbodyRef.querySelector('tr') === null) return;
89
212
 
90
- const cloneEl = tbodyRef.querySelector('.row-obj-clonelast');
91
- if (cloneEl !== null) return;
213
+ const curEl = tbodyRef.querySelector('.row-obj-clonelast');
214
+ if (curEl !== null) return;
92
215
 
93
216
 
94
217
  // Insert a row at the "index" of the table
@@ -96,7 +219,6 @@ function useTableDraggable({
96
219
  newRow.className = 'row-obj row-obj-clonelast';
97
220
  newRow.dataset.order = allRows(spyElement).length.toString();
98
221
  newRow.style.height = trHeight + 'px';
99
- newRow.style.display = 'none';
100
222
 
101
223
 
102
224
  // Insert a cell in the row at index
@@ -107,20 +229,33 @@ function useTableDraggable({
107
229
  const newText = document.createTextNode(' ');
108
230
  newCell.appendChild(newText);
109
231
 
232
+ //
233
+ lastPlaceholderGenerator(trHeight);
234
+
110
235
  return newRow;
111
236
  };
112
237
 
113
238
 
114
239
  const removePlaceholder = () => {
115
- const tbodyRef: any = getTbody(spyElement);
240
+ // Use cached tbodyRef
241
+ let tbodyRef = dragCacheRef.current.tbodyRef;
242
+ if (!tbodyRef) {
243
+ tbodyRef = getTbody(spyElement);
244
+ dragCacheRef.current.tbodyRef = tbodyRef;
245
+ }
246
+
116
247
  if (tbodyRef === null) return;
117
248
 
118
- // Delete row at the "index" of the table
119
- const placeholder = [].slice.call(tbodyRef.querySelectorAll(`[data-placeholder]`));
120
- placeholder.forEach((node: any) => {
121
- tbodyRef.removeChild(node);
122
- });
123
-
249
+ // Optimize: use querySelectorAll and remove in batch
250
+ const placeholders = tbodyRef.querySelectorAll(`[data-placeholder]`);
251
+ if (placeholders.length > 0) {
252
+ // Use DocumentFragment for batch removal (though in this case direct removal is fine)
253
+ placeholders.forEach((node: any) => {
254
+ if (node.parentNode) {
255
+ node.parentNode.removeChild(node);
256
+ }
257
+ });
258
+ }
124
259
  };
125
260
 
126
261
 
@@ -164,46 +299,131 @@ function useTableDraggable({
164
299
  };
165
300
 
166
301
  // events fired on the drop targets
302
+ // Optimized with requestAnimationFrame, throttling and caching
167
303
  const handledragOver = useCallback((e: any) => {
168
- const tbodyRef: any = getTbody(spyElement);
169
- if (tbodyRef === null) return;
170
-
304
+ // Always prevent default in sync code
171
305
  e.preventDefault();
172
- if (draggedObj === null) return;
173
-
174
- draggedObj.style.display = 'none';
175
-
306
+
307
+ // Use cached draggedObj and tbodyRef
308
+ const currentDraggedObj = dragCacheRef.current.draggedObj || draggedObj;
309
+ if (currentDraggedObj === null) return;
310
+
311
+ let tbodyRef = dragCacheRef.current.tbodyRef;
312
+ if (!tbodyRef) {
313
+ tbodyRef = getTbody(spyElement);
314
+ if (tbodyRef === null) return;
315
+ dragCacheRef.current.tbodyRef = tbodyRef;
316
+ }
176
317
 
318
+ // Early return for placeholder targets
177
319
  if (e.target.classList.contains('row-placeholder')) return;
178
320
 
179
321
  const itemsWrapper = e.target.parentNode;
180
- if (itemsWrapper.classList.contains('row-obj')) {
181
- overObj = itemsWrapper;
322
+ if (!itemsWrapper || !itemsWrapper.classList || !itemsWrapper.classList.contains('row-obj')) {
323
+ return;
324
+ }
325
+
326
+ // Skip cloned elements - they should not be valid drop targets
327
+ if (itemsWrapper.classList.contains('row-obj-lastplaceholder')) {
328
+ return;
329
+ }
330
+
331
+ // Check if we're still over the same row (avoid unnecessary operations)
332
+ const currentOrder = Number(itemsWrapper.dataset.order);
333
+ if (dragCacheRef.current.lastOverOrder === currentOrder) {
334
+ return; // Same target, skip
335
+ }
336
+
337
+ // console.log(' --> overObj: ', itemsWrapper);
338
+
339
+ // Use requestAnimationFrame for smoother DOM updates
340
+ // Cancel previous frame if pending
341
+ if (dragCacheRef.current.rafId !== null) {
342
+ cancelAnimationFrame(dragCacheRef.current.rafId);
343
+ }
344
+
345
+ // Store references for use in RAF callback
346
+ const targetWrapper = itemsWrapper;
347
+ const targetOrder = currentOrder;
348
+
349
+ dragCacheRef.current.rafId = requestAnimationFrame(() => {
350
+ overObj = targetWrapper;
351
+ dragCacheRef.current.overObj = targetWrapper;
352
+ dragCacheRef.current.lastOverOrder = targetOrder;
353
+
354
+ currentDraggedObj.style.display = 'none';
182
355
  removePlaceholder();
183
356
 
184
- if (Number(overObj.dataset.order) === allRows(spyElement).length - 1) {
185
- tbodyRef.insertBefore(placeholderGenerator((allRows(spyElement).at(-2) as any).clientHeight), overObj);
186
- } else {
187
- tbodyRef.insertBefore(placeholderGenerator(overObj.clientHeight), overObj);
357
+
358
+ // Cache allRows result to avoid multiple queries
359
+ let cachedRows = dragCacheRef.current.allRowsCache;
360
+ const now = Date.now();
361
+ if (!cachedRows || now - dragCacheRef.current.lastUpdateTime > 100) {
362
+ cachedRows = allRows(spyElement);
363
+ dragCacheRef.current.allRowsCache = cachedRows;
364
+ dragCacheRef.current.lastUpdateTime = now;
188
365
  }
189
366
 
367
+ // Filter out cloned elements to get real rows count
368
+ const realRows = getRealRows(cachedRows);
369
+ const totalRows = realRows.length;
370
+ const overOrder = Number(overObj.dataset.order);
371
+
372
+ // When hovering over the last real row, use its height for placeholder
373
+ // Otherwise use the overObj's height
374
+ const isOverLastRow = overOrder === totalRows - 1 && realRows.length > 0 && realRows[totalRows - 1];
375
+ const placeholderHeight = isOverLastRow
376
+ ? realRows[totalRows - 1].clientHeight
377
+ : overObj.clientHeight;
378
+
379
+ const placeholder = placeholderGenerator(placeholderHeight);
380
+
381
+ if (placeholder) {
382
+ const draggedOrder = Number(currentDraggedObj.dataset.order);
383
+ //console.log(' --> drag index list: ', draggedOrder, overOrder, totalRows - 1);
384
+ tbodyRef.insertBefore(placeholder, overObj);
385
+ }
190
386
 
191
- }
387
+ dragCacheRef.current.rafId = null;
388
+ });
192
389
 
193
- }, [sortData]);
390
+ }, [sortData, spyElement]);
194
391
 
195
392
 
196
393
  const handleDragStart = useCallback((e: any) => {
197
394
  const tbodyRef: any = getTbody(spyElement);
198
395
  if (tbodyRef === null) return;
199
396
 
397
+ setIsDragging(true);
398
+
200
399
  draggedObj = e.currentTarget;
400
+ // Cache draggedObj and tbodyRef for performance
401
+ dragCacheRef.current.draggedObj = draggedObj;
402
+ dragCacheRef.current.tbodyRef = tbodyRef;
403
+ dragCacheRef.current.lastOverOrder = null; // Reset
404
+
201
405
  e.dataTransfer.effectAllowed = 'move';
202
406
  e.dataTransfer.setData('text/html', draggedObj);
203
407
 
204
408
  draggedObj.classList.add('dragging');
205
- (allRows(spyElement).at(-1) as any).style.setProperty('display', 'table-row', "important");
206
-
409
+
410
+ // Cache allRows and use cached result
411
+ const cachedRows = allRows(spyElement);
412
+ dragCacheRef.current.allRowsCache = cachedRows;
413
+ dragCacheRef.current.lastUpdateTime = Date.now();
414
+
415
+ // Cache colCount if not already cached
416
+ if (dragCacheRef.current.colCount === 0) {
417
+ const firstRow = tbodyRef.querySelector('tr');
418
+ if (firstRow) {
419
+ dragCacheRef.current.colCount = firstRow.children.length;
420
+ }
421
+ }
422
+
423
+ const lastRow: any = cachedRows[cachedRows.length - 1];
424
+ if (lastRow && !lastRow.classList.contains('row-obj-lastplaceholder')) {
425
+ lastRow.style.setProperty('display', 'table-row', "important");
426
+ }
207
427
 
208
428
  // callback
209
429
  const dragStart: Function = (callback: Function) => {
@@ -212,99 +432,184 @@ function useTableDraggable({
212
432
  onRowDrag?.(dragStart, null);
213
433
 
214
434
 
215
- // init clone <tr>
216
- // !!! It needs to be put at the end of the code to fix the location of the clone element
217
- const cloneEl = tbodyRef.querySelector('.row-obj-clonelast');
218
- if (cloneEl !== null) {
219
- cloneEl.style.display = 'none';
220
- }
221
-
222
-
223
- }, [handledragOver]);
435
+ }, [handledragOver, sortData, data, spyElement, onRowDrag]);
224
436
 
225
437
  const handleDragEnd = useCallback((e: any) => {
226
438
  const tbodyRef: any = getTbody(spyElement);
227
439
  if (tbodyRef === null) return;
228
440
 
229
- draggedObj.style.display = 'table-row';
230
- removePlaceholder();
441
+ setIsDragging(false);
231
442
 
232
- draggedObj.classList.remove('dragging');
443
+ // Use cached draggedObj if available
444
+ const currentDraggedObj = dragCacheRef.current.draggedObj || draggedObj;
445
+ const currentOverObj = dragCacheRef.current.overObj || overObj;
446
+
447
+ if (currentDraggedObj) {
448
+ currentDraggedObj.style.display = 'table-row';
449
+ currentDraggedObj.classList.remove('dragging');
450
+ }
451
+
452
+ removePlaceholder();
233
453
  tbodyRef?.classList.remove('drag-trigger-mousedown');
234
454
 
455
+ // Cancel any pending animation frame
456
+ if (dragCacheRef.current.rafId !== null) {
457
+ cancelAnimationFrame(dragCacheRef.current.rafId);
458
+ dragCacheRef.current.rafId = null;
459
+ }
235
460
 
236
- if (overObj === null) return;
461
+ if (currentOverObj === null) {
462
+ // Reset cache
463
+ dragCacheRef.current.draggedObj = null;
464
+ dragCacheRef.current.overObj = null;
465
+ dragCacheRef.current.allRowsCache = null;
466
+ dragCacheRef.current.lastOverOrder = null;
467
+ return;
468
+ }
237
469
 
238
470
  // update state
239
471
  let curData: number[] = [];
240
472
  curData = JSON.parse(JSON.stringify(sortData));
241
- let from = Number(draggedObj.dataset.order);
242
- let to = Number(overObj.dataset.order);
243
- if (from < to) to--;
244
-
245
-
246
- //sort data
247
- const newData: number[] = [];
248
-
249
- // console.log('--> data1:', curData);
473
+ let from = Number(currentDraggedObj.dataset.order);
474
+ let to = Number(currentOverObj.dataset.order);
475
+
476
+ // Get real rows to determine the actual last row index
477
+ const allRowsForLastIndex = allRows(spyElement);
478
+ const realRows = getRealRows(allRowsForLastIndex);
479
+ const actualLastRowIndex = realRows.length - 1;
480
+
481
+ // Standard drag-and-drop logic:
482
+ // When dragging from a lower index to a higher index, we need to decrement 'to'
483
+ // because removing the element at 'from' causes all subsequent elements to shift left by 1
484
+ // However, when dragging to the last position, we want to swap with the last element
485
+ // After removing 'from', if we want to swap with the last element, we should insert
486
+ // at the position that will result in the dragged element being at the last position
487
+ if (from < to) {
488
+ // Special case: dragging to the last position
489
+ // We want to swap with the last element, so after removing 'from',
490
+ // we should insert at the new last position (which is curData.length - 1)
491
+ // Since 'to' is the original last index, and we're removing 'from' (which is < 'to'),
492
+ // the new last position after removal is still 'to' (no shift because 'from' is before 'to')
493
+ // Wait, that's not right. If we remove 'from', elements from 'from+1' to 'to' shift left by 1
494
+ // So 'to' becomes 'to-1'. But we want to insert at the last position, which is 'to-1'
495
+ // So we should decrement 'to' as normal. But then the element will be at 'to-1', not 'to'
496
+ //
497
+ // Actually, the issue is: when dragging to the last element, we want to SWAP with it
498
+ // So the dragged element should end up at the last position, and the last element should
499
+ // end up at the dragged element's original position
500
+ //
501
+ // Let's think step by step with an example: [A, B, C, D, E], from=1 (B), to=4 (E)
502
+ // We want result: [A, C, D, E, B] (B and E swapped)
503
+ // Step 1: Remove B -> [A, C, D, E] (indices 0-3)
504
+ // Step 2: Insert B at position 4 -> [A, C, D, E, B] ✓
505
+ // So 'to' should be 4 (not decremented) to get the correct result
506
+ if (to === actualLastRowIndex) {
507
+ // Don't decrement 'to' when dragging to the last position
508
+ // This ensures the element is inserted at the last position after removal
509
+ } else {
510
+ // Normal case: dragging forward but not to the last position
511
+ to--;
512
+ }
513
+ }
514
+ // If from >= to, no adjustment needed (dragging backward)
250
515
 
516
+ // Optimize: simplify the sorting logic (the nested loop was inefficient)
251
517
  curData.splice(to, 0, curData.splice(from, 1)[0]);
518
+ const newData: number[] = [...curData]; // Direct copy instead of nested loop
519
+
520
+ setSortData(newData);
252
521
 
253
- for (let i = 0; i < curData.length; i++) {
254
- for (let j = 0; j < curData.length; j++) {
522
+ // Performance optimization: batch DOM updates using a map
523
+ const table = spyElement.querySelector('table');
524
+ if (!table) return;
525
+
526
+ const tbody = table.querySelector('tbody');
527
+ if (!tbody) return;
255
528
 
256
- if (curData[i] === curData[j]) {
257
- newData.push(curData[j] as never);
529
+ // Get all rows once and create a map for faster lookups
530
+ // Support both data-key attribute (user-provided) and data-order fallback
531
+ const allRowsElements = Array.from(allRows(spyElement));
532
+
533
+ // Create a map: original index (from sortData) -> row element
534
+ const rowMap = new Map<number, HTMLElement>();
535
+ allRowsElements.forEach((row: any) => {
536
+ // First try to use data-key attribute (if user provided it)
537
+ const dataKey = row.getAttribute('data-key');
538
+ if (dataKey) {
539
+ const match = dataKey.match(/row-(\d+)/);
540
+ if (match) {
541
+ const index = Number(match[1]);
542
+ rowMap.set(index, row);
543
+ return;
258
544
  }
259
545
  }
260
- }
261
-
262
-
263
- // console.log("--> data2: ", newData);
264
- setSortData(newData);
546
+
547
+ // Fallback: use data-order to match with sortData indices
548
+ const currentOrder = Number(row.dataset.order);
549
+ if (sortData && !isNaN(currentOrder) && currentOrder >= 0 && currentOrder < sortData.length) {
550
+ const originalIndex = sortData[currentOrder];
551
+ if (originalIndex !== undefined) {
552
+ rowMap.set(originalIndex, row);
553
+ }
554
+ }
555
+ });
265
556
 
266
- // reset data-id in order to sort data
557
+ // Update order attributes using the map (batch operation)
267
558
  newData.forEach((curId: any, order: number) => {
268
- const _el = spyElement.querySelector('table').querySelector(`tbody [data-key="row-${curId}"]`);
269
- if (_el !== null) _el.dataset.order = order;
270
-
559
+ const _el = rowMap.get(curId);
560
+ if (_el !== null && _el !== undefined) {
561
+ _el.dataset.order = order.toString();
562
+ }
271
563
  });
272
564
 
565
+ // Performance optimization: Use DocumentFragment to batch DOM updates
566
+ // NOTE: Keep the special last placeholder row (`row-obj-lastplaceholder`)
567
+ // out of the main sort, otherwise it may jump to the top after each drag.
568
+ const lastPlaceholderRow = allRowsElements.find(
569
+ (row: any) => row.classList && row.classList.contains('row-obj-lastplaceholder')
570
+ );
273
571
 
274
- // sort elements
275
- const categoryItemsArray = allRows(spyElement);
276
- const sorter = (a: any, b: any) => {
277
- let txt1 = Number(a.dataset.order),
278
- txt2 = Number(b.dataset.order);
572
+ const rowsToSort = lastPlaceholderRow
573
+ ? allRowsElements.filter(row => row !== lastPlaceholderRow)
574
+ : allRowsElements;
279
575
 
576
+ const sorter = (a: any, b: any) => {
577
+ const txt1 = Number(a.dataset.order);
578
+ const txt2 = Number(b.dataset.order);
280
579
  return txt2 < txt1 ? -1 : txt2 > txt1 ? 1 : 0;
281
- }
282
- const sorted = categoryItemsArray.sort(sorter).reverse();
283
- sorted.forEach(e => spyElement.querySelector('table').querySelector('tbody').appendChild(e));
580
+ };
581
+
582
+ const sorted = [...rowsToSort].sort(sorter).reverse();
284
583
 
584
+ // Ensure the last placeholder row always stays at the bottom
585
+ if (lastPlaceholderRow) {
586
+ sorted.push(lastPlaceholderRow);
587
+ }
588
+
589
+ // Use DocumentFragment to minimize reflows
590
+ const fragment = document.createDocumentFragment();
591
+ sorted.forEach(e => fragment.appendChild(e));
592
+ tbody.appendChild(fragment);
285
593
 
286
594
  // callback
287
595
  const dragEnd: Function = (callback: Function) => {
288
- callback.call(null, draggedObj, newData, sortDataByIndex(newData as never, data as never));
596
+ callback.call(null, currentDraggedObj, newData, sortDataByIndex(newData as never, data as never));
289
597
  };
290
598
  onRowDrag?.(null, dragEnd);
291
599
 
292
-
293
-
294
600
  // init clone <tr>
295
601
  // !!! It needs to be put at the end of the code to fix the location of the clone element
296
602
  const _allRows = allRows(spyElement);
297
- const cloneEl = tbodyRef.querySelector('.row-obj-clonelast');
298
- if (cloneEl !== null) {
299
- if (typeof _allRows.at(-1) !== 'undefined') {
300
- insertAfter(cloneEl, _allRows.at(-1));
301
- cloneEl.style.display = 'none';
302
- }
303
- }
304
-
603
+ dragCacheRef.current.allRowsCache = _allRows;
604
+ dragCacheRef.current.lastUpdateTime = Date.now();
605
+
305
606
 
607
+ // Reset cache
608
+ dragCacheRef.current.draggedObj = null;
609
+ dragCacheRef.current.overObj = null;
610
+ dragCacheRef.current.lastOverOrder = null;
306
611
 
307
- }, [sortData]);
612
+ }, [sortData, spyElement, data, onRowDrag]);
308
613
 
309
614
 
310
615
 
@@ -329,12 +634,15 @@ function useTableDraggable({
329
634
  }, [data, enabled, spyElement, ...deps]);
330
635
 
331
636
  return {
332
- handleDragStart,
333
- handleDragEnd,
334
- handledragOver,
637
+ isDragging,
638
+ dragHandlers: {
639
+ handleDragStart,
640
+ handleDragOver: handledragOver,
641
+ handleDragEnd,
642
+ },
335
643
  handleTbodyEnter,
336
644
  handleTbodyLeave
337
- }
645
+ };
338
646
  }
339
647
 
340
648