react-live-data-table 1.0.16 → 1.0.17
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/package.json +1 -1
- package/src/ReactDataTable.jsx +242 -116
package/package.json
CHANGED
package/src/ReactDataTable.jsx
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
import React, { useEffect } from 'react';
|
1
|
+
import React, { useEffect, useState, useCallback } from 'react';
|
2
2
|
import "./index.css";
|
3
3
|
|
4
4
|
function ReactDataTable({
|
@@ -18,8 +18,8 @@ function ReactDataTable({
|
|
18
18
|
headerProps = {},
|
19
19
|
selected = {},
|
20
20
|
showSelectAllCheckbox = true,
|
21
|
-
rowStyle = {},
|
22
|
-
rowClassName = ""
|
21
|
+
rowStyle = {},
|
22
|
+
rowClassName = ""
|
23
23
|
}) {
|
24
24
|
const tableContainerRef = React.useRef(null);
|
25
25
|
const [data, setData] = React.useState({ pages: [], meta: { totalPages: 1 } });
|
@@ -27,15 +27,110 @@ function ReactDataTable({
|
|
27
27
|
const [pageParam, setPageParam] = React.useState(1);
|
28
28
|
const [selectedRows, setSelectedRows] = React.useState(selected);
|
29
29
|
const previousSelected = React.useRef(selected);
|
30
|
+
const [resizingIndex, setResizingIndex] = useState(null);
|
31
|
+
const [columnWidths, setColumnWidths] = useState([]);
|
32
|
+
const [startX, setStartX] = useState(null);
|
33
|
+
const [initialWidth, setInitialWidth] = useState(null);
|
34
|
+
const [tableWidth, setTableWidth] = useState(0);
|
35
|
+
// Ref to store column widths to ensure persistence during data loading
|
36
|
+
const persistedColumnWidthsRef = React.useRef([]);
|
37
|
+
|
38
|
+
const flatData = data.pages.flatMap(page => page.data);
|
39
|
+
|
40
|
+
const checkboxColumn = {
|
41
|
+
id: 'select',
|
42
|
+
size: 50,
|
43
|
+
minWidth: 50,
|
44
|
+
resizable: false,
|
45
|
+
textAlign: "center",
|
46
|
+
header: ({ data }) => {
|
47
|
+
const allSelected = flatData.length > 0 && flatData.every(row => selectedRows[row.id]);
|
48
|
+
const someSelected = flatData.some(row => selectedRows[row.id]) && !allSelected;
|
49
|
+
|
50
|
+
return (
|
51
|
+
<div className="flex items-center justify-center h-[40px]">
|
52
|
+
{showSelectAllCheckbox && (
|
53
|
+
<div className="relative">
|
54
|
+
<input
|
55
|
+
id={data.id}
|
56
|
+
type="checkbox"
|
57
|
+
className='bg-gray-700 rounded-4 border-gray-200 text-blue-400 focus:ring-0 focus:ring-white'
|
58
|
+
checked={allSelected}
|
59
|
+
onChange={(e) => handleSelectAll(e.target.checked, flatData)}
|
60
|
+
/>
|
61
|
+
{allSelected ? (
|
62
|
+
<svg
|
63
|
+
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-3 h-3 pointer-events-none text-white"
|
64
|
+
viewBox="0 0 20 20"
|
65
|
+
fill="currentColor"
|
66
|
+
>
|
67
|
+
<path
|
68
|
+
fillRule="evenodd"
|
69
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
70
|
+
clipRule="evenodd"
|
71
|
+
/>
|
72
|
+
</svg>
|
73
|
+
) : (
|
74
|
+
someSelected &&
|
75
|
+
<svg
|
76
|
+
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-3 h-3 pointer-events-none "
|
77
|
+
viewBox="0 0 20 20"
|
78
|
+
fill="currentColor"
|
79
|
+
>
|
80
|
+
<path
|
81
|
+
fillRule="evenodd"
|
82
|
+
d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
83
|
+
clipRule="evenodd"
|
84
|
+
/>
|
85
|
+
</svg>
|
86
|
+
)}
|
87
|
+
</div>
|
88
|
+
)}
|
89
|
+
</div>
|
90
|
+
);
|
91
|
+
},
|
92
|
+
cell: ({ row }) => {
|
93
|
+
return (
|
94
|
+
<div className="flex items-center justify-center h-[40px]" onClick={(e) => e.stopPropagation()}>
|
95
|
+
<input
|
96
|
+
id={row.id}
|
97
|
+
type="checkbox"
|
98
|
+
className='bg-gray-700 rounded-4 border-gray-200 text-blue-400 focus:ring-0 focus:ring-white'
|
99
|
+
checked={!!selectedRows[row.id]}
|
100
|
+
onClick={(e) => e.stopPropagation()}
|
101
|
+
onChange={(e) => { e.stopPropagation(); handleSelectRow(e.target.checked, row, flatData) }}
|
102
|
+
/>
|
103
|
+
</div>
|
104
|
+
)
|
105
|
+
}
|
106
|
+
};
|
107
|
+
|
108
|
+
const enhancedColumns = showCheckbox ? [checkboxColumn, ...columns] : columns;
|
30
109
|
|
31
|
-
|
32
110
|
useEffect(() => {
|
33
111
|
if (JSON.stringify(previousSelected.current) !== JSON.stringify(selected)) {
|
34
|
-
setSelectedRows({...selected});
|
112
|
+
setSelectedRows({ ...selected });
|
35
113
|
previousSelected.current = selected;
|
36
114
|
}
|
37
115
|
}, [selected]);
|
38
116
|
|
117
|
+
// Initialize column widths array only once when component mounts or columns change
|
118
|
+
useEffect(() => {
|
119
|
+
const allColumns = showCheckbox ? [{ id: 'select', size: 50 }, ...columns] : columns;
|
120
|
+
|
121
|
+
// If we have persisted widths and the number of columns matches, use those
|
122
|
+
if (persistedColumnWidthsRef.current.length === allColumns.length) {
|
123
|
+
setColumnWidths([...persistedColumnWidthsRef.current]);
|
124
|
+
setTableWidth(persistedColumnWidthsRef.current.reduce((sum, width) => sum + width, 0));
|
125
|
+
} else {
|
126
|
+
// Otherwise initialize with default widths
|
127
|
+
const initialWidths = allColumns.map(column => column.size || column.minWidth || 150);
|
128
|
+
setColumnWidths(initialWidths);
|
129
|
+
setTableWidth(initialWidths.reduce((sum, width) => sum + width, 0));
|
130
|
+
// Store in our ref for persistence
|
131
|
+
persistedColumnWidthsRef.current = [...initialWidths];
|
132
|
+
}
|
133
|
+
}, [columns, showCheckbox]);
|
39
134
|
|
40
135
|
useEffect(() => {
|
41
136
|
setData({ pages: [], meta: { totalPages: 1 } });
|
@@ -60,6 +155,57 @@ function ReactDataTable({
|
|
60
155
|
}
|
61
156
|
}, [dataSource, staticData]);
|
62
157
|
|
158
|
+
const handleMouseMove = useCallback((e) => {
|
159
|
+
if (resizingIndex === null || startX === null || initialWidth === null) return;
|
160
|
+
|
161
|
+
const delta = e.clientX - startX;
|
162
|
+
const newWidth = Math.max(enhancedColumns[resizingIndex]?.minWidth || 80, initialWidth + delta);
|
163
|
+
|
164
|
+
// Create a new array of column widths with the updated width
|
165
|
+
const newColumnWidths = [...columnWidths];
|
166
|
+
newColumnWidths[resizingIndex] = newWidth;
|
167
|
+
|
168
|
+
// Update the column widths state
|
169
|
+
setColumnWidths(newColumnWidths);
|
170
|
+
|
171
|
+
// Update our persisted ref to maintain widths during pagination
|
172
|
+
persistedColumnWidthsRef.current = newColumnWidths;
|
173
|
+
|
174
|
+
// Recalculate table width based on the new column widths
|
175
|
+
const newTableWidth = newColumnWidths.reduce((sum, width) => sum + width, 0);
|
176
|
+
setTableWidth(newTableWidth);
|
177
|
+
}, [resizingIndex, startX, initialWidth, enhancedColumns, columnWidths]);
|
178
|
+
|
179
|
+
const handleMouseUp = useCallback(() => {
|
180
|
+
setResizingIndex(null);
|
181
|
+
setStartX(null);
|
182
|
+
setInitialWidth(null);
|
183
|
+
}, []);
|
184
|
+
|
185
|
+
// Add resize event listeners
|
186
|
+
useEffect(() => {
|
187
|
+
if (resizingIndex !== null) {
|
188
|
+
document.addEventListener('mousemove', handleMouseMove);
|
189
|
+
document.addEventListener('mouseup', handleMouseUp);
|
190
|
+
return () => {
|
191
|
+
document.removeEventListener('mousemove', handleMouseMove);
|
192
|
+
document.removeEventListener('mouseup', handleMouseUp);
|
193
|
+
};
|
194
|
+
}
|
195
|
+
}, [resizingIndex, handleMouseMove, handleMouseUp]);
|
196
|
+
|
197
|
+
const handleResizeStart = (e, index) => {
|
198
|
+
const column = enhancedColumns[index];
|
199
|
+
if (!column.resizable) return;
|
200
|
+
|
201
|
+
e.preventDefault();
|
202
|
+
e.stopPropagation();
|
203
|
+
|
204
|
+
setResizingIndex(index);
|
205
|
+
setStartX(e.clientX);
|
206
|
+
setInitialWidth(columnWidths[index] || column.size || column.minWidth || 150);
|
207
|
+
};
|
208
|
+
|
63
209
|
const loadInitialData = async () => {
|
64
210
|
if (!dataSource) return;
|
65
211
|
|
@@ -84,10 +230,13 @@ function ReactDataTable({
|
|
84
230
|
skip: pageParam * defaultLimit,
|
85
231
|
limit: defaultLimit
|
86
232
|
});
|
233
|
+
|
234
|
+
// Update the data but maintain our column widths
|
87
235
|
setData(prev => ({
|
88
236
|
pages: [...prev.pages, nextData],
|
89
237
|
meta: nextData.meta
|
90
238
|
}));
|
239
|
+
|
91
240
|
setPageParam(prev => prev + 1);
|
92
241
|
} catch (error) {
|
93
242
|
console.error('Error fetching next page:', error);
|
@@ -170,78 +319,6 @@ function ReactDataTable({
|
|
170
319
|
handleScroll(tableContainerRef.current);
|
171
320
|
}, [data]);
|
172
321
|
|
173
|
-
const flatData = data.pages.flatMap(page => page.data);
|
174
|
-
|
175
|
-
const checkboxColumn = {
|
176
|
-
id: 'select',
|
177
|
-
size: 50,
|
178
|
-
minWidth: 50,
|
179
|
-
textAlign: "center",
|
180
|
-
header: ({ data }) => {
|
181
|
-
const allSelected = flatData.length > 0 && flatData.every(row => selectedRows[row.id]);
|
182
|
-
const someSelected = flatData.some(row => selectedRows[row.id]) && !allSelected;
|
183
|
-
|
184
|
-
|
185
|
-
return (
|
186
|
-
<div className="flex items-center justify-center h-[40px]">
|
187
|
-
{showSelectAllCheckbox && (
|
188
|
-
<div className="relative">
|
189
|
-
<input
|
190
|
-
id={data.id}
|
191
|
-
type="checkbox"
|
192
|
-
className='bg-gray-700 rounded-4 border-gray-200 text-blue-400 focus:ring-0 focus:ring-white'
|
193
|
-
checked={allSelected}
|
194
|
-
onChange={(e) => handleSelectAll(e.target.checked, flatData)}
|
195
|
-
/>
|
196
|
-
{allSelected ? (
|
197
|
-
<svg
|
198
|
-
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-3 h-3 pointer-events-none text-white"
|
199
|
-
viewBox="0 0 20 20"
|
200
|
-
fill="currentColor"
|
201
|
-
>
|
202
|
-
<path
|
203
|
-
fillRule="evenodd"
|
204
|
-
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
205
|
-
clipRule="evenodd"
|
206
|
-
/>
|
207
|
-
</svg>
|
208
|
-
) : (
|
209
|
-
someSelected &&
|
210
|
-
<svg
|
211
|
-
className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-3 h-3 pointer-events-none "
|
212
|
-
viewBox="0 0 20 20"
|
213
|
-
fill="currentColor"
|
214
|
-
>
|
215
|
-
<path
|
216
|
-
fillRule="evenodd"
|
217
|
-
d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
|
218
|
-
clipRule="evenodd"
|
219
|
-
/>
|
220
|
-
</svg>
|
221
|
-
)}
|
222
|
-
</div>
|
223
|
-
)}
|
224
|
-
</div>
|
225
|
-
);
|
226
|
-
},
|
227
|
-
cell: ({ row }) => {
|
228
|
-
return (
|
229
|
-
<div className="flex items-center justify-center h-[40px]" onClick={(e) => e.stopPropagation()}>
|
230
|
-
<input
|
231
|
-
id={row.id}
|
232
|
-
type="checkbox"
|
233
|
-
className='bg-gray-700 rounded-4 border-gray-200 text-blue-400 focus:ring-0 focus:ring-white'
|
234
|
-
checked={!!selectedRows[row.id]}
|
235
|
-
onClick={(e) => e.stopPropagation()}
|
236
|
-
onChange={(e) => { e.stopPropagation(); handleSelectRow(e.target.checked, row, flatData) }}
|
237
|
-
/>
|
238
|
-
</div>
|
239
|
-
)
|
240
|
-
}
|
241
|
-
};
|
242
|
-
|
243
|
-
const enhancedColumns = showCheckbox ? [checkboxColumn, ...columns] : columns;
|
244
|
-
|
245
322
|
return (
|
246
323
|
<div className="bg-white relative w-full react-live-data-table" >
|
247
324
|
{loading && (
|
@@ -273,7 +350,6 @@ function ReactDataTable({
|
|
273
350
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
274
351
|
/>
|
275
352
|
</svg>
|
276
|
-
|
277
353
|
</div>
|
278
354
|
)}
|
279
355
|
|
@@ -292,59 +368,97 @@ function ReactDataTable({
|
|
292
368
|
style={{ maxHeight, height }}
|
293
369
|
onScroll={(e) => handleScroll(e.currentTarget)}
|
294
370
|
>
|
295
|
-
<table
|
371
|
+
<table
|
372
|
+
className="w-full border-collapse"
|
373
|
+
style={{
|
374
|
+
tableLayout: 'fixed',
|
375
|
+
}}
|
376
|
+
>
|
296
377
|
<thead
|
297
|
-
className="sticky top-0 z-
|
378
|
+
className="sticky top-0 z-10 bg-blue-300"
|
298
379
|
style={{ ...headerProps.style }}
|
299
380
|
>
|
300
381
|
<tr>
|
301
|
-
{enhancedColumns.map((column, columnIndex) =>
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
382
|
+
{enhancedColumns.map((column, columnIndex) => {
|
383
|
+
// Use persisted column widths to ensure consistency
|
384
|
+
const width = columnWidths[columnIndex] || column.size || column.minWidth || 150;
|
385
|
+
|
386
|
+
return (
|
387
|
+
<th
|
388
|
+
key={column.accessorKey || column.id}
|
389
|
+
className={`text-left font-normal h-[40px] border-b border-t border-solid border-[#e4e3e2] relative select-none ${columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
|
390
|
+
}`}
|
391
|
+
style={{
|
392
|
+
width: `${width}px`,
|
393
|
+
minWidth: `${width}px`,
|
394
|
+
maxWidth: `${width}px`,
|
395
|
+
textAlign: column.textAlign,
|
396
|
+
}}
|
397
|
+
>
|
398
|
+
<div className="flex items-center h-full overflow-hidden justify-center pl-[17px]">
|
399
|
+
<span className="truncate">
|
400
|
+
{typeof column.header === 'function' ? column.header({ data: flatData }) : column.header}
|
401
|
+
</span>
|
402
|
+
</div>
|
403
|
+
|
404
|
+
{/* Resize handle - Only show if column is resizable */}
|
405
|
+
{column.resizable !== false && (
|
406
|
+
<div
|
407
|
+
className={`absolute top-0 right-0 h-full w-4 flex items-center justify-center group ${resizingIndex === columnIndex ? 'bg-blue-100' : 'hover:bg-blue-100'
|
408
|
+
} transition-colors duration-200`}
|
409
|
+
onMouseDown={(e) => handleResizeStart(e, columnIndex)}
|
410
|
+
style={{
|
411
|
+
touchAction: 'none',
|
412
|
+
userSelect: 'none',
|
413
|
+
cursor: 'col-resize'
|
414
|
+
}}
|
415
|
+
>
|
416
|
+
</div>
|
417
|
+
)}
|
418
|
+
</th>
|
419
|
+
);
|
420
|
+
})}
|
316
421
|
</tr>
|
317
422
|
</thead>
|
318
423
|
<tbody>
|
319
424
|
{flatData.length > 0 ? (
|
320
|
-
flatData.map((row,
|
321
|
-
const isLastRow =
|
425
|
+
flatData.map((row, rowIndex) => {
|
426
|
+
const isLastRow = rowIndex === flatData.length - 1;
|
322
427
|
return (
|
323
428
|
<tr
|
324
429
|
key={row.id}
|
325
|
-
className={`border-t ${isLastRow ? 'border-b' : ''} border-gray-200 hover:bg-[#dee1f2] ${selectedRows[row.id] ? 'bg-[#dee1f2]' : ''} ${rowClassName}`}
|
430
|
+
className={`border-t ${isLastRow ? 'border-b' : ''} border-gray-200 hover:bg-[#dee1f2] ${selectedRows[row.id] ? 'bg-[#dee1f2]' : ''} ${rowClassName} cursor-pointer`}
|
326
431
|
style={{
|
327
432
|
height: `${rowHeights}px`,
|
328
433
|
...rowStyle,
|
329
|
-
...(typeof rowStyle === 'function' ? rowStyle(row,
|
434
|
+
...(typeof rowStyle === 'function' ? rowStyle(row, rowIndex) : {})
|
330
435
|
}}
|
331
|
-
onClick={() => handleRowClick(row,
|
436
|
+
onClick={() => handleRowClick(row, rowIndex, flatData)}
|
332
437
|
>
|
333
|
-
{enhancedColumns.map((column,
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
438
|
+
{enhancedColumns.map((column, columnIndex) => {
|
439
|
+
// Use persisted column widths for cells as well
|
440
|
+
const width = columnWidths[columnIndex] || column.size || column.minWidth || 150;
|
441
|
+
|
442
|
+
return (
|
443
|
+
<td
|
444
|
+
key={column.accessorKey || column.id}
|
445
|
+
className={`text-left font-normal ${columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
|
446
|
+
} ${column?.cellProps?.className || ''}`}
|
447
|
+
style={{
|
448
|
+
width: `${width}px`,
|
449
|
+
minWidth: `${width}px`,
|
450
|
+
maxWidth: `${width}px`,
|
451
|
+
textAlign: column?.textAlign,
|
452
|
+
...column?.cellProps?.style,
|
453
|
+
overflow: 'hidden',
|
454
|
+
textOverflow: 'ellipsis',
|
455
|
+
whiteSpace: 'nowrap'
|
456
|
+
}}
|
457
|
+
>
|
458
|
+
{typeof column.cell === 'function' ? column.cell({ row }) : null}
|
459
|
+
</td>
|
460
|
+
);
|
461
|
+
})}
|
348
462
|
</tr>
|
349
463
|
);
|
350
464
|
})
|
@@ -361,7 +475,19 @@ function ReactDataTable({
|
|
361
475
|
</div>
|
362
476
|
)
|
363
477
|
}
|
364
|
-
|
478
|
+
|
479
|
+
{/* Resize overlay */}
|
480
|
+
{resizingIndex !== null && (
|
481
|
+
<div
|
482
|
+
className="fixed inset-0 z-40 bg-blue-50/5"
|
483
|
+
style={{
|
484
|
+
pointerEvents: 'none',
|
485
|
+
userSelect: 'none',
|
486
|
+
cursor: 'col-resize'
|
487
|
+
}}
|
488
|
+
/>
|
489
|
+
)}
|
490
|
+
</div>
|
365
491
|
);
|
366
492
|
}
|
367
493
|
|