@ornery/ui-grid-react 0.1.4

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.
@@ -0,0 +1,1414 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import {
3
+ createGridApi,
4
+ UiGridApi,
5
+ GridApiBindings,
6
+ GridOptions,
7
+ GridColumnDef,
8
+ GridRow,
9
+ GridRecord,
10
+ GridBenchmarkResult,
11
+ GridCellPosition,
12
+ GridLabels,
13
+ SortState,
14
+ GridSavedState,
15
+ GridCellEditableContext,
16
+ getCellValue,
17
+ setPathValue,
18
+ SORT_DIRECTIONS,
19
+ buildGridPipeline,
20
+ resolveGridLabels,
21
+ gridColumnWidth,
22
+ headerLabel as coreHeaderLabel,
23
+ gridSortButtonLabel,
24
+ gridSortAriaSort,
25
+ gridGroupingButtonLabel,
26
+ gridFilterPlaceholder,
27
+ gridGroupDisclosureLabel,
28
+ gridEditorInputType,
29
+ gridCellIndent,
30
+ gridTreeToggleLabelForRow,
31
+ gridExpandToggleLabelForRow,
32
+ isGridTreeRowExpanded,
33
+ isGridColumnSortable,
34
+ isGridColumnFilterable,
35
+ isGridColumnGrouped,
36
+ isGridGroupingEnabled,
37
+ isGridTreeEnabled,
38
+ isGridPaginationEnabled,
39
+ isGridSortingEnabled,
40
+ isGridFilteringEnabled,
41
+ canGridMoveColumns,
42
+ isGridPrimaryColumn,
43
+ shouldShowGridTreeToggle,
44
+ shouldShowGridExpandToggle,
45
+ shouldShowGridPaginationControls,
46
+ buildGridCellContext,
47
+ formatGridCellDisplayValue,
48
+ buildGridFocusCellResult,
49
+ findNextGridCell,
50
+ isPrintableGridKey,
51
+ isGridCellPosition,
52
+ exportCsvRows,
53
+ buildGridRows,
54
+ resolveGridRowId as coreResolveGridRowId,
55
+ findGridRowById as coreFindGridRowById,
56
+ getEffectivePageSize as coreGetEffectivePageSize,
57
+ getCurrentPageValue as coreGetCurrentPageValue,
58
+ getTotalPagesValue as coreGetTotalPagesValue,
59
+ getFirstRowIndexValue as coreGetFirstRowIndexValue,
60
+ getLastRowIndexValue as coreGetLastRowIndexValue,
61
+ isVirtualizationEnabled as coreIsVirtualizationEnabled,
62
+ buildGridSavedState,
63
+ sanitizeDownloadFilename,
64
+ parseGridEditedValue,
65
+ stringifyGridEditorValue,
66
+ canGridExpandRows,
67
+ areAllGridRowsExpanded,
68
+ addGridRowInvisibleReason,
69
+ clearGridRowInvisibleReason,
70
+ FEATURE_SORTING,
71
+ FEATURE_FILTERING,
72
+ FEATURE_GROUPING,
73
+ FEATURE_PAGINATION,
74
+ FEATURE_CELL_EDIT,
75
+ FEATURE_EXPANDABLE,
76
+ FEATURE_TREE_VIEW,
77
+ FEATURE_INFINITE_SCROLL,
78
+ FEATURE_COLUMN_MOVING,
79
+ FEATURE_CSV_EXPORT,
80
+ FEATURE_AUTO_RESIZE,
81
+ FEATURE_SAVE_STATE,
82
+ } from '@ornery/ui-grid';
83
+ import type {
84
+ DisplayItem,
85
+ GroupItem,
86
+ ExpandableItem,
87
+ RowItem,
88
+ PipelineResult,
89
+ GridInfiniteScrollState,
90
+ GridMoveDirection,
91
+ GridCellTemplateContext,
92
+ GridExpandableTemplateContext,
93
+ } from '@ornery/ui-grid';
94
+ import {
95
+ applyGridSortStateCommand,
96
+ updateGridFilterCommand,
97
+ clearGridFiltersCommand,
98
+ clearGridGroupingCommand,
99
+ moveGridColumnCommand,
100
+ moveGridVisibleColumnCommand,
101
+ seekGridPaginationCommand,
102
+ setGridPaginationPageSizeCommand,
103
+ sortGridColumnCommand,
104
+ toggleGridRowExpansionCommand,
105
+ expandAllGridRowsCommand,
106
+ collapseAllGridRowsCommand,
107
+ toggleGridTreeRowCommand,
108
+ setGridTreeRowExpandedCommand,
109
+ expandAllGridTreeRowsCommand,
110
+ collapseAllGridTreeRowsCommand,
111
+ beginGridCellEditCommand,
112
+ commitGridCellEditCommand,
113
+ cancelGridCellEditCommand,
114
+ maybeRequestInfiniteScrollCommand,
115
+ completeGridInfiniteScrollDataLoadCommand,
116
+ resetGridInfiniteScrollCommand,
117
+ saveGridInfiniteScrollPercentageCommand,
118
+ setGridInfiniteScrollDirectionsCommand,
119
+ restoreGridStateCommand,
120
+ } from '../../ui-grid/src/lib/grid/ui-grid.commands';
121
+ import {
122
+ raiseGridRenderingComplete,
123
+ raiseGridRowsRendered,
124
+ raiseGridRowsVisibleChanged,
125
+ raiseGridCanvasHeightChanged,
126
+ raiseGridDimensionChanged,
127
+ raiseGridScrollBegin,
128
+ raiseGridScrollEnd,
129
+ raiseGridBenchmarkComplete,
130
+ } from '../../ui-grid/src/lib/grid/ui-grid.events';
131
+ import {
132
+ downloadGridCsvFile,
133
+ observeGridHostSize,
134
+ } from '../../ui-grid/src/lib/grid/ui-grid.host';
135
+
136
+ export interface UseGridStateResult {
137
+ pipeline: PipelineResult;
138
+ visibleColumns: GridColumnDef[];
139
+ labels: GridLabels;
140
+ gridTemplateColumns: string;
141
+ gridApi: UiGridApi;
142
+ gridContainerRef: React.RefObject<HTMLDivElement | null>;
143
+
144
+ // State
145
+ activeFilters: Record<string, string>;
146
+ groupByColumns: string[];
147
+ collapsedGroups: Record<string, boolean>;
148
+ sortState: SortState;
149
+ focusedCell: GridCellPosition | null;
150
+ editingCell: GridCellPosition | null;
151
+ editingValue: string;
152
+ expandedRows: Record<string, boolean>;
153
+ expandedTreeRows: Record<string, boolean>;
154
+ currentPage: number;
155
+ pageSize: number;
156
+ benchmarkResult: GridBenchmarkResult | null;
157
+ infiniteScrollState: GridInfiniteScrollState;
158
+
159
+ // Computed
160
+ totalRows: number;
161
+ visibleRowCount: number;
162
+ displayItems: DisplayItem[];
163
+ virtualizationEnabled: boolean;
164
+ pipelineMs: number;
165
+ paginationCurrentPage: number;
166
+ paginationTotalPages: number;
167
+ paginationSelectedPageSize: number;
168
+ rowSize: number;
169
+ viewportHeightPx: string;
170
+
171
+ // Display helpers
172
+ headerLabel: (column: GridColumnDef) => string;
173
+ isGroupItem: (item: DisplayItem) => item is GroupItem;
174
+ isExpandableItem: (item: DisplayItem) => item is ExpandableItem;
175
+ isRowItem: (item: DisplayItem) => item is RowItem;
176
+ isOddStripedRow: (item: DisplayItem) => boolean;
177
+ sortButtonLabel: (column: GridColumnDef) => string;
178
+ sortAriaSort: (column: GridColumnDef) => string;
179
+ sortDirection: (column: GridColumnDef) => string;
180
+ groupingButtonLabel: (column: GridColumnDef) => string;
181
+ filterValue: (columnName: string) => string;
182
+ filterPlaceholder: (column: GridColumnDef) => string;
183
+ isFilterInputDisabled: (column: GridColumnDef) => boolean;
184
+ groupDisclosureLabel: (item: GroupItem) => string;
185
+ displayValue: (row: GridRow, column: GridColumnDef) => string;
186
+ isFocusedCell: (row: GridRow, column: GridColumnDef) => boolean;
187
+ isEditingCell: (row: GridRow, column: GridColumnDef) => boolean;
188
+ editorInputType: (column: GridColumnDef) => string;
189
+ cellContext: (row: GridRow, column: GridColumnDef) => GridCellTemplateContext;
190
+ expandedContext: (row: GridRow) => GridExpandableTemplateContext & Record<string, unknown>;
191
+ columnWidth: (column: GridColumnDef) => string;
192
+ isColumnSortable: (column: GridColumnDef) => boolean;
193
+ isColumnFilterable: (column: GridColumnDef) => boolean;
194
+ cellIndent: (row: GridRow, column: GridColumnDef) => string;
195
+ treeToggleLabel: (row: GridRow) => string;
196
+ isTreeRowExpanded: (row: GridRow) => boolean;
197
+ expandToggleLabel: (row: GridRow) => string;
198
+ isGrouped: (column: GridColumnDef) => boolean;
199
+ showTreeToggle: (row: GridRow, column: GridColumnDef) => boolean;
200
+ showExpandToggle: (row: GridRow, column: GridColumnDef) => boolean;
201
+ showPaginationControls: () => boolean;
202
+ paginationSummary: () => string;
203
+ pageSizeOptions: () => number[];
204
+ isCellEditable: (row: GridRow, column: GridColumnDef, triggerEvent?: Event | KeyboardEvent | null) => boolean;
205
+ shouldEditOnFocus: (column: GridColumnDef) => boolean;
206
+
207
+ // Feature flags
208
+ sortingFeature: boolean;
209
+ filteringFeature: boolean;
210
+ groupingFeature: boolean;
211
+ paginationFeature: boolean;
212
+ cellEditFeature: boolean;
213
+ expandableFeature: boolean;
214
+ treeViewFeature: boolean;
215
+ infiniteScrollFeature: boolean;
216
+ columnMovingFeature: boolean;
217
+ csvExportFeature: boolean;
218
+
219
+ // Feature check helpers
220
+ isGroupingEnabled: () => boolean;
221
+ isFilteringEnabled: () => boolean;
222
+
223
+ // Actions
224
+ toggleSort: (column: GridColumnDef) => void;
225
+ updateFilter: (columnName: string, value: string) => void;
226
+ clearAllFilters: () => void;
227
+ toggleGrouping: (column: GridColumnDef, event?: React.MouseEvent) => void;
228
+ toggleGroup: (item: GroupItem) => void;
229
+ focusCell: (row: GridRow, column: GridColumnDef, triggerEvent?: Event | KeyboardEvent | null) => void;
230
+ handleCellKeyDown: (row: GridRow, column: GridColumnDef, event: React.KeyboardEvent) => void;
231
+ handleCellDoubleClick: (row: GridRow, column: GridColumnDef, event: React.MouseEvent) => void;
232
+ updateEditingValue: (value: string) => void;
233
+ handleEditorKeyDown: (event: React.KeyboardEvent) => void;
234
+ handleEditorBlur: (event: React.FocusEvent) => void;
235
+ toggleRowExpansion: (row: GridRow, event?: React.MouseEvent) => void;
236
+ toggleTreeRow: (row: GridRow, event?: React.MouseEvent) => void;
237
+ moveColumn: (fromIndex: number, toIndex: number) => void;
238
+ nextPage: () => void;
239
+ previousPage: () => void;
240
+ onPageSizeChange: (value: string) => void;
241
+ runBenchmark: (iterations?: number) => GridBenchmarkResult;
242
+ exportCsv: () => void;
243
+ onViewportScroll: (startIndex: number) => void;
244
+ }
245
+
246
+ export function useGridState(options: GridOptions, onRegisterApi?: (api: UiGridApi) => void): UseGridStateResult {
247
+ const [activeFilters, setActiveFilters] = useState<Record<string, string>>({});
248
+ const [groupByColumns, setGroupByColumns] = useState<string[]>([]);
249
+ const [collapsedGroups, setCollapsedGroups] = useState<Record<string, boolean>>({});
250
+ const [columnOrder, setColumnOrder] = useState<string[]>([]);
251
+ const [hiddenRowReasons, setHiddenRowReasons] = useState<Record<string, string[]>>({});
252
+ const [sortState, setSortState] = useState<SortState>({ columnName: null, direction: SORT_DIRECTIONS.none });
253
+ const [focusedCell, setFocusedCell] = useState<GridCellPosition | null>(null);
254
+ const [editingCell, setEditingCell] = useState<GridCellPosition | null>(null);
255
+ const [editingValue, setEditingValue] = useState('');
256
+ const [expandedRows, setExpandedRows] = useState<Record<string, boolean>>({});
257
+ const [expandedTreeRows, setExpandedTreeRows] = useState<Record<string, boolean>>({});
258
+ const [currentPage, setCurrentPage] = useState(1);
259
+ const [pageSize, setPageSize] = useState(0);
260
+ const [benchmarkResult, setBenchmarkResult] = useState<GridBenchmarkResult | null>(null);
261
+ const [infiniteScrollState, setInfiniteScrollState] = useState<GridInfiniteScrollState>({
262
+ scrollUp: false,
263
+ scrollDown: true,
264
+ dataLoading: false,
265
+ previousVisibleRows: 0,
266
+ });
267
+ const [autoViewportHeight, setAutoViewportHeight] = useState<number | null>(null);
268
+
269
+ const gridContainerRef = useRef<HTMLDivElement | null>(null);
270
+ const initializedGridIdRef = useRef<string | null>(null);
271
+ const lastCanvasHeightRef = useRef(0);
272
+ const lastGridHeightRef = useRef(0);
273
+ const lastGridWidthRef = useRef(0);
274
+ const scrollEndHandleRef = useRef<number | undefined>(undefined);
275
+ const scrollingRef = useRef(false);
276
+ const editorFocusTokenRef = useRef(0);
277
+ const renderedCellFocusTokenRef = useRef(0);
278
+
279
+ // Refs for current state (used inside gridApi bindings which are created once)
280
+ const activeFiltersRef = useRef(activeFilters);
281
+ activeFiltersRef.current = activeFilters;
282
+ const groupByColumnsRef = useRef(groupByColumns);
283
+ groupByColumnsRef.current = groupByColumns;
284
+ const collapsedGroupsRef = useRef(collapsedGroups);
285
+ collapsedGroupsRef.current = collapsedGroups;
286
+ const columnOrderRef = useRef(columnOrder);
287
+ columnOrderRef.current = columnOrder;
288
+ const hiddenRowReasonsRef = useRef(hiddenRowReasons);
289
+ hiddenRowReasonsRef.current = hiddenRowReasons;
290
+ const sortStateRef = useRef(sortState);
291
+ sortStateRef.current = sortState;
292
+ const focusedCellRef = useRef(focusedCell);
293
+ focusedCellRef.current = focusedCell;
294
+ const editingCellRef = useRef(editingCell);
295
+ editingCellRef.current = editingCell;
296
+ const editingValueRef = useRef(editingValue);
297
+ editingValueRef.current = editingValue;
298
+ const expandedRowsRef = useRef(expandedRows);
299
+ expandedRowsRef.current = expandedRows;
300
+ const expandedTreeRowsRef = useRef(expandedTreeRows);
301
+ expandedTreeRowsRef.current = expandedTreeRows;
302
+ const currentPageRef = useRef(currentPage);
303
+ currentPageRef.current = currentPage;
304
+ const pageSizeRef = useRef(pageSize);
305
+ pageSizeRef.current = pageSize;
306
+ const infiniteScrollStateRef = useRef(infiniteScrollState);
307
+ infiniteScrollStateRef.current = infiniteScrollState;
308
+ const optionsRef = useRef(options);
309
+ optionsRef.current = options;
310
+
311
+ const rowSize = options.rowHeight ?? 44;
312
+
313
+ const visibleColumns = useMemo(() => {
314
+ const order = columnOrder;
315
+ return [...options.columnDefs]
316
+ .filter((column) => column.visible !== false)
317
+ .sort((left, right) => order.indexOf(left.name) - order.indexOf(right.name));
318
+ }, [options.columnDefs, columnOrder]);
319
+
320
+ const visibleColumnsRef = useRef(visibleColumns);
321
+ visibleColumnsRef.current = visibleColumns;
322
+
323
+ const pipeline = useMemo<PipelineResult>(() => {
324
+ return buildGridPipeline({
325
+ options,
326
+ columns: visibleColumns,
327
+ activeFilters,
328
+ sortState,
329
+ groupByColumns,
330
+ collapsedGroups,
331
+ hiddenRowReasons,
332
+ expandedRows,
333
+ expandedTreeRows,
334
+ currentPage,
335
+ pageSize,
336
+ rowSize,
337
+ });
338
+ }, [options, visibleColumns, activeFilters, sortState, groupByColumns, collapsedGroups, hiddenRowReasons, expandedRows, expandedTreeRows, currentPage, pageSize, rowSize]);
339
+
340
+ const pipelineRef = useRef(pipeline);
341
+ pipelineRef.current = pipeline;
342
+
343
+ const labels = useMemo(() => resolveGridLabels(options.labels), [options.labels]);
344
+
345
+ const gridTemplateColumns = useMemo(
346
+ () => visibleColumns.map((column) => gridColumnWidth(column)).join(' '),
347
+ [visibleColumns]
348
+ );
349
+
350
+ // --- Helper functions (all pure, no state closures needed beyond refs) ---
351
+
352
+ const resolveRowId = useCallback((row: GridRow | GridRecord | string): string => {
353
+ return coreResolveGridRowId(optionsRef.current, row);
354
+ }, []);
355
+
356
+ const buildRowsFromData = useCallback((data: readonly GridRecord[]): GridRow[] => {
357
+ return buildGridRows(
358
+ { ...optionsRef.current, data },
359
+ optionsRef.current.rowHeight ?? 44,
360
+ hiddenRowReasonsRef.current,
361
+ expandedRowsRef.current
362
+ );
363
+ }, []);
364
+
365
+ const findRowById = useCallback((rowId: string): GridRow | null => {
366
+ return coreFindGridRowById(buildRowsFromData(optionsRef.current.data), rowId);
367
+ }, [buildRowsFromData]);
368
+
369
+ const canExpandRowsFn = useCallback((): boolean => {
370
+ return FEATURE_EXPANDABLE && canGridExpandRows(optionsRef.current);
371
+ }, []);
372
+
373
+ const effectivePageSizeFn = useCallback((totalItems: number): number => {
374
+ return coreGetEffectivePageSize(optionsRef.current, pageSizeRef.current, totalItems);
375
+ }, []);
376
+
377
+ const getCurrentPageValueFn = useCallback((totalItems?: number): number => {
378
+ const ti = totalItems ?? pipelineRef.current.totalItems;
379
+ return coreGetCurrentPageValue(optionsRef.current, currentPageRef.current, ti, pageSizeRef.current);
380
+ }, []);
381
+
382
+ const getTotalPagesValueFn = useCallback((totalItems?: number): number => {
383
+ const ti = totalItems ?? pipelineRef.current.totalItems;
384
+ return coreGetTotalPagesValue(optionsRef.current, ti, pageSizeRef.current);
385
+ }, []);
386
+
387
+ const getFirstRowIndexValueFn = useCallback((totalItems?: number): number => {
388
+ const ti = totalItems ?? pipelineRef.current.totalItems;
389
+ return coreGetFirstRowIndexValue(optionsRef.current, currentPageRef.current, ti, pageSizeRef.current);
390
+ }, []);
391
+
392
+ const getLastRowIndexValueFn = useCallback((totalItems?: number): number => {
393
+ const ti = totalItems ?? pipelineRef.current.totalItems;
394
+ return coreGetLastRowIndexValue(optionsRef.current, currentPageRef.current, ti, pageSizeRef.current);
395
+ }, []);
396
+
397
+ const isCellEditable = useCallback((row: GridRow, column: GridColumnDef, triggerEvent?: Event | KeyboardEvent | null): boolean => {
398
+ if (!FEATURE_CELL_EDIT) return false;
399
+ const editable = column.enableCellEdit ?? optionsRef.current.enableCellEdit ?? false;
400
+ if (!editable) return false;
401
+
402
+ const condition = column.cellEditableCondition ?? optionsRef.current.cellEditableCondition ?? true;
403
+ if (typeof condition === 'boolean') return condition;
404
+
405
+ const context: GridCellEditableContext = {
406
+ row: row.entity,
407
+ column,
408
+ rowIndex: row.index,
409
+ triggerEvent,
410
+ };
411
+ return condition(context);
412
+ }, []);
413
+
414
+ const shouldEditOnFocusFn = useCallback((column: GridColumnDef): boolean => {
415
+ return column.enableCellEditOnFocus ?? optionsRef.current.enableCellEditOnFocus ?? false;
416
+ }, []);
417
+
418
+ // --- Focus helpers ---
419
+
420
+ const focusRenderedCell = useCallback((position: GridCellPosition): void => {
421
+ const focusToken = ++renderedCellFocusTokenRef.current;
422
+ const selector = `.body-cell[data-row-id="${position.rowId}"][data-col-name="${position.columnName}"]`;
423
+
424
+ const doFocus = (retry = true): void => {
425
+ if (focusToken !== renderedCellFocusTokenRef.current) return;
426
+ const container = gridContainerRef.current;
427
+ if (!container) return;
428
+ const target = container.querySelector(selector) as HTMLElement | null;
429
+ if (!target) {
430
+ if (retry) requestAnimationFrame(() => doFocus(false));
431
+ return;
432
+ }
433
+ target.focus();
434
+ if (retry && container.ownerDocument.activeElement !== target) {
435
+ requestAnimationFrame(() => doFocus(false));
436
+ }
437
+ };
438
+
439
+ queueMicrotask(() => doFocus(true));
440
+ }, []);
441
+
442
+ const focusEditorInput = useCallback((focusToken: number): void => {
443
+ if (focusToken !== editorFocusTokenRef.current) return;
444
+ const ec = editingCellRef.current;
445
+ if (!ec) return;
446
+
447
+ const selector = `.cell-editor[data-row-id="${ec.rowId}"][data-col-name="${ec.columnName}"]`;
448
+
449
+ const doFocus = (retry = true): void => {
450
+ if (focusToken !== editorFocusTokenRef.current) return;
451
+ const currentEc = editingCellRef.current;
452
+ if (!currentEc || currentEc.rowId !== ec.rowId || currentEc.columnName !== ec.columnName) return;
453
+
454
+ const container = gridContainerRef.current;
455
+ if (!container) return;
456
+ const input = container.querySelector(selector) as HTMLInputElement | null;
457
+ if (!input) {
458
+ if (retry) requestAnimationFrame(() => doFocus(false));
459
+ return;
460
+ }
461
+ input.focus();
462
+ input.select();
463
+ };
464
+
465
+ doFocus(true);
466
+ }, []);
467
+
468
+ // --- Create grid API once ---
469
+
470
+ const gridApiRef = useRef<UiGridApi | null>(null);
471
+ if (!gridApiRef.current) {
472
+ const bindings: GridApiBindings = {
473
+ refresh: () => setActiveFilters((current) => ({ ...current })),
474
+ getVisibleRows: () => pipelineRef.current.visibleRows,
475
+ setRowInvisible: (row, reason = 'user') => {
476
+ const rowId = coreResolveGridRowId(optionsRef.current, row);
477
+ setHiddenRowReasons((current) => addGridRowInvisibleReason(current, rowId, reason));
478
+ },
479
+ clearRowInvisible: (row, reason = 'user') => {
480
+ const rowId = coreResolveGridRowId(optionsRef.current, row);
481
+ setHiddenRowReasons((current) => clearGridRowInvisibleReason(current, rowId, reason));
482
+ },
483
+ setFilter: (columnName, value) => {
484
+ setActiveFilters((current) => {
485
+ const next = { ...current, [columnName]: value };
486
+ activeFiltersRef.current = next;
487
+ queueMicrotask(() => gridApiRef.current!.core.raise.filterChanged(next));
488
+ return next;
489
+ });
490
+ },
491
+ clearAllFilters: () => {
492
+ const nextFilters: Record<string, string> = {};
493
+ activeFiltersRef.current = nextFilters;
494
+ setActiveFilters(nextFilters);
495
+ queueMicrotask(() => gridApiRef.current!.core.raise.filterChanged(nextFilters));
496
+ },
497
+ sortColumn: (columnName, direction) => {
498
+ sortGridColumnCommand(gridApiRef.current!, (s) => setSortState(s), columnName, direction);
499
+ },
500
+ moveColumn: (fromIndex, toIndex) => {
501
+ moveGridColumnCommand(
502
+ gridApiRef.current!,
503
+ FEATURE_COLUMN_MOVING && (optionsRef.current.enableColumnMoving === true),
504
+ (updater) => setColumnOrder((current) => updater(current)),
505
+ fromIndex,
506
+ toIndex
507
+ );
508
+ },
509
+ toggleGrouping: (columnName) => {
510
+ if (!(FEATURE_GROUPING && isGridGroupingEnabled(optionsRef.current))) return;
511
+ const current = groupByColumnsRef.current;
512
+ const next = current.includes(columnName)
513
+ ? current.filter((n) => n !== columnName)
514
+ : [...current, columnName];
515
+ groupByColumnsRef.current = next;
516
+ setGroupByColumns(next);
517
+ gridApiRef.current!.core.raise.groupingChanged(next);
518
+ },
519
+ clearGrouping: () => {
520
+ clearGridGroupingCommand(gridApiRef.current!, (grouping) => setGroupByColumns(grouping), false);
521
+ },
522
+ benchmark: (iterations) => {
523
+ return runBenchmarkFn(iterations);
524
+ },
525
+ exportCsv: () => {
526
+ exportCsvFn();
527
+ },
528
+ paginationGetPage: () => getCurrentPageValueFn(),
529
+ paginationGetTotalPages: () => getTotalPagesValueFn(),
530
+ paginationGetFirstRowIndex: () => getFirstRowIndexValueFn(),
531
+ paginationGetLastRowIndex: () => getLastRowIndexValueFn(),
532
+ paginationNextPage: () => seekPageFn(getCurrentPageValueFn() + 1),
533
+ paginationPreviousPage: () => seekPageFn(getCurrentPageValueFn() - 1),
534
+ paginationSeek: (page) => seekPageFn(page),
535
+ paginationSetPageSize: (ps) => setPaginationPageSizeFn(ps),
536
+ toggleRowExpansion: (row) => toggleRowExpansionByRefFn(row),
537
+ expandAllRows: () => expandAllRowsFn(),
538
+ collapseAllRows: () => {
539
+ collapseAllGridRowsCommand((e) => setExpandedRows(e));
540
+ },
541
+ toggleAllRows: () => toggleAllRowsFn(),
542
+ treeExpandAllRows: () => {
543
+ expandAllGridTreeRowsCommand(
544
+ (data) => buildRowsFromData(data),
545
+ optionsRef.current.data,
546
+ (e) => setExpandedTreeRows(e)
547
+ );
548
+ },
549
+ treeCollapseAllRows: () => {
550
+ collapseAllGridTreeRowsCommand((e) => setExpandedTreeRows(e));
551
+ },
552
+ treeToggleRow: (row) => toggleTreeRowByRefFn(row),
553
+ treeExpandRow: (row) => expandTreeRowByRefFn(row),
554
+ treeCollapseRow: (row) => collapseTreeRowByRefFn(row),
555
+ treeGetRowChildren: (row) => {
556
+ const rowId = coreResolveGridRowId(optionsRef.current, row);
557
+ return buildRowsFromData(optionsRef.current.data).filter((r) => r.parentId === rowId);
558
+ },
559
+ treeGetState: () => expandedTreeRowsRef.current,
560
+ treeSetState: (state) => setExpandedTreeRows({ ...state }),
561
+ infiniteScrollDataLoaded: (scrollUp, scrollDown) => {
562
+ return completeGridInfiniteScrollDataLoadCommand(
563
+ infiniteScrollStateRef.current,
564
+ (s) => setInfiniteScrollState(s),
565
+ scrollUp ?? infiniteScrollStateRef.current.scrollUp,
566
+ scrollDown ?? infiniteScrollStateRef.current.scrollDown
567
+ );
568
+ },
569
+ infiniteScrollReset: (scrollUp, scrollDown) => {
570
+ resetGridInfiniteScrollCommand(
571
+ (s) => setInfiniteScrollState(s),
572
+ scrollUp ?? infiniteScrollStateRef.current.scrollUp,
573
+ scrollDown ?? infiniteScrollStateRef.current.scrollDown
574
+ );
575
+ },
576
+ infiniteScrollSaveScrollPercentage: () => {
577
+ saveGridInfiniteScrollPercentageCommand(
578
+ infiniteScrollStateRef.current,
579
+ pipelineRef.current.visibleRows.length,
580
+ (s) => setInfiniteScrollState(s)
581
+ );
582
+ },
583
+ infiniteScrollDataRemovedTop: (scrollUp, scrollDown) => {
584
+ resetGridInfiniteScrollCommand(
585
+ (s) => setInfiniteScrollState(s),
586
+ scrollUp ?? infiniteScrollStateRef.current.scrollUp,
587
+ scrollDown ?? infiniteScrollStateRef.current.scrollDown
588
+ );
589
+ },
590
+ infiniteScrollDataRemovedBottom: (scrollUp, scrollDown) => {
591
+ resetGridInfiniteScrollCommand(
592
+ (s) => setInfiniteScrollState(s),
593
+ scrollUp ?? infiniteScrollStateRef.current.scrollUp,
594
+ scrollDown ?? infiniteScrollStateRef.current.scrollDown
595
+ );
596
+ },
597
+ infiniteScrollSetDirections: (scrollUp, scrollDown) => {
598
+ setGridInfiniteScrollDirectionsCommand(
599
+ infiniteScrollStateRef.current,
600
+ (s) => setInfiniteScrollState(s),
601
+ scrollUp,
602
+ scrollDown
603
+ );
604
+ },
605
+ saveState: () => {
606
+ return buildGridSavedState({
607
+ columnOrder: columnOrderRef.current,
608
+ activeFilters: activeFiltersRef.current,
609
+ sortState: sortStateRef.current,
610
+ groupByColumns: groupByColumnsRef.current,
611
+ currentPage: currentPageRef.current,
612
+ pageSize: pageSizeRef.current,
613
+ totalItems: pipelineRef.current.totalItems,
614
+ expandedRows: expandedRowsRef.current,
615
+ expandedTreeRows: expandedTreeRowsRef.current,
616
+ });
617
+ },
618
+ restoreState: (state) => {
619
+ restoreGridStateCommand(gridApiRef.current!, state, {
620
+ setColumnOrder: (order) => setColumnOrder(order),
621
+ setActiveFilters: (filters) => setActiveFilters(filters),
622
+ setSortState: (s) => setSortState(s),
623
+ setGroupByColumns: (grouping) => setGroupByColumns(grouping),
624
+ setCurrentPage: (page) => setCurrentPage(page),
625
+ setPageSize: (ps) => setPageSize(ps),
626
+ setExpandedRows: (e) => setExpandedRows(e),
627
+ setExpandedTreeRows: (e) => setExpandedTreeRows(e),
628
+ getEffectivePageSize: () => effectivePageSizeFn(pipelineRef.current.totalItems),
629
+ });
630
+ },
631
+ beginCellEdit: (row, columnName, triggerEvent) => {
632
+ const rowId = coreResolveGridRowId(optionsRef.current, row);
633
+ const gridRow = coreFindGridRowById(buildRowsFromData(optionsRef.current.data), rowId);
634
+ const column = visibleColumnsRef.current.find((c) => c.name === columnName);
635
+ if (!gridRow || !column || !isCellEditable(gridRow, column, triggerEvent)) return;
636
+ startCellEditFn(gridRow, column, triggerEvent);
637
+ },
638
+ endCellEdit: () => commitCellEditFn(),
639
+ cancelCellEdit: () => cancelCellEditFn(),
640
+ getEditingCell: () => editingCellRef.current,
641
+ };
642
+
643
+ gridApiRef.current = createGridApi(bindings);
644
+ }
645
+
646
+ const gridApi = gridApiRef.current!;
647
+
648
+ // --- Memoized action functions ---
649
+
650
+ const seekPageFn = useCallback((page: number): void => {
651
+ seekGridPaginationCommand(
652
+ gridApiRef.current!,
653
+ (nextPage) => setCurrentPage(nextPage),
654
+ () => getTotalPagesValueFn(),
655
+ () => effectivePageSizeFn(pipelineRef.current.totalItems),
656
+ page
657
+ );
658
+ }, [getTotalPagesValueFn, effectivePageSizeFn]);
659
+
660
+ const setPaginationPageSizeFn = useCallback((ps: number): void => {
661
+ setGridPaginationPageSizeCommand(
662
+ gridApiRef.current!,
663
+ (nextPageSize) => setPageSize(nextPageSize),
664
+ (nextPage) => setCurrentPage(nextPage),
665
+ ps
666
+ );
667
+ }, []);
668
+
669
+ const toggleRowExpansionByRefFn = useCallback((row: GridRow | GridRecord | string): void => {
670
+ const rowId = coreResolveGridRowId(optionsRef.current, row);
671
+ toggleGridRowExpansionCommand(
672
+ gridApiRef.current!,
673
+ FEATURE_EXPANDABLE && canGridExpandRows(optionsRef.current),
674
+ expandedRowsRef.current,
675
+ rowId,
676
+ (e) => setExpandedRows(e),
677
+ (resolvedRowId) => coreFindGridRowById(buildRowsFromData(optionsRef.current.data), resolvedRowId)
678
+ );
679
+ }, [buildRowsFromData]);
680
+
681
+ const expandAllRowsFn = useCallback((): void => {
682
+ if (!canGridExpandRows(optionsRef.current)) return;
683
+ expandAllGridRowsCommand(
684
+ (data) => buildRowsFromData(data),
685
+ optionsRef.current.data,
686
+ (e) => setExpandedRows(e)
687
+ );
688
+ }, [buildRowsFromData]);
689
+
690
+ const toggleAllRowsFn = useCallback((): void => {
691
+ const allExpanded = areAllGridRowsExpanded(buildRowsFromData(optionsRef.current.data), expandedRowsRef.current);
692
+ if (allExpanded) {
693
+ collapseAllGridRowsCommand((e) => setExpandedRows(e));
694
+ } else {
695
+ expandAllRowsFn();
696
+ }
697
+ }, [buildRowsFromData, expandAllRowsFn]);
698
+
699
+ const toggleTreeRowByRefFn = useCallback((row: GridRow | GridRecord | string): void => {
700
+ const rowId = coreResolveGridRowId(optionsRef.current, row);
701
+ toggleGridTreeRowCommand(
702
+ gridApiRef.current!,
703
+ expandedTreeRowsRef.current,
704
+ rowId,
705
+ (e) => setExpandedTreeRows(e),
706
+ (resolvedRowId) => coreFindGridRowById(buildRowsFromData(optionsRef.current.data), resolvedRowId)
707
+ );
708
+ }, [buildRowsFromData]);
709
+
710
+ const expandTreeRowByRefFn = useCallback((row: GridRow | GridRecord | string): void => {
711
+ const rowId = coreResolveGridRowId(optionsRef.current, row);
712
+ setGridTreeRowExpandedCommand(
713
+ gridApiRef.current!,
714
+ expandedTreeRowsRef.current,
715
+ rowId,
716
+ true,
717
+ (e) => setExpandedTreeRows(e),
718
+ (resolvedRowId) => coreFindGridRowById(buildRowsFromData(optionsRef.current.data), resolvedRowId)
719
+ );
720
+ }, [buildRowsFromData]);
721
+
722
+ const collapseTreeRowByRefFn = useCallback((row: GridRow | GridRecord | string): void => {
723
+ const rowId = coreResolveGridRowId(optionsRef.current, row);
724
+ setGridTreeRowExpandedCommand(
725
+ gridApiRef.current!,
726
+ expandedTreeRowsRef.current,
727
+ rowId,
728
+ false,
729
+ (e) => setExpandedTreeRows(e),
730
+ (resolvedRowId) => coreFindGridRowById(buildRowsFromData(optionsRef.current.data), resolvedRowId)
731
+ );
732
+ }, [buildRowsFromData]);
733
+
734
+ const startCellEditFn = useCallback((row: GridRow, column: GridColumnDef, triggerEvent?: Event | KeyboardEvent | null, initialValue?: string): void => {
735
+ const currentValue = getCellValue(row.entity, column);
736
+ const focusToken = ++editorFocusTokenRef.current;
737
+ const ec = beginGridCellEditCommand(
738
+ gridApiRef.current!,
739
+ {
740
+ setFocusedCell: (fc) => setFocusedCell(fc),
741
+ setEditingCell: (ec2) => setEditingCell(ec2),
742
+ setEditingValue: (ev) => setEditingValue(ev),
743
+ },
744
+ row,
745
+ column,
746
+ currentValue,
747
+ triggerEvent,
748
+ initialValue
749
+ );
750
+
751
+ if (ec) {
752
+ queueMicrotask(() => focusEditorInput(focusToken));
753
+ }
754
+ }, [focusEditorInput]);
755
+
756
+ const commitCellEditFn = useCallback((direction?: GridMoveDirection, restoreFocus = true): void => {
757
+ const result = commitGridCellEditCommand(gridApiRef.current!, {
758
+ getEditingCell: () => editingCellRef.current,
759
+ getEditingValue: () => editingValueRef.current,
760
+ setEditingCell: (ec) => setEditingCell(ec),
761
+ setEditingValue: (ev) => setEditingValue(ev),
762
+ findRowById: (rowId) => coreFindGridRowById(buildRowsFromData(optionsRef.current.data), rowId),
763
+ findColumnByName: (columnName) => visibleColumnsRef.current.find((c) => c.name === columnName),
764
+ parseEditedValue: (column, value, oldValue) => parseGridEditedValue(column, value, oldValue),
765
+ setCellValue: (rowEntity, column, value) => {
766
+ const fieldPath = column.editModelField ?? column.field ?? column.name;
767
+ setPathValue(rowEntity, fieldPath, value);
768
+ },
769
+ });
770
+
771
+ if (!result.committed || !result.row || !result.column || !result.focusTarget) return;
772
+
773
+ editorFocusTokenRef.current += 1;
774
+
775
+ if (direction) {
776
+ const moved = moveFocusFn(result.row, result.column, direction);
777
+ if (!moved) focusRenderedCell(result.focusTarget);
778
+ } else if (restoreFocus) {
779
+ focusRenderedCell(result.focusTarget);
780
+ }
781
+ }, [buildRowsFromData, focusRenderedCell]);
782
+
783
+ const cancelCellEditFn = useCallback((): void => {
784
+ const hadEditingCell = editingCellRef.current !== null;
785
+ const result = cancelGridCellEditCommand(gridApiRef.current!, {
786
+ getEditingCell: () => editingCellRef.current,
787
+ setEditingCell: (ec) => setEditingCell(ec),
788
+ setEditingValue: (ev) => setEditingValue(ev),
789
+ findRowById: (rowId) => coreFindGridRowById(buildRowsFromData(optionsRef.current.data), rowId),
790
+ findColumnByName: (columnName) => visibleColumnsRef.current.find((c) => c.name === columnName),
791
+ });
792
+
793
+ if (!hadEditingCell) return;
794
+ editorFocusTokenRef.current += 1;
795
+ if (result.focusTarget) focusRenderedCell(result.focusTarget);
796
+ }, [buildRowsFromData, focusRenderedCell]);
797
+
798
+ const moveFocusFn = useCallback((row: GridRow, column: GridColumnDef, direction: GridMoveDirection, triggerEvent?: Event | KeyboardEvent | null): boolean => {
799
+ const nextCell = findNextGridCell({
800
+ rows: pipelineRef.current.visibleRows,
801
+ columns: visibleColumnsRef.current,
802
+ rowId: row.id,
803
+ columnName: column.name,
804
+ direction,
805
+ });
806
+ if (!nextCell) return false;
807
+
808
+ setFocusedCell({ rowId: nextCell.row.id, columnName: nextCell.column.name });
809
+ focusRenderedCell({ rowId: nextCell.row.id, columnName: nextCell.column.name });
810
+
811
+ if (shouldEditOnFocusFn(nextCell.column) && isCellEditable(nextCell.row, nextCell.column, triggerEvent)) {
812
+ startCellEditFn(nextCell.row, nextCell.column, triggerEvent);
813
+ }
814
+
815
+ return true;
816
+ }, [focusRenderedCell, isCellEditable, shouldEditOnFocusFn, startCellEditFn]);
817
+
818
+ const runBenchmarkFn = useCallback((iterations?: number): GridBenchmarkResult => {
819
+ const safeIterations = Math.max(1, iterations ?? optionsRef.current.benchmark?.iterations ?? 25);
820
+ const startedAt = performance.now();
821
+ let lastResult = buildGridPipeline({
822
+ options: optionsRef.current,
823
+ columns: visibleColumnsRef.current,
824
+ activeFilters: activeFiltersRef.current,
825
+ sortState: sortStateRef.current,
826
+ groupByColumns: groupByColumnsRef.current,
827
+ collapsedGroups: collapsedGroupsRef.current,
828
+ hiddenRowReasons: hiddenRowReasonsRef.current,
829
+ expandedRows: expandedRowsRef.current,
830
+ expandedTreeRows: expandedTreeRowsRef.current,
831
+ currentPage: currentPageRef.current,
832
+ pageSize: pageSizeRef.current,
833
+ rowSize: optionsRef.current.rowHeight ?? 44,
834
+ });
835
+
836
+ for (let i = 1; i < safeIterations; i++) {
837
+ lastResult = buildGridPipeline({
838
+ options: optionsRef.current,
839
+ columns: visibleColumnsRef.current,
840
+ activeFilters: activeFiltersRef.current,
841
+ sortState: sortStateRef.current,
842
+ groupByColumns: groupByColumnsRef.current,
843
+ collapsedGroups: collapsedGroupsRef.current,
844
+ hiddenRowReasons: hiddenRowReasonsRef.current,
845
+ expandedRows: expandedRowsRef.current,
846
+ expandedTreeRows: expandedTreeRowsRef.current,
847
+ currentPage: currentPageRef.current,
848
+ pageSize: pageSizeRef.current,
849
+ rowSize: optionsRef.current.rowHeight ?? 44,
850
+ });
851
+ }
852
+
853
+ const totalMs = performance.now() - startedAt;
854
+ const result: GridBenchmarkResult = {
855
+ iterations: safeIterations,
856
+ totalMs,
857
+ averageMs: totalMs / safeIterations,
858
+ visibleRows: lastResult.visibleRows.length,
859
+ renderedItems: lastResult.displayItems.length,
860
+ };
861
+
862
+ setBenchmarkResult(result);
863
+ raiseGridBenchmarkComplete(gridApiRef.current!, result);
864
+ return result;
865
+ }, []);
866
+
867
+ const exportCsvFn = useCallback((): void => {
868
+ if (!FEATURE_CSV_EXPORT) return;
869
+ const columns = visibleColumnsRef.current;
870
+ const csv = exportCsvRows(columns, pipelineRef.current.visibleRows);
871
+ downloadGridCsvFile(csv, `${sanitizeDownloadFilename(optionsRef.current.id)}.csv`);
872
+ }, []);
873
+
874
+ // --- Initialization effect: reset state when options.id changes ---
875
+
876
+ useEffect(() => {
877
+ if (initializedGridIdRef.current === options.id) return;
878
+ initializedGridIdRef.current = options.id;
879
+
880
+ setActiveFilters({});
881
+ setHiddenRowReasons({});
882
+ setCollapsedGroups({});
883
+ setFocusedCell(null);
884
+ setEditingCell(null);
885
+ setEditingValue('');
886
+ setExpandedRows({});
887
+ setExpandedTreeRows({});
888
+ setColumnOrder(options.columnDefs.map((column) => column.name));
889
+ setGroupByColumns(options.grouping?.groupBy ?? []);
890
+ setCurrentPage(options.paginationCurrentPage ?? 1);
891
+ setPageSize(coreGetEffectivePageSize(options, 0, options.data.length));
892
+
893
+ setInfiniteScrollState({
894
+ scrollUp: options.infiniteScrollUp === true,
895
+ scrollDown: options.infiniteScrollDown !== false,
896
+ dataLoading: false,
897
+ previousVisibleRows: 0,
898
+ });
899
+
900
+ const initialSort = options.columnDefs.find(
901
+ (column) => column.sort?.direction && !column.sort.ignoreSort
902
+ );
903
+ setSortState({
904
+ columnName: initialSort?.name ?? null,
905
+ direction: initialSort?.sort?.direction ?? SORT_DIRECTIONS.none,
906
+ });
907
+
908
+ onRegisterApi?.(gridApi);
909
+ raiseGridRenderingComplete(gridApi);
910
+ }, [options.id]); // eslint-disable-line react-hooks/exhaustive-deps
911
+
912
+ // --- Pipeline side-effects ---
913
+
914
+ useEffect(() => {
915
+ raiseGridRowsRendered(gridApi, pipeline.visibleRows);
916
+ raiseGridRowsVisibleChanged(gridApi, pipeline.visibleRows);
917
+
918
+ const newHeight = pipeline.displayItems.length * rowSize;
919
+ if (newHeight !== lastCanvasHeightRef.current) {
920
+ raiseGridCanvasHeightChanged(gridApi, lastCanvasHeightRef.current, newHeight);
921
+ lastCanvasHeightRef.current = newHeight;
922
+ }
923
+ }, [pipeline, gridApi, rowSize]);
924
+
925
+ // --- Auto resize effect ---
926
+
927
+ useEffect(() => {
928
+ if (!FEATURE_AUTO_RESIZE || !options.enableAutoResize) return;
929
+
930
+ const container = gridContainerRef.current;
931
+ if (!container) return;
932
+
933
+ const observer = observeGridHostSize(container, ({ height: nextHeight, width: nextWidth }) => {
934
+ if (nextHeight === lastGridHeightRef.current && nextWidth === lastGridWidthRef.current) return;
935
+
936
+ raiseGridDimensionChanged(gridApi, lastGridHeightRef.current, lastGridWidthRef.current, nextHeight, nextWidth);
937
+ lastGridHeightRef.current = nextHeight;
938
+ lastGridWidthRef.current = nextWidth;
939
+
940
+ if (!options.viewportHeight && nextHeight > 0) {
941
+ setAutoViewportHeight(nextHeight);
942
+ }
943
+ });
944
+
945
+ if (!observer) return;
946
+ return () => observer.disconnect();
947
+ }, [options.enableAutoResize, options.viewportHeight, gridApi]);
948
+
949
+ // --- Computed values ---
950
+
951
+ const totalRows = pipeline.totalItems;
952
+ const visibleRowCount = pipeline.visibleRows.length;
953
+ const displayItems = pipeline.displayItems;
954
+ const virtualizationEnabled = pipeline.virtualizationEnabled;
955
+ const pipelineMsVal = pipeline.pipelineMs;
956
+ const paginationCurrentPage = getCurrentPageValueFn();
957
+ const paginationTotalPages = getTotalPagesValueFn();
958
+ const paginationSelectedPageSize = effectivePageSizeFn(pipeline.totalItems);
959
+ const viewportHeightPx = `${options.viewportHeight ?? autoViewportHeight ?? 560}px`;
960
+
961
+ // --- Display helper functions ---
962
+
963
+ const headerLabelFn = useCallback((column: GridColumnDef): string => coreHeaderLabel(column), []);
964
+ const isGroupItemFn = useCallback((item: DisplayItem): item is GroupItem => item.kind === 'group', []);
965
+ const isExpandableItemFn = useCallback((item: DisplayItem): item is ExpandableItem => item.kind === 'expandable', []);
966
+ const isRowItemFn = useCallback((item: DisplayItem): item is RowItem => item.kind === 'row', []);
967
+ const isOddStripedRowFn = useCallback((item: DisplayItem): boolean => item.kind === 'row' && item.visibleIndex % 2 === 0, []);
968
+
969
+ const sortDirectionFn = useCallback((column: GridColumnDef): string => {
970
+ return sortStateRef.current.columnName === column.name ? sortStateRef.current.direction : SORT_DIRECTIONS.none;
971
+ }, []);
972
+
973
+ const sortButtonLabelFn = useCallback((column: GridColumnDef): string => {
974
+ return gridSortButtonLabel(sortDirectionFn(column) as any, labels);
975
+ }, [labels, sortDirectionFn]);
976
+
977
+ const sortAriaSortFn = useCallback((column: GridColumnDef): string => {
978
+ return gridSortAriaSort(sortDirectionFn(column) as any);
979
+ }, [sortDirectionFn]);
980
+
981
+ const groupingButtonLabelFn = useCallback((column: GridColumnDef): string => {
982
+ return gridGroupingButtonLabel(isGridColumnGrouped(groupByColumnsRef.current, column), labels);
983
+ }, [labels]);
984
+
985
+ const filterValueFn = useCallback((columnName: string): string => {
986
+ return activeFiltersRef.current[columnName] ?? '';
987
+ }, []);
988
+
989
+ const filterPlaceholderFn = useCallback((column: GridColumnDef): string => {
990
+ return gridFilterPlaceholder(isGridColumnFilterable(optionsRef.current, column), labels);
991
+ }, [labels]);
992
+
993
+ const isFilterInputDisabledFn = useCallback((column: GridColumnDef): boolean => {
994
+ return !isGridColumnFilterable(optionsRef.current, column);
995
+ }, []);
996
+
997
+ const groupDisclosureLabelFn = useCallback((item: GroupItem): string => {
998
+ return gridGroupDisclosureLabel(item.collapsed, labels);
999
+ }, [labels]);
1000
+
1001
+ const cellContextFn = useCallback((row: GridRow, column: GridColumnDef): GridCellTemplateContext => {
1002
+ return buildGridCellContext(row, column);
1003
+ }, []);
1004
+
1005
+ const displayValueFn = useCallback((row: GridRow, column: GridColumnDef): string => {
1006
+ return formatGridCellDisplayValue(cellContextFn(row, column));
1007
+ }, [cellContextFn]);
1008
+
1009
+ const isFocusedCellFn = useCallback((row: GridRow, column: GridColumnDef): boolean => {
1010
+ return isGridCellPosition(focusedCellRef.current, row.id, column.name);
1011
+ }, []);
1012
+
1013
+ const isEditingCellFn = useCallback((row: GridRow, column: GridColumnDef): boolean => {
1014
+ return isGridCellPosition(editingCellRef.current, row.id, column.name);
1015
+ }, []);
1016
+
1017
+ const editorInputTypeFn = useCallback((column: GridColumnDef): string => {
1018
+ return gridEditorInputType(column);
1019
+ }, []);
1020
+
1021
+ const expandedContextFn = useCallback((row: GridRow): GridExpandableTemplateContext & Record<string, unknown> => {
1022
+ return {
1023
+ $implicit: row.entity,
1024
+ row: row.entity,
1025
+ rowIndex: row.index,
1026
+ expanded: true,
1027
+ ...(optionsRef.current.expandableRowScope ?? {}),
1028
+ };
1029
+ }, []);
1030
+
1031
+ const columnWidthFn = useCallback((column: GridColumnDef): string => gridColumnWidth(column), []);
1032
+
1033
+ const isColumnSortableFn = useCallback((column: GridColumnDef): boolean => {
1034
+ return isGridColumnSortable(optionsRef.current, column);
1035
+ }, []);
1036
+
1037
+ const isColumnFilterableFn = useCallback((column: GridColumnDef): boolean => {
1038
+ return isGridColumnFilterable(optionsRef.current, column);
1039
+ }, []);
1040
+
1041
+ const cellIndentFn = useCallback((row: GridRow, column: GridColumnDef): string => {
1042
+ return gridCellIndent(optionsRef.current, visibleColumnsRef.current, row, column);
1043
+ }, []);
1044
+
1045
+ const treeToggleLabelFn = useCallback((row: GridRow): string => {
1046
+ return gridTreeToggleLabelForRow(expandedTreeRowsRef.current, row, labels);
1047
+ }, [labels]);
1048
+
1049
+ const isTreeRowExpandedFn = useCallback((row: GridRow): boolean => {
1050
+ return isGridTreeRowExpanded(expandedTreeRowsRef.current, row);
1051
+ }, []);
1052
+
1053
+ const expandToggleLabelFn = useCallback((row: GridRow): string => {
1054
+ return gridExpandToggleLabelForRow(row, labels);
1055
+ }, [labels]);
1056
+
1057
+ const isGroupedFn = useCallback((column: GridColumnDef): boolean => {
1058
+ return isGridColumnGrouped(groupByColumnsRef.current, column);
1059
+ }, []);
1060
+
1061
+ const showTreeToggleFn = useCallback((row: GridRow, column: GridColumnDef): boolean => {
1062
+ return shouldShowGridTreeToggle(optionsRef.current, visibleColumnsRef.current, row, column);
1063
+ }, []);
1064
+
1065
+ const showExpandToggleFn = useCallback((row: GridRow, column: GridColumnDef): boolean => {
1066
+ return shouldShowGridExpandToggle(optionsRef.current, visibleColumnsRef.current, column);
1067
+ }, []);
1068
+
1069
+ const showPaginationControlsFn = useCallback((): boolean => {
1070
+ return FEATURE_PAGINATION && shouldShowGridPaginationControls(optionsRef.current);
1071
+ }, []);
1072
+
1073
+ const paginationSummaryFn = useCallback((): string => {
1074
+ const ti = pipelineRef.current.totalItems;
1075
+ if (ti === 0) return '0-0 of 0';
1076
+ return `${getFirstRowIndexValueFn(ti) + 1}-${getLastRowIndexValueFn(ti) + 1} of ${ti}`;
1077
+ }, [getFirstRowIndexValueFn, getLastRowIndexValueFn]);
1078
+
1079
+ const pageSizeOptionsFn = useCallback((): number[] => {
1080
+ return optionsRef.current.paginationPageSizes ?? [];
1081
+ }, []);
1082
+
1083
+ const isGroupingEnabledFn = useCallback((): boolean => {
1084
+ return FEATURE_GROUPING && isGridGroupingEnabled(optionsRef.current);
1085
+ }, []);
1086
+
1087
+ const isFilteringEnabledFn = useCallback((): boolean => {
1088
+ return FEATURE_FILTERING && isGridFilteringEnabled(optionsRef.current);
1089
+ }, []);
1090
+
1091
+ // --- Action dispatchers ---
1092
+
1093
+ const toggleSortFn = useCallback((column: GridColumnDef): void => {
1094
+ if (!FEATURE_SORTING || !isGridColumnSortable(optionsRef.current, column)) return;
1095
+
1096
+ const currentDirection = sortStateRef.current.columnName === column.name ? sortStateRef.current.direction : SORT_DIRECTIONS.none;
1097
+ const nextDirection =
1098
+ currentDirection === SORT_DIRECTIONS.none
1099
+ ? SORT_DIRECTIONS.asc
1100
+ : currentDirection === SORT_DIRECTIONS.asc
1101
+ ? SORT_DIRECTIONS.desc
1102
+ : SORT_DIRECTIONS.none;
1103
+
1104
+ applyGridSortStateCommand(gridApiRef.current!, (state) => setSortState(state), {
1105
+ columnName: nextDirection === SORT_DIRECTIONS.none ? null : column.name,
1106
+ direction: nextDirection,
1107
+ });
1108
+ }, []);
1109
+
1110
+ const updateFilterFn = useCallback((columnName: string, value: string): void => {
1111
+ updateGridFilterCommand(
1112
+ gridApiRef.current!,
1113
+ (updater) => setActiveFilters((current) => updater(current)),
1114
+ () => activeFiltersRef.current,
1115
+ columnName,
1116
+ value
1117
+ );
1118
+ }, []);
1119
+
1120
+ const clearAllFiltersFn = useCallback((): void => {
1121
+ clearGridFiltersCommand(gridApiRef.current!, (filters) => setActiveFilters(filters));
1122
+ }, []);
1123
+
1124
+ const toggleGroupingFn = useCallback((column: GridColumnDef, event?: React.MouseEvent): void => {
1125
+ event?.stopPropagation();
1126
+ if (!(FEATURE_GROUPING && isGridGroupingEnabled(optionsRef.current))) return;
1127
+ const current = groupByColumnsRef.current;
1128
+ const next = current.includes(column.name)
1129
+ ? current.filter((n) => n !== column.name)
1130
+ : [...current, column.name];
1131
+ groupByColumnsRef.current = next;
1132
+ setGroupByColumns(next);
1133
+ gridApiRef.current!.core.raise.groupingChanged(next);
1134
+ }, []);
1135
+
1136
+ const toggleGroupFn = useCallback((item: GroupItem): void => {
1137
+ setCollapsedGroups((current) => ({
1138
+ ...current,
1139
+ [item.id]: !current[item.id],
1140
+ }));
1141
+ }, []);
1142
+
1143
+ const focusCellFn = useCallback((row: GridRow, column: GridColumnDef, triggerEvent?: Event | KeyboardEvent | null): void => {
1144
+ const nextFocusResult = buildGridFocusCellResult({
1145
+ currentFocusedCell: focusedCellRef.current,
1146
+ currentEditingCell: editingCellRef.current,
1147
+ rowId: row.id,
1148
+ columnName: column.name,
1149
+ shouldEditOnFocus: shouldEditOnFocusFn(column),
1150
+ isCellEditable: isCellEditable(row, column, triggerEvent),
1151
+ });
1152
+ setFocusedCell(nextFocusResult.focusedCell);
1153
+
1154
+ if (nextFocusResult.shouldBeginEdit) {
1155
+ startCellEditFn(row, column, triggerEvent);
1156
+ }
1157
+ }, [isCellEditable, shouldEditOnFocusFn, startCellEditFn]);
1158
+
1159
+ const handleCellKeyDownFn = useCallback((row: GridRow, column: GridColumnDef, event: React.KeyboardEvent): void => {
1160
+ focusCellFn(row, column, event.nativeEvent);
1161
+
1162
+ switch (event.key) {
1163
+ case 'ArrowLeft':
1164
+ event.preventDefault();
1165
+ moveFocusFn(row, column, 'left', event.nativeEvent);
1166
+ return;
1167
+ case 'ArrowRight':
1168
+ event.preventDefault();
1169
+ moveFocusFn(row, column, 'right', event.nativeEvent);
1170
+ return;
1171
+ case 'ArrowUp':
1172
+ event.preventDefault();
1173
+ moveFocusFn(row, column, 'up', event.nativeEvent);
1174
+ return;
1175
+ case 'ArrowDown':
1176
+ event.preventDefault();
1177
+ moveFocusFn(row, column, 'down', event.nativeEvent);
1178
+ return;
1179
+ case 'Tab':
1180
+ event.preventDefault();
1181
+ moveFocusFn(row, column, event.shiftKey ? 'left' : 'right', event.nativeEvent);
1182
+ return;
1183
+ case 'Enter':
1184
+ event.preventDefault();
1185
+ moveFocusFn(row, column, event.shiftKey ? 'up' : 'down', event.nativeEvent);
1186
+ return;
1187
+ case 'F2':
1188
+ event.preventDefault();
1189
+ if (isCellEditable(row, column, event.nativeEvent)) {
1190
+ startCellEditFn(row, column, event.nativeEvent);
1191
+ }
1192
+ return;
1193
+ case 'Backspace':
1194
+ case 'Delete':
1195
+ if (isCellEditable(row, column, event.nativeEvent)) {
1196
+ event.preventDefault();
1197
+ startCellEditFn(row, column, event.nativeEvent, '');
1198
+ }
1199
+ return;
1200
+ default:
1201
+ break;
1202
+ }
1203
+
1204
+ if (isPrintableGridKey(event.key, event.ctrlKey, event.metaKey, event.altKey) && isCellEditable(row, column, event.nativeEvent)) {
1205
+ event.preventDefault();
1206
+ startCellEditFn(row, column, event.nativeEvent, event.key);
1207
+ }
1208
+ }, [focusCellFn, moveFocusFn, isCellEditable, startCellEditFn]);
1209
+
1210
+ const handleCellDoubleClickFn = useCallback((row: GridRow, column: GridColumnDef, event: React.MouseEvent): void => {
1211
+ focusCellFn(row, column, event.nativeEvent);
1212
+ if (isCellEditable(row, column, event.nativeEvent)) {
1213
+ startCellEditFn(row, column, event.nativeEvent);
1214
+ }
1215
+ }, [focusCellFn, isCellEditable, startCellEditFn]);
1216
+
1217
+ const updateEditingValueFn = useCallback((value: string): void => {
1218
+ setEditingValue(value);
1219
+ }, []);
1220
+
1221
+ const handleEditorKeyDownFn = useCallback((event: React.KeyboardEvent): void => {
1222
+ if (event.key === 'Escape') {
1223
+ event.preventDefault();
1224
+ cancelCellEditFn();
1225
+ return;
1226
+ }
1227
+ if (event.key === 'Enter') {
1228
+ event.preventDefault();
1229
+ commitCellEditFn(event.shiftKey ? 'up' : 'down');
1230
+ return;
1231
+ }
1232
+ if (event.key === 'Tab') {
1233
+ event.preventDefault();
1234
+ commitCellEditFn(event.shiftKey ? 'left' : 'right');
1235
+ }
1236
+ }, [cancelCellEditFn, commitCellEditFn]);
1237
+
1238
+ const handleEditorBlurFn = useCallback((event: React.FocusEvent): void => {
1239
+ const ec = editingCellRef.current;
1240
+ const target = event.target as HTMLElement | null;
1241
+ if (!ec || !target) return;
1242
+ if (target.dataset['rowId'] !== ec.rowId || target.dataset['colName'] !== ec.columnName) return;
1243
+ commitCellEditFn(undefined, false);
1244
+ }, [commitCellEditFn]);
1245
+
1246
+ const toggleRowExpansionFn = useCallback((row: GridRow, event?: React.MouseEvent): void => {
1247
+ event?.stopPropagation();
1248
+ toggleRowExpansionByRefFn(row);
1249
+ }, [toggleRowExpansionByRefFn]);
1250
+
1251
+ const toggleTreeRowFn = useCallback((row: GridRow, event?: React.MouseEvent): void => {
1252
+ event?.stopPropagation();
1253
+ toggleTreeRowByRefFn(row);
1254
+ }, [toggleTreeRowByRefFn]);
1255
+
1256
+ const moveColumnFn = useCallback((fromIndex: number, toIndex: number): void => {
1257
+ moveGridColumnCommand(
1258
+ gridApiRef.current!,
1259
+ FEATURE_COLUMN_MOVING && (optionsRef.current.enableColumnMoving === true),
1260
+ (updater) => setColumnOrder((current) => updater(current)),
1261
+ fromIndex,
1262
+ toIndex
1263
+ );
1264
+ }, []);
1265
+
1266
+ const nextPageFn = useCallback((): void => {
1267
+ seekPageFn(getCurrentPageValueFn() + 1);
1268
+ }, [seekPageFn, getCurrentPageValueFn]);
1269
+
1270
+ const previousPageFn = useCallback((): void => {
1271
+ seekPageFn(getCurrentPageValueFn() - 1);
1272
+ }, [seekPageFn, getCurrentPageValueFn]);
1273
+
1274
+ const onPageSizeChangeFn = useCallback((value: string): void => {
1275
+ setPaginationPageSizeFn(Number(value));
1276
+ }, [setPaginationPageSizeFn]);
1277
+
1278
+ const onViewportScrollFn = useCallback((startIndex: number): void => {
1279
+ if (!scrollingRef.current) {
1280
+ scrollingRef.current = true;
1281
+ raiseGridScrollBegin(gridApiRef.current!);
1282
+ }
1283
+
1284
+ if (scrollEndHandleRef.current) {
1285
+ window.clearTimeout(scrollEndHandleRef.current);
1286
+ }
1287
+
1288
+ scrollEndHandleRef.current = window.setTimeout(() => {
1289
+ scrollingRef.current = false;
1290
+ raiseGridScrollEnd(gridApiRef.current!);
1291
+ }, 120);
1292
+
1293
+ const isInfiniteScrollEnabled = FEATURE_INFINITE_SCROLL && (
1294
+ optionsRef.current.infiniteScrollRowsFromEnd !== undefined
1295
+ || optionsRef.current.infiniteScrollUp === true
1296
+ || optionsRef.current.infiniteScrollDown !== undefined
1297
+ );
1298
+
1299
+ maybeRequestInfiniteScrollCommand(gridApiRef.current!, {
1300
+ enabled: isInfiniteScrollEnabled,
1301
+ virtualizationEnabled: pipelineRef.current.virtualizationEnabled,
1302
+ state: infiniteScrollStateRef.current,
1303
+ startIndex,
1304
+ visibleRows: pipelineRef.current.visibleRows.length,
1305
+ viewportRows: Math.max(1, Math.ceil((optionsRef.current.viewportHeight ?? 560) / (optionsRef.current.rowHeight ?? 44))),
1306
+ threshold: optionsRef.current.infiniteScrollRowsFromEnd ?? 20,
1307
+ setState: (state) => setInfiniteScrollState(state),
1308
+ });
1309
+ }, []);
1310
+
1311
+ return {
1312
+ pipeline,
1313
+ visibleColumns,
1314
+ labels,
1315
+ gridTemplateColumns,
1316
+ gridApi,
1317
+ gridContainerRef,
1318
+
1319
+ activeFilters,
1320
+ groupByColumns,
1321
+ collapsedGroups,
1322
+ sortState,
1323
+ focusedCell,
1324
+ editingCell,
1325
+ editingValue,
1326
+ expandedRows,
1327
+ expandedTreeRows,
1328
+ currentPage,
1329
+ pageSize,
1330
+ benchmarkResult,
1331
+ infiniteScrollState,
1332
+
1333
+ totalRows,
1334
+ visibleRowCount,
1335
+ displayItems,
1336
+ virtualizationEnabled,
1337
+ pipelineMs: pipelineMsVal,
1338
+ paginationCurrentPage,
1339
+ paginationTotalPages,
1340
+ paginationSelectedPageSize,
1341
+ rowSize,
1342
+ viewportHeightPx,
1343
+
1344
+ headerLabel: headerLabelFn,
1345
+ isGroupItem: isGroupItemFn,
1346
+ isExpandableItem: isExpandableItemFn,
1347
+ isRowItem: isRowItemFn,
1348
+ isOddStripedRow: isOddStripedRowFn,
1349
+ sortButtonLabel: sortButtonLabelFn,
1350
+ sortAriaSort: sortAriaSortFn,
1351
+ sortDirection: sortDirectionFn,
1352
+ groupingButtonLabel: groupingButtonLabelFn,
1353
+ filterValue: filterValueFn,
1354
+ filterPlaceholder: filterPlaceholderFn,
1355
+ isFilterInputDisabled: isFilterInputDisabledFn,
1356
+ groupDisclosureLabel: groupDisclosureLabelFn,
1357
+ displayValue: displayValueFn,
1358
+ isFocusedCell: isFocusedCellFn,
1359
+ isEditingCell: isEditingCellFn,
1360
+ editorInputType: editorInputTypeFn,
1361
+ cellContext: cellContextFn,
1362
+ expandedContext: expandedContextFn,
1363
+ columnWidth: columnWidthFn,
1364
+ isColumnSortable: isColumnSortableFn,
1365
+ isColumnFilterable: isColumnFilterableFn,
1366
+ cellIndent: cellIndentFn,
1367
+ treeToggleLabel: treeToggleLabelFn,
1368
+ isTreeRowExpanded: isTreeRowExpandedFn,
1369
+ expandToggleLabel: expandToggleLabelFn,
1370
+ isGrouped: isGroupedFn,
1371
+ showTreeToggle: showTreeToggleFn,
1372
+ showExpandToggle: showExpandToggleFn,
1373
+ showPaginationControls: showPaginationControlsFn,
1374
+ paginationSummary: paginationSummaryFn,
1375
+ pageSizeOptions: pageSizeOptionsFn,
1376
+ isCellEditable,
1377
+ shouldEditOnFocus: shouldEditOnFocusFn,
1378
+
1379
+ sortingFeature: FEATURE_SORTING,
1380
+ filteringFeature: FEATURE_FILTERING,
1381
+ groupingFeature: FEATURE_GROUPING,
1382
+ paginationFeature: FEATURE_PAGINATION,
1383
+ cellEditFeature: FEATURE_CELL_EDIT,
1384
+ expandableFeature: FEATURE_EXPANDABLE,
1385
+ treeViewFeature: FEATURE_TREE_VIEW,
1386
+ infiniteScrollFeature: FEATURE_INFINITE_SCROLL,
1387
+ columnMovingFeature: FEATURE_COLUMN_MOVING,
1388
+ csvExportFeature: FEATURE_CSV_EXPORT,
1389
+
1390
+ isGroupingEnabled: isGroupingEnabledFn,
1391
+ isFilteringEnabled: isFilteringEnabledFn,
1392
+
1393
+ toggleSort: toggleSortFn,
1394
+ updateFilter: updateFilterFn,
1395
+ clearAllFilters: clearAllFiltersFn,
1396
+ toggleGrouping: toggleGroupingFn,
1397
+ toggleGroup: toggleGroupFn,
1398
+ focusCell: focusCellFn,
1399
+ handleCellKeyDown: handleCellKeyDownFn,
1400
+ handleCellDoubleClick: handleCellDoubleClickFn,
1401
+ updateEditingValue: updateEditingValueFn,
1402
+ handleEditorKeyDown: handleEditorKeyDownFn,
1403
+ handleEditorBlur: handleEditorBlurFn,
1404
+ toggleRowExpansion: toggleRowExpansionFn,
1405
+ toggleTreeRow: toggleTreeRowFn,
1406
+ moveColumn: moveColumnFn,
1407
+ nextPage: nextPageFn,
1408
+ previousPage: previousPageFn,
1409
+ onPageSizeChange: onPageSizeChangeFn,
1410
+ runBenchmark: runBenchmarkFn,
1411
+ exportCsv: exportCsvFn,
1412
+ onViewportScroll: onViewportScrollFn,
1413
+ };
1414
+ }