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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/ReactDataTable.jsx +242 -116
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.17",
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,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 className="w-full border-collapse">
371
+ <table
372
+ className="w-full border-collapse"
373
+ style={{
374
+ tableLayout: 'fixed',
375
+ }}
376
+ >
296
377
  <thead
297
- className="sticky top-0 z-1 bg-blue-300"
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
- <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
- ))}
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, index) => {
321
- const isLastRow = index === flatData.length - 1;
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, index) : {})
434
+ ...(typeof rowStyle === 'function' ? rowStyle(row, rowIndex) : {})
330
435
  }}
331
- onClick={() => handleRowClick(row, index, flatData)}
436
+ onClick={() => handleRowClick(row, rowIndex, flatData)}
332
437
  >
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
- ))}
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
- </div >
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