react-live-data-table 1.0.16 → 1.0.18

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/ReactDataTable.jsx +319 -117
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-live-data-table",
3
- "version": "1.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Your React component package with Tailwind",
5
5
  "main": "src/index.js",
6
6
  "module": "dist/index.esm.js",
@@ -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,9 @@ function ReactDataTable({
18
18
  headerProps = {},
19
19
  selected = {},
20
20
  showSelectAllCheckbox = true,
21
- rowStyle = {},
22
- rowClassName = ""
21
+ rowStyle = {},
22
+ rowClassName = "",
23
+ columnReorder = false,
23
24
  }) {
24
25
  const tableContainerRef = React.useRef(null);
25
26
  const [data, setData] = React.useState({ pages: [], meta: { totalPages: 1 } });
@@ -27,15 +28,122 @@ function ReactDataTable({
27
28
  const [pageParam, setPageParam] = React.useState(1);
28
29
  const [selectedRows, setSelectedRows] = React.useState(selected);
29
30
  const previousSelected = React.useRef(selected);
31
+ const [resizingIndex, setResizingIndex] = useState(null);
32
+ const [columnWidths, setColumnWidths] = useState([]);
33
+ const [startX, setStartX] = useState(null);
34
+ const [initialWidth, setInitialWidth] = useState(null);
35
+ const [tableWidth, setTableWidth] = useState(0);
36
+
37
+ // Column reordering state - only used when columnReorder is true
38
+ const [orderedColumns, setOrderedColumns] = useState(columns);
39
+ const [draggedColumn, setDraggedColumn] = useState(null);
40
+ const [dragOverColumn, setDragOverColumn] = useState(null);
41
+
42
+ // Ref to store column widths to ensure persistence during data loading
43
+ const persistedColumnWidthsRef = React.useRef([]);
44
+
45
+ const flatData = data.pages.flatMap(page => page.data);
46
+
47
+ const checkboxColumn = {
48
+ id: 'select',
49
+ size: 50,
50
+ minWidth: 50,
51
+ resizable: false,
52
+ reorderable: false, // Prevent checkbox column from being reordered
53
+ textAlign: "center",
54
+ header: ({ data }) => {
55
+ const allSelected = flatData.length > 0 && flatData.every(row => selectedRows[row.id]);
56
+ const someSelected = flatData.some(row => selectedRows[row.id]) && !allSelected;
57
+
58
+ return (
59
+ <div className="flex items-center justify-center h-[40px]">
60
+ {showSelectAllCheckbox && (
61
+ <div className="relative">
62
+ <input
63
+ id={data.id}
64
+ type="checkbox"
65
+ className='bg-gray-700 rounded-4 border-gray-200 text-blue-400 focus:ring-0 focus:ring-white'
66
+ checked={allSelected}
67
+ onChange={(e) => handleSelectAll(e.target.checked, flatData)}
68
+ />
69
+ {allSelected ? (
70
+ <svg
71
+ 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"
72
+ viewBox="0 0 20 20"
73
+ fill="currentColor"
74
+ >
75
+ <path
76
+ fillRule="evenodd"
77
+ 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"
78
+ clipRule="evenodd"
79
+ />
80
+ </svg>
81
+ ) : (
82
+ someSelected &&
83
+ <svg
84
+ className="absolute top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 w-3 h-3 pointer-events-none "
85
+ viewBox="0 0 20 20"
86
+ fill="currentColor"
87
+ >
88
+ <path
89
+ fillRule="evenodd"
90
+ d="M3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z"
91
+ clipRule="evenodd"
92
+ />
93
+ </svg>
94
+ )}
95
+ </div>
96
+ )}
97
+ </div>
98
+ );
99
+ },
100
+ cell: ({ row }) => {
101
+ return (
102
+ <div className="flex items-center justify-center h-[40px]" onClick={(e) => e.stopPropagation()}>
103
+ <input
104
+ id={row.id}
105
+ type="checkbox"
106
+ className='bg-gray-700 rounded-4 border-gray-200 text-blue-400 focus:ring-0 focus:ring-white'
107
+ checked={!!selectedRows[row.id]}
108
+ onClick={(e) => e.stopPropagation()}
109
+ onChange={(e) => { e.stopPropagation(); handleSelectRow(e.target.checked, row, flatData) }}
110
+ />
111
+ </div>
112
+ )
113
+ }
114
+ };
115
+
116
+ const enhancedColumns = showCheckbox ? [checkboxColumn, ...orderedColumns] : orderedColumns;
117
+
118
+ // Update ordered columns when columns prop changes
119
+ useEffect(() => {
120
+ setOrderedColumns(columns);
121
+ }, [columns]);
30
122
 
31
-
32
123
  useEffect(() => {
33
124
  if (JSON.stringify(previousSelected.current) !== JSON.stringify(selected)) {
34
- setSelectedRows({...selected});
125
+ setSelectedRows({ ...selected });
35
126
  previousSelected.current = selected;
36
127
  }
37
128
  }, [selected]);
38
129
 
130
+ // Initialize column widths array only once when component mounts or columns change
131
+ useEffect(() => {
132
+ const allColumns = showCheckbox ? [{ id: 'select', size: 50 }, ...orderedColumns] : orderedColumns;
133
+
134
+ // If we have persisted widths and the number of columns matches, use those
135
+ if (persistedColumnWidthsRef.current.length === allColumns.length) {
136
+ setColumnWidths([...persistedColumnWidthsRef.current]);
137
+ setTableWidth(persistedColumnWidthsRef.current.reduce((sum, width) => sum + width, 0));
138
+ } else {
139
+ // Otherwise initialize with default widths
140
+ const initialWidths = allColumns.map(column => column.size || column.minWidth || 150);
141
+ setColumnWidths(initialWidths);
142
+ setTableWidth(initialWidths.reduce((sum, width) => sum + width, 0));
143
+ // Store in our ref for persistence
144
+ persistedColumnWidthsRef.current = [...initialWidths];
145
+ }
146
+ }, [orderedColumns, showCheckbox]);
39
147
 
40
148
  useEffect(() => {
41
149
  setData({ pages: [], meta: { totalPages: 1 } });
@@ -60,6 +168,112 @@ function ReactDataTable({
60
168
  }
61
169
  }, [dataSource, staticData]);
62
170
 
171
+ // Column reordering handlers - only work when columnReorder is true
172
+ const handleDragStart = (e, columnIndex) => {
173
+ if (!columnReorder || enhancedColumns[columnIndex].reorderable === false) {
174
+ e.preventDefault();
175
+ return;
176
+ }
177
+ setDraggedColumn(columnIndex);
178
+ e.dataTransfer.effectAllowed = 'move';
179
+ };
180
+
181
+ const handleDragOver = (e, columnIndex) => {
182
+ e.preventDefault();
183
+ if (!columnReorder || enhancedColumns[columnIndex].reorderable === false) return;
184
+ setDragOverColumn(columnIndex);
185
+ };
186
+
187
+ const handleDrop = (e, dropIndex) => {
188
+ e.preventDefault();
189
+
190
+ if (!columnReorder || draggedColumn === null || draggedColumn === dropIndex) {
191
+ setDraggedColumn(null);
192
+ setDragOverColumn(null);
193
+ return;
194
+ }
195
+
196
+ if (enhancedColumns[dropIndex].reorderable === false) {
197
+ setDraggedColumn(null);
198
+ setDragOverColumn(null);
199
+ return;
200
+ }
201
+
202
+ const newColumns = [...enhancedColumns];
203
+ const draggedColumnData = newColumns[draggedColumn];
204
+
205
+ newColumns.splice(draggedColumn, 1);
206
+ const adjustedDropIndex = draggedColumn < dropIndex ? dropIndex - 1 : dropIndex;
207
+ newColumns.splice(adjustedDropIndex, 0, draggedColumnData);
208
+
209
+ const updatedOrderedColumns = showCheckbox ? newColumns.slice(1) : newColumns;
210
+ setOrderedColumns(updatedOrderedColumns);
211
+
212
+ // Reorder column widths
213
+ const newColumnWidths = [...columnWidths];
214
+ const draggedWidth = newColumnWidths[draggedColumn];
215
+ newColumnWidths.splice(draggedColumn, 1);
216
+ newColumnWidths.splice(adjustedDropIndex, 0, draggedWidth);
217
+ setColumnWidths(newColumnWidths);
218
+ persistedColumnWidthsRef.current = newColumnWidths;
219
+
220
+ // onColumnReorder?.(updatedOrderedColumns);
221
+
222
+ setDraggedColumn(null);
223
+ setDragOverColumn(null);
224
+ };
225
+
226
+ const handleMouseMove = useCallback((e) => {
227
+ if (resizingIndex === null || startX === null || initialWidth === null) return;
228
+
229
+ const delta = e.clientX - startX;
230
+ const newWidth = Math.max(enhancedColumns[resizingIndex]?.minWidth || 80, initialWidth + delta);
231
+
232
+ // Create a new array of column widths with the updated width
233
+ const newColumnWidths = [...columnWidths];
234
+ newColumnWidths[resizingIndex] = newWidth;
235
+
236
+ // Update the column widths state
237
+ setColumnWidths(newColumnWidths);
238
+
239
+ // Update our persisted ref to maintain widths during pagination
240
+ persistedColumnWidthsRef.current = newColumnWidths;
241
+
242
+ // Recalculate table width based on the new column widths
243
+ const newTableWidth = newColumnWidths.reduce((sum, width) => sum + width, 0);
244
+ setTableWidth(newTableWidth);
245
+ }, [resizingIndex, startX, initialWidth, enhancedColumns, columnWidths]);
246
+
247
+ const handleMouseUp = useCallback(() => {
248
+ setResizingIndex(null);
249
+ setStartX(null);
250
+ setInitialWidth(null);
251
+ }, []);
252
+
253
+ // Add resize event listeners
254
+ useEffect(() => {
255
+ if (resizingIndex !== null) {
256
+ document.addEventListener('mousemove', handleMouseMove);
257
+ document.addEventListener('mouseup', handleMouseUp);
258
+ return () => {
259
+ document.removeEventListener('mousemove', handleMouseMove);
260
+ document.removeEventListener('mouseup', handleMouseUp);
261
+ };
262
+ }
263
+ }, [resizingIndex, handleMouseMove, handleMouseUp]);
264
+
265
+ const handleResizeStart = (e, index) => {
266
+ const column = enhancedColumns[index];
267
+ if (!column.resizable) return;
268
+
269
+ e.preventDefault();
270
+ e.stopPropagation();
271
+
272
+ setResizingIndex(index);
273
+ setStartX(e.clientX);
274
+ setInitialWidth(columnWidths[index] || column.size || column.minWidth || 150);
275
+ };
276
+
63
277
  const loadInitialData = async () => {
64
278
  if (!dataSource) return;
65
279
 
@@ -84,10 +298,13 @@ function ReactDataTable({
84
298
  skip: pageParam * defaultLimit,
85
299
  limit: defaultLimit
86
300
  });
301
+
302
+ // Update the data but maintain our column widths
87
303
  setData(prev => ({
88
304
  pages: [...prev.pages, nextData],
89
305
  meta: nextData.meta
90
306
  }));
307
+
91
308
  setPageParam(prev => prev + 1);
92
309
  } catch (error) {
93
310
  console.error('Error fetching next page:', error);
@@ -170,78 +387,6 @@ function ReactDataTable({
170
387
  handleScroll(tableContainerRef.current);
171
388
  }, [data]);
172
389
 
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
390
  return (
246
391
  <div className="bg-white relative w-full react-live-data-table" >
247
392
  {loading && (
@@ -273,7 +418,6 @@ function ReactDataTable({
273
418
  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
419
  />
275
420
  </svg>
276
-
277
421
  </div>
278
422
  )}
279
423
 
@@ -292,59 +436,105 @@ function ReactDataTable({
292
436
  style={{ maxHeight, height }}
293
437
  onScroll={(e) => handleScroll(e.currentTarget)}
294
438
  >
295
- <table className="w-full border-collapse">
439
+ <table
440
+ className="w-full border-collapse"
441
+ style={{
442
+ tableLayout: 'fixed',
443
+ }}
444
+ >
296
445
  <thead
297
- className="sticky top-0 z-1 bg-blue-300"
446
+ className="sticky top-0 z-10 bg-blue-300"
298
447
  style={{ ...headerProps.style }}
299
448
  >
300
- <tr>
301
- {enhancedColumns.map((column, columnIndex) => (
302
- <th
303
- key={column.accessorKey || column.id}
304
- className={`text-left font-normal h-[40px] border-b border-t border-solid border-[#e4e3e2] ${
305
- columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
306
- }`}
307
- style={{
308
- width: column.size,
309
- minWidth: column.minWidth,
310
- textAlign: column.textAlign,
311
- }}
312
- >
313
- {typeof column.header === 'function' ? column.header({ data: flatData }) : column.header}
314
- </th>
315
- ))}
449
+ <tr className='react-live-data-table-row-header'>
450
+ {enhancedColumns.map((column, columnIndex) => {
451
+ const width = columnWidths[columnIndex] || column.size || column.minWidth || 150;
452
+ const isReorderable = columnReorder && column.reorderable !== false;
453
+ const isDropTarget = dragOverColumn === columnIndex && draggedColumn !== columnIndex;
454
+
455
+ return (
456
+ <th
457
+ key={column.accessorKey || column.id}
458
+ className={`text-left font-normal h-[40px] border-b border-t border-solid border-[#e4e3e2] relative select-none ${
459
+ columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
460
+ } ${isReorderable ? 'cursor-move' : ''} ${
461
+ isDropTarget ? 'bg-blue-400' : ''
462
+ }`}
463
+ style={{
464
+ width: `${width}px`,
465
+ minWidth: `${width}px`,
466
+ maxWidth: `${width}px`,
467
+ textAlign: column.textAlign,
468
+ }}
469
+ draggable={isReorderable}
470
+ onDragStart={(e) => handleDragStart(e, columnIndex)}
471
+ onDragOver={(e) => handleDragOver(e, columnIndex)}
472
+ onDrop={(e) => handleDrop(e, columnIndex)}
473
+ >
474
+ <div className="flex items-center h-full overflow-hidden justify-center pl-[17px]">
475
+ <span className="truncate">
476
+ {typeof column.header === 'function' ? column.header({ data: flatData }) : column.header}
477
+ </span>
478
+ </div>
479
+
480
+ {/* Resize handle - Only show if column is resizable */}
481
+ {column.resizable !== false && (
482
+ <div
483
+ className={`absolute top-0 right-0 h-full w-4 flex items-center justify-center group ${
484
+ resizingIndex === columnIndex ? 'bg-blue-100' : 'hover:bg-blue-100'
485
+ } transition-colors duration-200`}
486
+ onMouseDown={(e) => handleResizeStart(e, columnIndex)}
487
+ style={{
488
+ touchAction: 'none',
489
+ userSelect: 'none',
490
+ cursor: 'col-resize'
491
+ }}
492
+ >
493
+ </div>
494
+ )}
495
+ </th>
496
+ );
497
+ })}
316
498
  </tr>
317
499
  </thead>
318
500
  <tbody>
319
501
  {flatData.length > 0 ? (
320
- flatData.map((row, index) => {
321
- const isLastRow = index === flatData.length - 1;
502
+ flatData.map((row, rowIndex) => {
503
+ const isLastRow = rowIndex === flatData.length - 1;
322
504
  return (
323
505
  <tr
324
506
  key={row.id}
325
- className={`border-t ${isLastRow ? 'border-b' : ''} border-gray-200 hover:bg-[#dee1f2] ${selectedRows[row.id] ? 'bg-[#dee1f2]' : ''} ${rowClassName}`}
507
+ className={`react-live-data-table-row-${index} border-t ${isLastRow ? 'border-b' : ''} border-gray-200 hover:bg-[#dee1f2] ${selectedRows[row.id] ? 'bg-[#dee1f2]' : ''} ${rowClassName} cursor-pointer`}
326
508
  style={{
327
509
  height: `${rowHeights}px`,
328
510
  ...rowStyle,
329
- ...(typeof rowStyle === 'function' ? rowStyle(row, index) : {})
511
+ ...(typeof rowStyle === 'function' ? rowStyle(row, rowIndex) : {})
330
512
  }}
331
- onClick={() => handleRowClick(row, index, flatData)}
513
+ onClick={() => handleRowClick(row, rowIndex, flatData)}
332
514
  >
333
- {enhancedColumns.map((column, cellIndex) => (
334
- <td
335
- key={column.accessorKey || column.id}
336
- className={`text-left font-normal ${
337
- cellIndex < enhancedColumns.length-1 ? 'border-r' : ''
338
- } ${column?.cellProps?.className || ''}`}
339
- style={{
340
- minWidth: `${column.minWidth}px`,
341
- textAlign: column?.textAlign,
342
- ...column?.cellProps?.style,
343
- }}
344
- >
345
- {typeof column.cell === 'function' ? column.cell({ row }) : null}
346
- </td>
347
- ))}
515
+ {enhancedColumns.map((column, columnIndex) => {
516
+ const width = columnWidths[columnIndex] || column.size || column.minWidth || 150;
517
+
518
+ return (
519
+ <td
520
+ key={column.accessorKey || column.id}
521
+ className={`text-left font-normal ${columnIndex < enhancedColumns.length - 1 ? 'border-r' : ''
522
+ } ${column?.cellProps?.className || ''}`}
523
+ style={{
524
+ width: `${width}px`,
525
+ minWidth: `${width}px`,
526
+ maxWidth: `${width}px`,
527
+ textAlign: column?.textAlign,
528
+ ...column?.cellProps?.style,
529
+ overflow: 'hidden',
530
+ textOverflow: 'ellipsis',
531
+ whiteSpace: 'nowrap'
532
+ }}
533
+ >
534
+ {typeof column.cell === 'function' ? column.cell({ row }) : null}
535
+ </td>
536
+ );
537
+ })}
348
538
  </tr>
349
539
  );
350
540
  })
@@ -361,7 +551,19 @@ function ReactDataTable({
361
551
  </div>
362
552
  )
363
553
  }
364
- </div >
554
+
555
+ {/* Resize overlay */}
556
+ {resizingIndex !== null && (
557
+ <div
558
+ className="fixed inset-0 z-40 bg-blue-50/5"
559
+ style={{
560
+ pointerEvents: 'none',
561
+ userSelect: 'none',
562
+ cursor: 'col-resize'
563
+ }}
564
+ />
565
+ )}
566
+ </div>
365
567
  );
366
568
  }
367
569