@ornery/ui-grid-vanilla 0.1.7

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,906 @@
1
+ import {
2
+ SORT_DIRECTIONS,
3
+ beginGridCellEditCommand,
4
+ buildGridRows,
5
+ buildGridCellContext,
6
+ cancelGridCellEditCommand,
7
+ canGridExpandRows,
8
+ canGridMoveColumns,
9
+ clearGridFiltersCommand,
10
+ clearGridGroupingCommand,
11
+ commitGridCellEditCommand,
12
+ computePinnedOffset,
13
+ createGridApi,
14
+ defaultGridEngine,
15
+ expandAllGridRowsCommand,
16
+ expandAllGridTreeRowsCommand,
17
+ findGridRowById as coreFindGridRowById,
18
+ formatGridCellDisplayValue,
19
+ getCellValue,
20
+ getCurrentPageValue,
21
+ getEffectivePageSize,
22
+ getFirstRowIndexValue,
23
+ getLastRowIndexValue,
24
+ getTotalPagesValue,
25
+ gridCellIndent,
26
+ gridColumnWidth,
27
+ gridEditorInputType,
28
+ gridExpandToggleLabelForRow,
29
+ gridFilterPlaceholder,
30
+ gridGroupingButtonLabel,
31
+ gridSortButtonLabel,
32
+ gridTreeToggleLabelForRow,
33
+ headerLabel,
34
+ isGridColumnFilterable,
35
+ isGridColumnGrouped,
36
+ isGridColumnSortable,
37
+ isGridFilteringEnabled,
38
+ isGridGroupingEnabled,
39
+ isGridPaginationEnabled,
40
+ isGridSortingEnabled,
41
+ isGridTreeEnabled,
42
+ isGridTreeRowExpanded,
43
+ isPinningEnabled,
44
+ isColumnPinnable,
45
+ gridGroupDisclosureLabel,
46
+ moveGridVisibleColumnCommand,
47
+ parseGridEditedValue,
48
+ pinGridColumnCommand,
49
+ resolveGridLabels,
50
+ resolveGridRowId as coreResolveGridRowId,
51
+ seekGridPaginationCommand,
52
+ setGridPaginationPageSizeCommand,
53
+ setPathValue,
54
+ shouldShowGridExpandToggle,
55
+ shouldShowGridPaginationControls,
56
+ shouldShowGridTreeToggle,
57
+ sortGridColumnCommand,
58
+ toggleGridGroupingCommand,
59
+ toggleGridRowExpansionCommand,
60
+ toggleGridTreeRowCommand,
61
+ updateGridFilterCommand,
62
+ type DisplayItem,
63
+ type GridCellPosition,
64
+ type GridColumnDef,
65
+ type GridLabels,
66
+ type GridOptions,
67
+ type GridRecord,
68
+ type GridRow,
69
+ type PipelineResult,
70
+ type PinDirection,
71
+ type PinnedColumnState,
72
+ type SortDirection,
73
+ type GroupItem,
74
+ type SortState,
75
+ type UiGridApi,
76
+ } from '@ornery/ui-grid-core';
77
+
78
+ export interface GridSaveState {
79
+ sortState: SortState;
80
+ activeFilters: Record<string, string>;
81
+ groupByColumns: string[];
82
+ pinnedColumns: PinnedColumnState;
83
+ columnOrder: string[];
84
+ currentPage: number;
85
+ pageSize: number;
86
+ }
87
+
88
+ export interface GridControllerSnapshot {
89
+ options: GridOptions;
90
+ labels: GridLabels;
91
+ rowSize: number;
92
+ visibleColumns: GridColumnDef[];
93
+ gridTemplateColumns: string;
94
+ pipeline: PipelineResult;
95
+ activeFilters: Record<string, string>;
96
+ sortState: SortState;
97
+ groupByColumns: string[];
98
+ pinnedColumns: PinnedColumnState;
99
+ currentPage: number;
100
+ totalPages: number;
101
+ firstRowIndex: number;
102
+ lastRowIndex: number;
103
+ pageSize: number;
104
+ editingCell: GridCellPosition | null;
105
+ editingValue: string;
106
+ }
107
+
108
+ export type GridControllerSubscriber = (snapshot: GridControllerSnapshot) => void;
109
+
110
+ function orderVisibleColumns(
111
+ columns: readonly GridColumnDef[],
112
+ order: readonly string[],
113
+ ): GridColumnDef[] {
114
+ return [...columns]
115
+ .filter((column) => column.visible !== false)
116
+ .sort((left, right) => order.indexOf(left.name) - order.indexOf(right.name));
117
+ }
118
+
119
+ function buildGridTemplateColumns(columns: readonly GridColumnDef[]): string {
120
+ return columns.map((column) => gridColumnWidth(column)).join(' ');
121
+ }
122
+
123
+ export class VanillaGridController {
124
+ private options: GridOptions;
125
+ private activeFilters: Record<string, string> = {};
126
+ private sortState: SortState = {
127
+ columnName: null,
128
+ direction: SORT_DIRECTIONS.none,
129
+ };
130
+ private groupByColumns: string[] = [];
131
+ private collapsedGroups: Record<string, boolean> = {};
132
+ private hiddenRowReasons: Record<string, string[]> = {};
133
+ private expandedRows: Record<string, boolean> = {};
134
+ private expandedTreeRows: Record<string, boolean> = {};
135
+ private pinnedColumns: PinnedColumnState = {};
136
+ private columnOrder: string[] = [];
137
+ private currentPage = 1;
138
+ private pageSize = 0;
139
+ private editingCell: GridCellPosition | null = null;
140
+ private editingValue = '';
141
+ private pipeline: PipelineResult;
142
+ private labels: GridLabels;
143
+ private visibleColumns: GridColumnDef[] = [];
144
+ private apiRegistered = false;
145
+
146
+ private readonly subscribers = new Set<GridControllerSubscriber>();
147
+
148
+ readonly gridApi: UiGridApi;
149
+
150
+ constructor(options: GridOptions) {
151
+ this.options = options;
152
+ this.labels = resolveGridLabels(options.labels);
153
+ this.columnOrder = options.columnDefs.map((column) => column.name);
154
+
155
+ this.pipeline = {
156
+ visibleRows: [],
157
+ displayItems: [],
158
+ virtualizationEnabled: false,
159
+ pipelineMs: 0,
160
+ totalItems: 0,
161
+ };
162
+
163
+ this.gridApi = createGridApi({
164
+ refresh: () => this.refresh(),
165
+ getVisibleRows: () => this.pipeline.visibleRows,
166
+ setRowInvisible: (row, reason) => this.setRowInvisible(row, reason),
167
+ clearRowInvisible: (row, reason) => this.clearRowInvisible(row, reason),
168
+ setFilter: (columnName, value) => this.setFilter(columnName, value),
169
+ clearAllFilters: () => this.clearAllFilters(),
170
+ sortColumn: (columnName, direction) => this.sortColumn(columnName, direction),
171
+ moveColumn: (fromIndex, toIndex) => {
172
+ const visibleNames = this.visibleColumns.map((column) => column.name);
173
+ const source = visibleNames[fromIndex];
174
+ const target = visibleNames[toIndex];
175
+ if (source && target) {
176
+ this.moveVisibleColumn(source, target);
177
+ }
178
+ },
179
+ pinColumn: (columnName, direction) => this.pinColumn(columnName, direction),
180
+ toggleGrouping: (columnName) => this.toggleGrouping(columnName),
181
+ clearGrouping: () => this.clearGrouping(),
182
+ benchmark: (iterations) => this.benchmark(iterations),
183
+ exportCsv: () => {
184
+ // Export remains hosted by framework wrappers for now.
185
+ },
186
+ paginationGetPage: () => this.getCurrentPage(),
187
+ paginationGetTotalPages: () => this.getTotalPages(),
188
+ paginationGetFirstRowIndex: () => this.getFirstRowIndex(),
189
+ paginationGetLastRowIndex: () => this.getLastRowIndex(),
190
+ paginationNextPage: () => this.seekPage(this.currentPage + 1),
191
+ paginationPreviousPage: () => this.seekPage(this.currentPage - 1),
192
+ paginationSeek: (page) => this.seekPage(page),
193
+ paginationSetPageSize: (pageSize) => this.setPageSize(pageSize),
194
+ toggleRowExpansion: (row) => this.toggleRowExpansion(row),
195
+ expandAllRows: () => this.expandAllRows(),
196
+ collapseAllRows: () => this.collapseAllRows(),
197
+ toggleAllRows: () => {
198
+ if (Object.keys(this.expandedRows).length > 0) {
199
+ this.collapseAllRows();
200
+ return;
201
+ }
202
+ this.expandAllRows();
203
+ },
204
+ treeToggleRow: (row) => this.toggleTreeRow(row),
205
+ treeExpandAllRows: () => this.expandAllTreeRows(),
206
+ treeCollapseAllRows: () => this.collapseAllTreeRows(),
207
+ treeExpandRow: (row) => this.setTreeRowExpanded(row, true),
208
+ treeCollapseRow: (row) => this.setTreeRowExpanded(row, false),
209
+ treeGetRowChildren: (row) => {
210
+ const rowId = this.resolveRowId(row);
211
+ return this.pipeline.visibleRows.filter((candidate) => candidate.parentId === rowId);
212
+ },
213
+ treeGetState: () => ({ ...this.expandedTreeRows }),
214
+ treeSetState: (state) => {
215
+ this.expandedTreeRows = { ...state };
216
+ this.refresh();
217
+ },
218
+ beginCellEdit: (row, columnName, triggerEvent) =>
219
+ this.beginCellEdit(row, columnName, triggerEvent),
220
+ endCellEdit: () => {
221
+ this.commitCellEdit();
222
+ },
223
+ cancelCellEdit: () => {
224
+ this.cancelCellEdit();
225
+ },
226
+ getEditingCell: () => this.editingCell,
227
+ });
228
+
229
+ this.refresh();
230
+ }
231
+
232
+ setOptions(options: GridOptions): void {
233
+ this.options = options;
234
+ this.labels = resolveGridLabels(options.labels);
235
+ if (this.columnOrder.length === 0) {
236
+ this.columnOrder = options.columnDefs.map((column) => column.name);
237
+ } else {
238
+ const names = new Set(options.columnDefs.map((column) => column.name));
239
+ this.columnOrder = this.columnOrder.filter((name) => names.has(name));
240
+ for (const column of options.columnDefs) {
241
+ if (!this.columnOrder.includes(column.name)) {
242
+ this.columnOrder.push(column.name);
243
+ }
244
+ }
245
+ }
246
+
247
+ this.apiRegistered = false;
248
+ this.refresh();
249
+ }
250
+
251
+ subscribe(subscriber: GridControllerSubscriber): () => void {
252
+ this.subscribers.add(subscriber);
253
+ subscriber(this.getSnapshot());
254
+ return () => {
255
+ this.subscribers.delete(subscriber);
256
+ };
257
+ }
258
+
259
+ getSnapshot(): GridControllerSnapshot {
260
+ const totalPages = this.getTotalPages();
261
+ return {
262
+ options: this.options,
263
+ labels: this.labels,
264
+ rowSize: this.getRowSize(),
265
+ visibleColumns: this.visibleColumns,
266
+ gridTemplateColumns: buildGridTemplateColumns(this.visibleColumns),
267
+ pipeline: this.pipeline,
268
+ activeFilters: this.activeFilters,
269
+ sortState: this.sortState,
270
+ groupByColumns: this.groupByColumns,
271
+ pinnedColumns: this.pinnedColumns,
272
+ currentPage: this.getCurrentPage(),
273
+ totalPages,
274
+ firstRowIndex: this.getFirstRowIndex(),
275
+ lastRowIndex: this.getLastRowIndex(),
276
+ pageSize: this.getEffectivePageSize(),
277
+ editingCell: this.editingCell,
278
+ editingValue: this.editingValue,
279
+ };
280
+ }
281
+
282
+ getDisplayItems(): DisplayItem[] {
283
+ return this.pipeline.displayItems;
284
+ }
285
+
286
+ setCollapsedGroup(groupId: string, collapsed: boolean): void {
287
+ this.collapsedGroups = {
288
+ ...this.collapsedGroups,
289
+ [groupId]: collapsed,
290
+ };
291
+ this.refresh();
292
+ }
293
+
294
+ toggleSort(columnName: string): void {
295
+ const current =
296
+ this.sortState.columnName === columnName ? this.sortState.direction : SORT_DIRECTIONS.none;
297
+ if (current === SORT_DIRECTIONS.none) {
298
+ this.sortColumn(columnName, SORT_DIRECTIONS.asc);
299
+ return;
300
+ }
301
+ if (current === SORT_DIRECTIONS.asc) {
302
+ this.sortColumn(columnName, SORT_DIRECTIONS.desc);
303
+ return;
304
+ }
305
+ this.sortColumn(columnName, SORT_DIRECTIONS.none);
306
+ }
307
+
308
+ sortColumn(columnName: string, direction: SortDirection = SORT_DIRECTIONS.none): void {
309
+ sortGridColumnCommand(
310
+ this.gridApi,
311
+ (next) => {
312
+ this.sortState = next;
313
+ },
314
+ columnName,
315
+ direction,
316
+ );
317
+ this.refresh();
318
+ }
319
+
320
+ setFilter(columnName: string, value: string): void {
321
+ updateGridFilterCommand(
322
+ this.gridApi,
323
+ (updater) => {
324
+ this.activeFilters = updater(this.activeFilters);
325
+ },
326
+ () => this.activeFilters,
327
+ columnName,
328
+ value,
329
+ );
330
+ this.currentPage = 1;
331
+ this.refresh();
332
+ }
333
+
334
+ clearAllFilters(): void {
335
+ clearGridFiltersCommand(this.gridApi, (next) => {
336
+ this.activeFilters = next;
337
+ });
338
+ this.currentPage = 1;
339
+ this.refresh();
340
+ }
341
+
342
+ toggleGrouping(columnName: string): void {
343
+ toggleGridGroupingCommand(
344
+ this.gridApi,
345
+ isGridGroupingEnabled(this.options),
346
+ (updater) => {
347
+ this.groupByColumns = updater(this.groupByColumns);
348
+ },
349
+ () => this.groupByColumns,
350
+ columnName,
351
+ );
352
+ this.refresh();
353
+ }
354
+
355
+ clearGrouping(): void {
356
+ clearGridGroupingCommand(this.gridApi, (next) => {
357
+ this.groupByColumns = next;
358
+ });
359
+ this.refresh();
360
+ }
361
+
362
+ seekPage(page: number): void {
363
+ seekGridPaginationCommand(
364
+ this.gridApi,
365
+ (next) => {
366
+ this.currentPage = next;
367
+ },
368
+ () => this.getTotalPages(),
369
+ () => this.getEffectivePageSize(),
370
+ page,
371
+ );
372
+ this.refresh();
373
+ }
374
+
375
+ setPageSize(pageSize: number): void {
376
+ setGridPaginationPageSizeCommand(
377
+ this.gridApi,
378
+ (next) => {
379
+ this.pageSize = next;
380
+ },
381
+ (next) => {
382
+ this.currentPage = next;
383
+ },
384
+ pageSize,
385
+ );
386
+ this.refresh();
387
+ }
388
+
389
+ pinColumn(columnName: string, direction: PinDirection): void {
390
+ pinGridColumnCommand(
391
+ this.gridApi,
392
+ isPinningEnabled(this.options),
393
+ (next) => {
394
+ this.pinnedColumns = next;
395
+ },
396
+ () => this.pinnedColumns,
397
+ columnName,
398
+ direction,
399
+ );
400
+ this.refresh();
401
+ }
402
+
403
+ moveVisibleColumn(columnName: string, targetColumnName: string): void {
404
+ moveGridVisibleColumnCommand(
405
+ this.gridApi,
406
+ canGridMoveColumns(this.options),
407
+ this.columnOrder,
408
+ this.visibleColumns.map((column) => column.name),
409
+ columnName,
410
+ targetColumnName,
411
+ (next) => {
412
+ this.columnOrder = next;
413
+ },
414
+ );
415
+ this.refresh();
416
+ }
417
+
418
+ toggleRowExpansion(row: GridRow | GridRecord | string): void {
419
+ const rowId = this.resolveRowId(row);
420
+ toggleGridRowExpansionCommand(
421
+ this.gridApi,
422
+ canGridExpandRows(this.options),
423
+ this.expandedRows,
424
+ rowId,
425
+ (next) => {
426
+ this.expandedRows = next;
427
+ },
428
+ (candidateRowId) => this.findRowById(candidateRowId),
429
+ );
430
+ this.refresh();
431
+ }
432
+
433
+ expandAllRows(): void {
434
+ expandAllGridRowsCommand(
435
+ (data) => this.buildRowsFromData(data),
436
+ this.options.data,
437
+ (next) => {
438
+ this.expandedRows = next;
439
+ },
440
+ );
441
+ this.refresh();
442
+ }
443
+
444
+ collapseAllRows(): void {
445
+ this.expandedRows = {};
446
+ this.refresh();
447
+ }
448
+
449
+ toggleTreeRow(row: GridRow | GridRecord | string): void {
450
+ const rowId = this.resolveRowId(row);
451
+ toggleGridTreeRowCommand(
452
+ this.gridApi,
453
+ this.expandedTreeRows,
454
+ rowId,
455
+ (next) => {
456
+ this.expandedTreeRows = next;
457
+ },
458
+ (candidateRowId) => this.findRowById(candidateRowId),
459
+ );
460
+ this.refresh();
461
+ }
462
+
463
+ expandAllTreeRows(): void {
464
+ expandAllGridTreeRowsCommand(
465
+ (data) => this.buildRowsFromData(data),
466
+ this.options.data,
467
+ (next) => {
468
+ this.expandedTreeRows = next;
469
+ },
470
+ );
471
+ this.refresh();
472
+ }
473
+
474
+ collapseAllTreeRows(): void {
475
+ this.expandedTreeRows = {};
476
+ this.refresh();
477
+ }
478
+
479
+ setTreeRowExpanded(row: GridRow | GridRecord | string, expanded: boolean): void {
480
+ const rowId = this.resolveRowId(row);
481
+ if (expanded) {
482
+ this.expandedTreeRows = {
483
+ ...this.expandedTreeRows,
484
+ [rowId]: true,
485
+ };
486
+ } else {
487
+ const nextState = { ...this.expandedTreeRows };
488
+ delete nextState[rowId];
489
+ this.expandedTreeRows = nextState;
490
+ }
491
+ this.refresh();
492
+ }
493
+
494
+ beginCellEdit(
495
+ row: GridRow | GridRecord | string,
496
+ columnName: string,
497
+ triggerEvent?: Event | KeyboardEvent | null,
498
+ ): void {
499
+ const rowId = this.resolveRowId(row);
500
+ const gridRow = this.findRowById(rowId);
501
+ const column = this.findColumnByName(columnName);
502
+ if (!gridRow || !column) {
503
+ return;
504
+ }
505
+
506
+ beginGridCellEditCommand(
507
+ this.gridApi,
508
+ {
509
+ setFocusedCell: () => {
510
+ // Focus state is intentionally owned by wrappers.
511
+ },
512
+ setEditingCell: (next) => {
513
+ this.editingCell = next;
514
+ },
515
+ setEditingValue: (next) => {
516
+ this.editingValue = next;
517
+ },
518
+ },
519
+ gridRow,
520
+ column,
521
+ getCellValue(gridRow.entity, column),
522
+ triggerEvent,
523
+ );
524
+
525
+ this.refresh();
526
+ }
527
+
528
+ updateEditingValue(value: string): void {
529
+ this.editingValue = value;
530
+ this.emit();
531
+ }
532
+
533
+ commitCellEdit(): void {
534
+ commitGridCellEditCommand(this.gridApi, {
535
+ getEditingCell: () => this.editingCell,
536
+ getEditingValue: () => this.editingValue,
537
+ setEditingCell: (next) => {
538
+ this.editingCell = next;
539
+ },
540
+ setEditingValue: (next) => {
541
+ this.editingValue = next;
542
+ },
543
+ findRowById: (rowId) => this.findRowById(rowId),
544
+ findColumnByName: (columnName) => this.findColumnByName(columnName),
545
+ parseEditedValue: (column, value, oldValue) => parseGridEditedValue(column, value, oldValue),
546
+ setCellValue: (rowEntity, column, value) =>
547
+ setPathValue(rowEntity, column.field ?? column.name, value),
548
+ });
549
+
550
+ this.refresh();
551
+ }
552
+
553
+ cancelCellEdit(): void {
554
+ cancelGridCellEditCommand(this.gridApi, {
555
+ getEditingCell: () => this.editingCell,
556
+ setEditingCell: (next) => {
557
+ this.editingCell = next;
558
+ },
559
+ setEditingValue: (next) => {
560
+ this.editingValue = next;
561
+ },
562
+ findRowById: (rowId) => this.findRowById(rowId),
563
+ findColumnByName: (columnName) => this.findColumnByName(columnName),
564
+ });
565
+
566
+ this.refresh();
567
+ }
568
+
569
+ isGroupingEnabled(): boolean {
570
+ return isGridGroupingEnabled(this.options);
571
+ }
572
+
573
+ isSortingEnabled(): boolean {
574
+ return isGridSortingEnabled(this.options);
575
+ }
576
+
577
+ isFilteringEnabled(): boolean {
578
+ return isGridFilteringEnabled(this.options);
579
+ }
580
+
581
+ isPaginationEnabled(): boolean {
582
+ return isGridPaginationEnabled(this.options);
583
+ }
584
+
585
+ shouldShowPaginationControls(): boolean {
586
+ return shouldShowGridPaginationControls(this.options);
587
+ }
588
+
589
+ isTreeEnabled(): boolean {
590
+ return isGridTreeEnabled(this.options);
591
+ }
592
+
593
+ isExpandableEnabled(): boolean {
594
+ return canGridExpandRows(this.options);
595
+ }
596
+
597
+ isColumnSortable(column: GridColumnDef): boolean {
598
+ return isGridColumnSortable(this.options, column);
599
+ }
600
+
601
+ isColumnFilterable(column: GridColumnDef): boolean {
602
+ return isGridColumnFilterable(this.options, column);
603
+ }
604
+
605
+ isColumnGrouped(column: GridColumnDef): boolean {
606
+ return isGridColumnGrouped(this.groupByColumns, column);
607
+ }
608
+
609
+ sortButtonLabel(column: GridColumnDef): string {
610
+ return gridSortButtonLabel(this.getSortDirection(column), this.labels);
611
+ }
612
+
613
+ groupingButtonLabel(column: GridColumnDef): string {
614
+ return gridGroupingButtonLabel(this.isColumnGrouped(column), this.labels);
615
+ }
616
+
617
+ filterPlaceholder(column: GridColumnDef): string {
618
+ return gridFilterPlaceholder(this.isColumnFilterable(column), this.labels);
619
+ }
620
+
621
+ getSortDirection(column: GridColumnDef): SortDirection {
622
+ return this.sortState.columnName === column.name
623
+ ? this.sortState.direction
624
+ : SORT_DIRECTIONS.none;
625
+ }
626
+
627
+ isPinningEnabled(): boolean {
628
+ return isPinningEnabled(this.options);
629
+ }
630
+
631
+ isColumnPinnable(column: GridColumnDef): boolean {
632
+ return isColumnPinnable(this.options, column);
633
+ }
634
+
635
+ isPinned(column: GridColumnDef): boolean {
636
+ return this.pinnedColumns[column.name] !== undefined;
637
+ }
638
+
639
+ isPinnedLeftLast(column: GridColumnDef): boolean {
640
+ const leftPinned = this.visibleColumns.filter((c) => this.pinnedColumns[c.name] === 'left');
641
+ return leftPinned.length > 0 && leftPinned[leftPinned.length - 1].name === column.name;
642
+ }
643
+
644
+ isPinnedRightFirst(column: GridColumnDef): boolean {
645
+ const rightPinned = this.visibleColumns.filter((c) => this.pinnedColumns[c.name] === 'right');
646
+ return rightPinned.length > 0 && rightPinned[0].name === column.name;
647
+ }
648
+
649
+ groupDisclosureLabel(group: GroupItem): string {
650
+ return gridGroupDisclosureLabel(group.collapsed, this.labels);
651
+ }
652
+
653
+ pinnedOffset(column: GridColumnDef): { side: 'left' | 'right'; offset: string } | null {
654
+ return computePinnedOffset(this.visibleColumns, this.pinnedColumns, column);
655
+ }
656
+
657
+ displayValue(row: GridRow, column: GridColumnDef): unknown {
658
+ return formatGridCellDisplayValue(buildGridCellContext(row, column));
659
+ }
660
+
661
+ headerLabel(column: GridColumnDef): string {
662
+ return headerLabel(column);
663
+ }
664
+
665
+ cellIndent(row: GridRow, column: GridColumnDef): string {
666
+ return gridCellIndent(this.options, this.visibleColumns, row, column);
667
+ }
668
+
669
+ showTreeToggle(row: GridRow, column: GridColumnDef): boolean {
670
+ return shouldShowGridTreeToggle(this.options, this.visibleColumns, row, column);
671
+ }
672
+
673
+ showExpandToggle(_row: GridRow, column: GridColumnDef): boolean {
674
+ return shouldShowGridExpandToggle(this.options, this.visibleColumns, column);
675
+ }
676
+
677
+ isTreeRowExpanded(row: GridRow): boolean {
678
+ return isGridTreeRowExpanded(this.expandedTreeRows, row);
679
+ }
680
+
681
+ treeToggleLabel(row: GridRow): string {
682
+ return gridTreeToggleLabelForRow(this.expandedTreeRows, row, this.labels);
683
+ }
684
+
685
+ expandToggleLabel(row: GridRow): string {
686
+ return gridExpandToggleLabelForRow(row, this.labels);
687
+ }
688
+
689
+ editorInputType(column: GridColumnDef): string {
690
+ return gridEditorInputType(column);
691
+ }
692
+
693
+ isEditingCell(rowId: string, columnName: string): boolean {
694
+ return this.editingCell?.rowId === rowId && this.editingCell?.columnName === columnName;
695
+ }
696
+
697
+ getState(): GridSaveState {
698
+ return {
699
+ sortState: { ...this.sortState },
700
+ activeFilters: { ...this.activeFilters },
701
+ groupByColumns: [...this.groupByColumns],
702
+ pinnedColumns: { ...this.pinnedColumns },
703
+ columnOrder: [...this.columnOrder],
704
+ currentPage: this.currentPage,
705
+ pageSize: this.pageSize,
706
+ };
707
+ }
708
+
709
+ setState(state: Partial<GridSaveState>): void {
710
+ if (state.sortState !== undefined) this.sortState = { ...state.sortState };
711
+ if (state.activeFilters !== undefined) this.activeFilters = { ...state.activeFilters };
712
+ if (state.groupByColumns !== undefined) this.groupByColumns = [...state.groupByColumns];
713
+ if (state.pinnedColumns !== undefined) this.pinnedColumns = { ...state.pinnedColumns };
714
+ if (state.columnOrder !== undefined) this.columnOrder = [...state.columnOrder];
715
+ if (state.currentPage !== undefined) this.currentPage = state.currentPage;
716
+ if (state.pageSize !== undefined) this.pageSize = state.pageSize;
717
+ this.refresh();
718
+ }
719
+
720
+ private refresh(): void {
721
+ const orderedColumns = orderVisibleColumns(this.options.columnDefs, this.columnOrder);
722
+ const pinnedEntries = Object.entries(this.pinnedColumns);
723
+
724
+ if (pinnedEntries.length === 0) {
725
+ this.visibleColumns = orderedColumns;
726
+ } else {
727
+ const byName = new Map(orderedColumns.map((column) => [column.name, column]));
728
+ const pinnedLeft = pinnedEntries
729
+ .filter((entry) => entry[1] === 'left')
730
+ .map((entry) => byName.get(entry[0]))
731
+ .filter((column): column is GridColumnDef => column !== undefined);
732
+ const pinnedRight = pinnedEntries
733
+ .filter((entry) => entry[1] === 'right')
734
+ .map((entry) => byName.get(entry[0]))
735
+ .filter((column): column is GridColumnDef => column !== undefined);
736
+ const middleColumns = orderedColumns.filter(
737
+ (column) => this.pinnedColumns[column.name] === undefined,
738
+ );
739
+ this.visibleColumns = [...pinnedLeft, ...middleColumns, ...pinnedRight];
740
+ }
741
+
742
+ this.pipeline = defaultGridEngine.buildPipeline({
743
+ options: this.options,
744
+ columns: this.visibleColumns,
745
+ activeFilters: this.activeFilters,
746
+ sortState: this.sortState,
747
+ groupByColumns: this.groupByColumns,
748
+ collapsedGroups: this.collapsedGroups,
749
+ hiddenRowReasons: this.hiddenRowReasons,
750
+ expandedRows: this.expandedRows,
751
+ expandedTreeRows: this.expandedTreeRows,
752
+ currentPage: this.currentPage,
753
+ pageSize: this.pageSize,
754
+ rowSize: this.getRowSize(),
755
+ });
756
+
757
+ if (!this.apiRegistered) {
758
+ this.options.onRegisterApi?.(this.gridApi);
759
+ this.apiRegistered = true;
760
+ }
761
+
762
+ this.emit();
763
+ }
764
+
765
+ private emit(): void {
766
+ const snapshot = this.getSnapshot();
767
+ for (const subscriber of this.subscribers) {
768
+ subscriber(snapshot);
769
+ }
770
+ }
771
+
772
+ private findRowById(rowId: string): GridRow | null {
773
+ return coreFindGridRowById(this.buildRowsFromData(this.options.data), rowId);
774
+ }
775
+
776
+ private buildRowsFromData(data: readonly GridRecord[]): GridRow[] {
777
+ return buildGridRows(
778
+ { ...this.options, data },
779
+ this.getRowSize(),
780
+ this.hiddenRowReasons,
781
+ this.expandedRows,
782
+ );
783
+ }
784
+
785
+ private findColumnByName(columnName: string): GridColumnDef | undefined {
786
+ return this.visibleColumns.find((column) => column.name === columnName);
787
+ }
788
+
789
+ private resolveRowId(row: GridRow | GridRecord | string): string {
790
+ return coreResolveGridRowId(this.options, row);
791
+ }
792
+
793
+ private getRowSize(): number {
794
+ return this.options.rowHeight ?? 44;
795
+ }
796
+
797
+ private getTotalPages(): number {
798
+ return getTotalPagesValue(this.options, this.pipeline.totalItems, this.pageSize);
799
+ }
800
+
801
+ private getEffectivePageSize(): number {
802
+ return getEffectivePageSize(this.options, this.pageSize, this.pipeline.totalItems);
803
+ }
804
+
805
+ private getCurrentPage(): number {
806
+ return getCurrentPageValue(
807
+ this.options,
808
+ this.currentPage,
809
+ this.pipeline.totalItems,
810
+ this.pageSize,
811
+ );
812
+ }
813
+
814
+ private getFirstRowIndex(): number {
815
+ return getFirstRowIndexValue(
816
+ this.options,
817
+ this.currentPage,
818
+ this.pipeline.totalItems,
819
+ this.pageSize,
820
+ );
821
+ }
822
+
823
+ private getLastRowIndex(): number {
824
+ return getLastRowIndexValue(
825
+ this.options,
826
+ this.currentPage,
827
+ this.pipeline.totalItems,
828
+ this.pageSize,
829
+ );
830
+ }
831
+
832
+ private setRowInvisible(row: GridRow | GridRecord | string, reason?: string): void {
833
+ const rowId = this.resolveRowId(row);
834
+ const next = { ...this.hiddenRowReasons };
835
+ const reasonValue = reason ?? 'api';
836
+ next[rowId] = [...(next[rowId] ?? []), reasonValue];
837
+ this.hiddenRowReasons = next;
838
+ this.refresh();
839
+ }
840
+
841
+ private clearRowInvisible(row: GridRow | GridRecord | string, reason?: string): void {
842
+ const rowId = this.resolveRowId(row);
843
+ const existing = this.hiddenRowReasons[rowId] ?? [];
844
+ if (existing.length === 0) {
845
+ return;
846
+ }
847
+
848
+ if (!reason) {
849
+ const next = { ...this.hiddenRowReasons };
850
+ delete next[rowId];
851
+ this.hiddenRowReasons = next;
852
+ this.refresh();
853
+ return;
854
+ }
855
+
856
+ const filtered = existing.filter((entry) => entry !== reason);
857
+ const next = { ...this.hiddenRowReasons };
858
+ if (filtered.length > 0) {
859
+ next[rowId] = filtered;
860
+ } else {
861
+ delete next[rowId];
862
+ }
863
+ this.hiddenRowReasons = next;
864
+ this.refresh();
865
+ }
866
+
867
+ private benchmark(iterations?: number) {
868
+ const now = () => (typeof performance === 'undefined' ? Date.now() : performance.now());
869
+ const loops = Math.max(1, iterations ?? this.options.benchmark?.iterations ?? 25);
870
+ const started = now();
871
+ let lastResult = this.pipeline;
872
+
873
+ for (let index = 0; index < loops; index += 1) {
874
+ lastResult = defaultGridEngine.buildPipeline({
875
+ options: this.options,
876
+ columns: this.visibleColumns,
877
+ activeFilters: this.activeFilters,
878
+ sortState: this.sortState,
879
+ groupByColumns: this.groupByColumns,
880
+ collapsedGroups: this.collapsedGroups,
881
+ hiddenRowReasons: this.hiddenRowReasons,
882
+ expandedRows: this.expandedRows,
883
+ expandedTreeRows: this.expandedTreeRows,
884
+ currentPage: this.currentPage,
885
+ pageSize: this.pageSize,
886
+ rowSize: this.getRowSize(),
887
+ });
888
+ }
889
+
890
+ const elapsedMs = now() - started;
891
+ const result = {
892
+ iterations: loops,
893
+ totalMs: elapsedMs,
894
+ averageMs: elapsedMs / loops,
895
+ visibleRows: lastResult.visibleRows.length,
896
+ renderedItems: lastResult.displayItems.length,
897
+ };
898
+
899
+ this.gridApi.core.raise.benchmarkComplete(result);
900
+ return result;
901
+ }
902
+ }
903
+
904
+ export function createVanillaGridController(options: GridOptions): VanillaGridController {
905
+ return new VanillaGridController(options);
906
+ }