@papernote/ui 1.10.25 → 1.10.26
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/dist/components/DataGrid.d.ts +6 -0
- package/dist/components/DataGrid.d.ts.map +1 -1
- package/dist/index.d.ts +6 -0
- package/dist/index.esm.js +78 -36
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +78 -36
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/DataGrid.tsx +159 -21
package/package.json
CHANGED
|
@@ -159,6 +159,12 @@ export interface DataGridProps {
|
|
|
159
159
|
className?: string;
|
|
160
160
|
/** Density */
|
|
161
161
|
density?: 'compact' | 'normal' | 'comfortable';
|
|
162
|
+
/** Enable virtual scrolling for large datasets (only renders visible rows) */
|
|
163
|
+
virtualized?: boolean;
|
|
164
|
+
/** Row height in pixels when virtualized (default: 40) */
|
|
165
|
+
virtualRowHeight?: number;
|
|
166
|
+
/** Number of rows to render above/below visible area (default: 5) */
|
|
167
|
+
virtualOverscan?: number;
|
|
162
168
|
}
|
|
163
169
|
|
|
164
170
|
/**
|
|
@@ -287,11 +293,15 @@ export const DataGrid = forwardRef<DataGridHandle, DataGridProps>(
|
|
|
287
293
|
toolbarActions,
|
|
288
294
|
className = '',
|
|
289
295
|
density = 'normal',
|
|
296
|
+
virtualized = false,
|
|
297
|
+
virtualRowHeight = 40,
|
|
298
|
+
virtualOverscan = 5,
|
|
290
299
|
},
|
|
291
300
|
ref
|
|
292
301
|
) => {
|
|
293
302
|
// State
|
|
294
303
|
const [data, setData] = useState<DataGridCell[][]>(initialData);
|
|
304
|
+
const [scrollTop, setScrollTop] = useState(0);
|
|
295
305
|
const [editingCell, setEditingCell] = useState<{ row: number; col: number } | null>(null);
|
|
296
306
|
const [editValue, setEditValue] = useState<string>('');
|
|
297
307
|
const [sortConfig, setSortConfig] = useState<SortConfig | null>(null);
|
|
@@ -358,25 +368,18 @@ export const DataGrid = forwardRef<DataGridHandle, DataGridProps>(
|
|
|
358
368
|
[groupColorMap]
|
|
359
369
|
);
|
|
360
370
|
|
|
361
|
-
// Check if a specific row is frozen
|
|
362
|
-
const isRowFrozen = useCallback(
|
|
363
|
-
(rowIndex: number) => {
|
|
364
|
-
if (frozenRowsState === 'none') return false;
|
|
365
|
-
if (frozenRowsState === 'first') return rowIndex === 0;
|
|
366
|
-
if (frozenRowsState === 'selected') {
|
|
367
|
-
return selectedCell ? rowIndex === selectedCell.row : false;
|
|
368
|
-
}
|
|
369
|
-
if (typeof frozenRowsState === 'number') return rowIndex < frozenRowsState;
|
|
370
|
-
return false;
|
|
371
|
-
},
|
|
372
|
-
[frozenRowsState, selectedCell]
|
|
373
|
-
);
|
|
374
|
-
|
|
375
371
|
// Update data when initialData changes
|
|
376
372
|
useEffect(() => {
|
|
377
373
|
setData(initialData);
|
|
378
374
|
}, [initialData]);
|
|
379
375
|
|
|
376
|
+
// Handle scroll for virtualization
|
|
377
|
+
const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
|
378
|
+
if (virtualized) {
|
|
379
|
+
setScrollTop(e.currentTarget.scrollTop);
|
|
380
|
+
}
|
|
381
|
+
}, [virtualized]);
|
|
382
|
+
|
|
380
383
|
// Get computed data with formulas evaluated
|
|
381
384
|
// Uses a cache to handle formula dependencies (formulas referencing other formulas)
|
|
382
385
|
const computedData = useMemo(() => {
|
|
@@ -496,6 +499,36 @@ export const DataGrid = forwardRef<DataGridHandle, DataGridProps>(
|
|
|
496
499
|
return [...frozenData, ...filtered];
|
|
497
500
|
}, [sortedData, filters, columns, frozenRows]);
|
|
498
501
|
|
|
502
|
+
// Calculate visible rows for virtualization
|
|
503
|
+
const visibleRowRange = useMemo(() => {
|
|
504
|
+
if (!virtualized) {
|
|
505
|
+
return { startIndex: 0, endIndex: filteredData.length, paddingTop: 0, paddingBottom: 0, frozenRowCount: 0 };
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const containerHeight = typeof height === 'number' ? height : 400;
|
|
509
|
+
const headerHeight = 40; // Approximate header height
|
|
510
|
+
const availableHeight = containerHeight - headerHeight;
|
|
511
|
+
|
|
512
|
+
// Account for frozen rows
|
|
513
|
+
const frozenRowCount = frozenRows;
|
|
514
|
+
const scrollableData = filteredData.slice(frozenRowCount);
|
|
515
|
+
|
|
516
|
+
const visibleCount = Math.ceil(availableHeight / virtualRowHeight);
|
|
517
|
+
const startIndex = Math.max(0, Math.floor(scrollTop / virtualRowHeight) - virtualOverscan);
|
|
518
|
+
const endIndex = Math.min(scrollableData.length, startIndex + visibleCount + (virtualOverscan * 2));
|
|
519
|
+
|
|
520
|
+
const paddingTop = startIndex * virtualRowHeight;
|
|
521
|
+
const paddingBottom = Math.max(0, (scrollableData.length - endIndex) * virtualRowHeight);
|
|
522
|
+
|
|
523
|
+
return {
|
|
524
|
+
startIndex: startIndex + frozenRowCount,
|
|
525
|
+
endIndex: endIndex + frozenRowCount,
|
|
526
|
+
paddingTop,
|
|
527
|
+
paddingBottom,
|
|
528
|
+
frozenRowCount
|
|
529
|
+
};
|
|
530
|
+
}, [virtualized, filteredData.length, scrollTop, height, virtualRowHeight, virtualOverscan, frozenRows]);
|
|
531
|
+
|
|
499
532
|
// Handle cell edit start
|
|
500
533
|
const handleCellDoubleClick = useCallback(
|
|
501
534
|
(rowIndex: number, colIndex: number, cellElement?: HTMLElement) => {
|
|
@@ -866,6 +899,7 @@ export const DataGrid = forwardRef<DataGridHandle, DataGridProps>(
|
|
|
866
899
|
className="relative overflow-auto border border-stone-200 rounded-lg bg-white"
|
|
867
900
|
style={{ height }}
|
|
868
901
|
onKeyDown={handleKeyDown}
|
|
902
|
+
onScroll={handleScroll}
|
|
869
903
|
tabIndex={0}
|
|
870
904
|
>
|
|
871
905
|
<table className="border-collapse" style={{ tableLayout: 'auto' }}>
|
|
@@ -974,18 +1008,23 @@ export const DataGrid = forwardRef<DataGridHandle, DataGridProps>(
|
|
|
974
1008
|
|
|
975
1009
|
{/* Body */}
|
|
976
1010
|
<tbody>
|
|
977
|
-
{
|
|
978
|
-
|
|
1011
|
+
{/* Top spacer for virtualization */}
|
|
1012
|
+
{virtualized && visibleRowRange.paddingTop > 0 && (
|
|
1013
|
+
<tr style={{ height: visibleRowRange.paddingTop }}>
|
|
1014
|
+
<td colSpan={columns.length + (rowHeaders ? 1 : 0)} />
|
|
1015
|
+
</tr>
|
|
1016
|
+
)}
|
|
1017
|
+
|
|
1018
|
+
{/* Render frozen rows first (always visible) */}
|
|
1019
|
+
{filteredData.slice(0, visibleRowRange.frozenRowCount).map((row, rowIndex) => {
|
|
979
1020
|
const isZebra = zebraStripes && rowIndex % 2 === 1;
|
|
980
1021
|
|
|
981
1022
|
return (
|
|
982
1023
|
<tr
|
|
983
|
-
key={rowIndex}
|
|
984
|
-
className={`${isZebra ? 'bg-paper-50' : 'bg-white'}
|
|
985
|
-
isFrozen ? 'sticky z-10' : ''
|
|
986
|
-
} ${isFrozen ? 'shadow-sm' : ''}`}
|
|
1024
|
+
key={`frozen-${rowIndex}`}
|
|
1025
|
+
className={`${isZebra ? 'bg-paper-50' : 'bg-white'} sticky z-10 shadow-sm`}
|
|
987
1026
|
style={{
|
|
988
|
-
top:
|
|
1027
|
+
top: `${40 + rowIndex * virtualRowHeight}px`,
|
|
989
1028
|
}}
|
|
990
1029
|
>
|
|
991
1030
|
{/* Row header */}
|
|
@@ -1023,6 +1062,7 @@ export const DataGrid = forwardRef<DataGridHandle, DataGridProps>(
|
|
|
1023
1062
|
style={{
|
|
1024
1063
|
left: isFrozenCol ? leftOffset : undefined,
|
|
1025
1064
|
minWidth: column?.minWidth || 80,
|
|
1065
|
+
height: virtualized ? virtualRowHeight : undefined,
|
|
1026
1066
|
}}
|
|
1027
1067
|
onClick={() => handleCellClick(rowIndex, colIndex)}
|
|
1028
1068
|
onDoubleClick={(e) => handleCellDoubleClick(rowIndex, colIndex, e.currentTarget)}
|
|
@@ -1058,6 +1098,104 @@ export const DataGrid = forwardRef<DataGridHandle, DataGridProps>(
|
|
|
1058
1098
|
</tr>
|
|
1059
1099
|
);
|
|
1060
1100
|
})}
|
|
1101
|
+
|
|
1102
|
+
{/* Render visible rows (virtualized or all) */}
|
|
1103
|
+
{filteredData
|
|
1104
|
+
.slice(
|
|
1105
|
+
virtualized ? Math.max(visibleRowRange.startIndex, visibleRowRange.frozenRowCount) : visibleRowRange.frozenRowCount,
|
|
1106
|
+
virtualized ? visibleRowRange.endIndex : filteredData.length
|
|
1107
|
+
)
|
|
1108
|
+
.map((row, idx) => {
|
|
1109
|
+
const rowIndex = virtualized
|
|
1110
|
+
? Math.max(visibleRowRange.startIndex, visibleRowRange.frozenRowCount) + idx
|
|
1111
|
+
: visibleRowRange.frozenRowCount + idx;
|
|
1112
|
+
const isZebra = zebraStripes && rowIndex % 2 === 1;
|
|
1113
|
+
|
|
1114
|
+
return (
|
|
1115
|
+
<tr
|
|
1116
|
+
key={rowIndex}
|
|
1117
|
+
className={`${isZebra ? 'bg-paper-50' : 'bg-white'}`}
|
|
1118
|
+
style={{
|
|
1119
|
+
height: virtualized ? virtualRowHeight : undefined,
|
|
1120
|
+
}}
|
|
1121
|
+
>
|
|
1122
|
+
{/* Row header */}
|
|
1123
|
+
{rowHeaders && (
|
|
1124
|
+
<td
|
|
1125
|
+
className={`${cellPadding} border-b border-r border-stone-200 bg-stone-50 text-ink-500 font-medium sticky left-0 z-10`}
|
|
1126
|
+
style={{ width: 50, minWidth: 50, maxWidth: 50 }}
|
|
1127
|
+
>
|
|
1128
|
+
{Array.isArray(rowHeaders) ? rowHeaders[rowIndex] : rowIndex + 1}
|
|
1129
|
+
</td>
|
|
1130
|
+
)}
|
|
1131
|
+
|
|
1132
|
+
{/* Data cells */}
|
|
1133
|
+
{row.map((cell, colIndex) => {
|
|
1134
|
+
const column = columns[colIndex];
|
|
1135
|
+
const isFrozenCol = colIndex < frozenColumns;
|
|
1136
|
+
const isEditing =
|
|
1137
|
+
editingCell?.row === rowIndex && editingCell?.col === colIndex;
|
|
1138
|
+
const isSelected =
|
|
1139
|
+
selectedCell?.row === rowIndex && selectedCell?.col === colIndex;
|
|
1140
|
+
const hasFormula = !!cell?.formula;
|
|
1141
|
+
const leftOffset = rowHeaders
|
|
1142
|
+
? 50 + columns.slice(0, colIndex).reduce((sum, c) => sum + (c.width || 100), 0)
|
|
1143
|
+
: columns.slice(0, colIndex).reduce((sum, c) => sum + (c.width || 100), 0);
|
|
1144
|
+
const cellBgClass = getCellBgClass(column, isZebra, isFrozenCol);
|
|
1145
|
+
|
|
1146
|
+
return (
|
|
1147
|
+
<td
|
|
1148
|
+
key={colIndex}
|
|
1149
|
+
className={`${cellPadding} border-b border-r border-stone-200 text-${
|
|
1150
|
+
column?.align || 'left'
|
|
1151
|
+
} ${isFrozenCol ? 'sticky z-10' : ''} ${cellBgClass} ${
|
|
1152
|
+
isSelected ? 'ring-2 ring-inset ring-primary-500' : ''
|
|
1153
|
+
} ${hasFormula ? 'bg-blue-50' : ''} ${cell?.className || ''}`}
|
|
1154
|
+
style={{
|
|
1155
|
+
left: isFrozenCol ? leftOffset : undefined,
|
|
1156
|
+
minWidth: column?.minWidth || 80,
|
|
1157
|
+
}}
|
|
1158
|
+
onClick={() => handleCellClick(rowIndex, colIndex)}
|
|
1159
|
+
onDoubleClick={(e) => handleCellDoubleClick(rowIndex, colIndex, e.currentTarget)}
|
|
1160
|
+
>
|
|
1161
|
+
{isEditing ? (
|
|
1162
|
+
formulas ? (
|
|
1163
|
+
<FormulaAutocomplete
|
|
1164
|
+
value={editValue}
|
|
1165
|
+
onChange={setEditValue}
|
|
1166
|
+
onComplete={handleEditComplete}
|
|
1167
|
+
onCancel={handleEditCancel}
|
|
1168
|
+
anchorRect={editingCellRect}
|
|
1169
|
+
autoFocus
|
|
1170
|
+
/>
|
|
1171
|
+
) : (
|
|
1172
|
+
<input
|
|
1173
|
+
ref={inputRef}
|
|
1174
|
+
type="text"
|
|
1175
|
+
value={editValue}
|
|
1176
|
+
onChange={(e) => setEditValue(e.target.value)}
|
|
1177
|
+
onBlur={handleEditComplete}
|
|
1178
|
+
onKeyDown={handleEditKeyDown}
|
|
1179
|
+
className="w-full h-full border-none outline-none bg-transparent"
|
|
1180
|
+
style={{ margin: '-4px', padding: '4px' }}
|
|
1181
|
+
/>
|
|
1182
|
+
)
|
|
1183
|
+
) : (
|
|
1184
|
+
formatValue(cell?.value, column)
|
|
1185
|
+
)}
|
|
1186
|
+
</td>
|
|
1187
|
+
);
|
|
1188
|
+
})}
|
|
1189
|
+
</tr>
|
|
1190
|
+
);
|
|
1191
|
+
})}
|
|
1192
|
+
|
|
1193
|
+
{/* Bottom spacer for virtualization */}
|
|
1194
|
+
{virtualized && visibleRowRange.paddingBottom > 0 && (
|
|
1195
|
+
<tr style={{ height: visibleRowRange.paddingBottom }}>
|
|
1196
|
+
<td colSpan={columns.length + (rowHeaders ? 1 : 0)} />
|
|
1197
|
+
</tr>
|
|
1198
|
+
)}
|
|
1061
1199
|
</tbody>
|
|
1062
1200
|
</table>
|
|
1063
1201
|
</div>
|