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
|
@@ -31,7 +31,34 @@ const App = () => {
|
|
|
31
31
|
|
|
32
32
|
*/
|
|
33
33
|
|
|
34
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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 =
|
|
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 = ' '; // Use instead of regular space for better height consistency
|
|
78
154
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 = ' ';
|
|
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
|
|
91
|
-
if (
|
|
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
|
-
|
|
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
|
-
//
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
|
|
169
|
-
if (tbodyRef === null) return;
|
|
170
|
-
|
|
304
|
+
// Always prevent default in sync code
|
|
171
305
|
e.preventDefault();
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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
|
-
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
230
|
-
removePlaceholder();
|
|
441
|
+
setIsDragging(false);
|
|
231
442
|
|
|
232
|
-
draggedObj
|
|
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 (
|
|
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(
|
|
242
|
-
let to = Number(
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
//
|
|
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
|
-
|
|
254
|
-
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
|
|
264
|
-
|
|
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
|
-
//
|
|
557
|
+
// Update order attributes using the map (batch operation)
|
|
267
558
|
newData.forEach((curId: any, order: number) => {
|
|
268
|
-
const _el =
|
|
269
|
-
if (_el !== null
|
|
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
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
283
|
-
sorted
|
|
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,
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
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
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
|