argent-grid 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/.github/workflows/ci.yml +69 -0
  2. package/.github/workflows/pages.yml +6 -12
  3. package/.storybook/main.ts +20 -0
  4. package/.storybook/preview.ts +18 -0
  5. package/.storybook/tsconfig.json +24 -0
  6. package/AGENTS.md +70 -27
  7. package/README.md +51 -34
  8. package/angular.json +66 -0
  9. package/biome.json +66 -0
  10. package/demo-app/e2e/selection-screenshot.spec.ts +20 -0
  11. package/docs/AG-GRID-COMPARISON.md +725 -0
  12. package/docs/CELL-RENDERER-GUIDE.md +241 -0
  13. package/docs/CONTEXT-MENU-GUIDE.md +371 -0
  14. package/docs/LIVE-DATA-OPTIMIZATIONS.md +497 -0
  15. package/docs/PERFORMANCE-OPTIMIZATIONS-PHASE1.md +162 -0
  16. package/docs/PERFORMANCE-REVIEW.md +571 -0
  17. package/docs/RESEARCH-STATUS.md +234 -0
  18. package/docs/STATE-PERSISTENCE-GUIDE.md +370 -0
  19. package/docs/STORYBOOK-REFACTOR.md +215 -0
  20. package/docs/STORYBOOK-STATUS.md +156 -0
  21. package/docs/TEST-COVERAGE-REPORT.md +276 -0
  22. package/docs/THEME-API-GUIDE.md +445 -0
  23. package/docs/THEME-API-PLAN.md +364 -0
  24. package/e2e/advanced.spec.ts +109 -0
  25. package/e2e/argentgrid.spec.ts +65 -0
  26. package/e2e/benchmark.spec.ts +52 -0
  27. package/e2e/cell-renderers.spec.ts +152 -0
  28. package/e2e/debug-streaming.spec.ts +31 -0
  29. package/e2e/dnd.spec.ts +73 -0
  30. package/e2e/screenshots.spec.ts +52 -0
  31. package/e2e/theming.spec.ts +35 -0
  32. package/e2e/visual.spec.ts +112 -0
  33. package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
  34. package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
  35. package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
  36. package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
  37. package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
  38. package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
  39. package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
  40. package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
  41. package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
  42. package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
  43. package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
  44. package/package.json +21 -7
  45. package/plan.md +56 -28
  46. package/playwright.config.ts +38 -0
  47. package/setup-vitest.ts +10 -13
  48. package/src/lib/argent-grid.module.ts +10 -12
  49. package/src/lib/components/argent-grid.component.css +281 -321
  50. package/src/lib/components/argent-grid.component.html +295 -207
  51. package/src/lib/components/argent-grid.component.spec.ts +120 -160
  52. package/src/lib/components/argent-grid.component.ts +1193 -290
  53. package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
  54. package/src/lib/components/argent-grid.selection.spec.ts +132 -0
  55. package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
  56. package/src/lib/components/set-filter/set-filter.component.ts +307 -0
  57. package/src/lib/directives/ag-grid-compatibility.directive.ts +16 -26
  58. package/src/lib/directives/click-outside.directive.ts +19 -0
  59. package/src/lib/rendering/canvas-renderer.spec.ts +513 -0
  60. package/src/lib/rendering/canvas-renderer.ts +456 -452
  61. package/src/lib/rendering/live-data-handler.ts +110 -0
  62. package/src/lib/rendering/live-data-optimizations.ts +133 -0
  63. package/src/lib/rendering/render/blit.spec.ts +16 -27
  64. package/src/lib/rendering/render/blit.ts +48 -36
  65. package/src/lib/rendering/render/cells.spec.ts +132 -0
  66. package/src/lib/rendering/render/cells.ts +167 -28
  67. package/src/lib/rendering/render/column-utils.ts +95 -0
  68. package/src/lib/rendering/render/hit-test.ts +50 -0
  69. package/src/lib/rendering/render/index.ts +88 -76
  70. package/src/lib/rendering/render/lines.ts +53 -47
  71. package/src/lib/rendering/render/primitives.ts +423 -0
  72. package/src/lib/rendering/render/theme.spec.ts +8 -12
  73. package/src/lib/rendering/render/theme.ts +7 -10
  74. package/src/lib/rendering/render/types.ts +3 -2
  75. package/src/lib/rendering/render/walk.spec.ts +35 -38
  76. package/src/lib/rendering/render/walk.ts +94 -64
  77. package/src/lib/rendering/utils/damage-tracker.spec.ts +8 -7
  78. package/src/lib/rendering/utils/damage-tracker.ts +6 -18
  79. package/src/lib/rendering/utils/index.ts +1 -1
  80. package/src/lib/services/grid.service.set-filter.spec.ts +219 -0
  81. package/src/lib/services/grid.service.spec.ts +1241 -201
  82. package/src/lib/services/grid.service.ts +1204 -235
  83. package/src/lib/themes/parts/color-schemes.ts +132 -0
  84. package/src/lib/themes/parts/icon-sets.ts +258 -0
  85. package/src/lib/themes/theme-builder.ts +347 -0
  86. package/src/lib/themes/theme-quartz.ts +72 -0
  87. package/src/lib/themes/types.ts +238 -0
  88. package/src/lib/types/ag-grid-types.ts +573 -14
  89. package/src/public-api.ts +39 -9
  90. package/src/stories/Advanced.stories.ts +249 -0
  91. package/src/stories/ArgentGrid.stories.ts +301 -0
  92. package/src/stories/Benchmark.stories.ts +76 -0
  93. package/src/stories/CellRenderers.stories.ts +395 -0
  94. package/src/stories/Filtering.stories.ts +292 -0
  95. package/src/stories/Grouping.stories.ts +290 -0
  96. package/src/stories/Streaming.stories.ts +57 -0
  97. package/src/stories/Theming.stories.ts +137 -0
  98. package/src/stories/Tooltips.stories.ts +381 -0
  99. package/src/stories/benchmark-wrapper.component.ts +355 -0
  100. package/src/stories/story-utils.ts +88 -0
  101. package/src/stories/streaming-wrapper.component.ts +441 -0
  102. package/tsconfig.json +1 -0
  103. package/tsconfig.storybook.json +10 -0
  104. package/vitest.config.ts +9 -9
  105. package/demo-app/README.md +0 -70
  106. package/demo-app/angular.json +0 -78
  107. package/demo-app/e2e/benchmark.spec.ts +0 -53
  108. package/demo-app/e2e/demo-page.spec.ts +0 -77
  109. package/demo-app/e2e/grid-features.spec.ts +0 -269
  110. package/demo-app/package-lock.json +0 -14023
  111. package/demo-app/package.json +0 -36
  112. package/demo-app/playwright-test-menu.js +0 -19
  113. package/demo-app/playwright.config.ts +0 -23
  114. package/demo-app/src/app/app.component.ts +0 -10
  115. package/demo-app/src/app/app.config.ts +0 -13
  116. package/demo-app/src/app/app.routes.ts +0 -7
  117. package/demo-app/src/app/demo-page/demo-page.component.css +0 -313
  118. package/demo-app/src/app/demo-page/demo-page.component.html +0 -124
  119. package/demo-app/src/app/demo-page/demo-page.component.ts +0 -366
  120. package/demo-app/src/index.html +0 -19
  121. package/demo-app/src/main.ts +0 -6
  122. package/demo-app/tsconfig.json +0 -31
@@ -1,25 +1,59 @@
1
- import { Component, Input, Output, EventEmitter, OnInit, OnDestroy, ElementRef, ViewChild, ChangeDetectionStrategy, AfterViewInit, OnChanges, SimpleChanges, ChangeDetectorRef, Inject } from '@angular/core';
2
- import { CdkDragDrop, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
3
- import { GridApi, GridOptions, ColDef, ColGroupDef, IRowNode, Column, CellRange, DefaultMenuItem, MenuItemDef, GetContextMenuItemsParams } from '../types/ag-grid-types';
1
+ import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
2
+ import {
3
+ AfterViewInit,
4
+ ChangeDetectionStrategy,
5
+ ChangeDetectorRef,
6
+ Component,
7
+ ElementRef,
8
+ EventEmitter,
9
+ Inject,
10
+ Input,
11
+ OnChanges,
12
+ OnDestroy,
13
+ OnInit,
14
+ Output,
15
+ SimpleChanges,
16
+ ViewChild,
17
+ } from '@angular/core';
4
18
  import { Subject } from 'rxjs';
5
19
  import { takeUntil } from 'rxjs/operators';
6
- import { GridService } from '../services/grid.service';
7
20
  import { CanvasRenderer } from '../rendering/canvas-renderer';
21
+ import { isColumnVisible } from '../rendering/render/column-utils';
22
+ import { GridService } from '../services/grid.service';
23
+ import { applyThemeCSSVariables, convertThemeToGridTheme } from '../themes/theme-builder';
24
+ import {
25
+ CellRange,
26
+ ColDef,
27
+ ColGroupDef,
28
+ Column,
29
+ ColumnGroup,
30
+ DefaultMenuItem,
31
+ GetContextMenuItemsParams,
32
+ GridApi,
33
+ GridOptions,
34
+ IRowNode,
35
+ MenuItemDef,
36
+ RowSelectionOptions,
37
+ } from '../types/ag-grid-types';
8
38
 
9
39
  @Component({
10
40
  selector: 'argent-grid',
11
41
  templateUrl: './argent-grid.component.html',
12
42
  styleUrls: ['./argent-grid.component.css'],
13
- changeDetection: ChangeDetectionStrategy.OnPush
43
+ changeDetection: ChangeDetectionStrategy.OnPush,
14
44
  })
15
- export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, AfterViewInit, OnChanges {
45
+ export class ArgentGridComponent<TData = any>
46
+ implements OnInit, OnDestroy, AfterViewInit, OnChanges
47
+ {
16
48
  @Input() columnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null = null;
17
49
  @Input() rowData: TData[] | null = null;
18
50
  @Input() gridOptions: GridOptions<TData> | null = null;
51
+ @Input() theme: any;
19
52
  @Input() height = '500px';
20
53
  @Input() width = '100%';
21
- @Input() rowHeight = 32;
22
-
54
+ @Input() rowHeight?: number;
55
+ @Input() rowSelection: RowSelectionOptions | 'single' | 'multiple' | undefined;
56
+
23
57
  @Output() gridReady = new EventEmitter<GridApi<TData>>();
24
58
  @Output() rowClicked = new EventEmitter<{ data: TData; node: IRowNode<TData> }>();
25
59
  @Output() selectionChanged = new EventEmitter<IRowNode<TData>[]>();
@@ -34,16 +68,31 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
34
68
  showOverlay = false;
35
69
  private viewportHeight = 500;
36
70
 
71
+ /**
72
+ * Returns the current effective row height, prioritizing grid options, then the input property, and defaulting to 32.
73
+ */
74
+ get effectiveRowHeight(): number {
75
+ return this.gridApi?.getGridOption('rowHeight') || this.rowHeight || 32;
76
+ }
77
+
78
+ /**
79
+ * Returns the current effective header height, prioritizing grid options, then defaulting to effectiveRowHeight.
80
+ */
81
+ get effectiveHeaderHeight(): number {
82
+ return this.gridApi?.getGridOption('headerHeight') || this.effectiveRowHeight;
83
+ }
84
+
37
85
  get totalHeight(): number {
38
86
  if (this.gridApi) return this.gridApi.getTotalHeight();
39
- return (this.rowData?.length || 0) * this.rowHeight;
87
+ return (this.rowData?.length || 0) * (this.rowHeight || 32);
40
88
  }
41
89
 
42
90
  get totalWidth(): number {
43
91
  if (!this.gridApi) return 0;
44
- return this.gridApi.getAllColumns()
45
- .filter(col => col.visible)
46
- .reduce((sum, col) => sum + col.width, 0);
92
+ return this.gridApi
93
+ .getAllColumns()
94
+ .filter((col) => isColumnVisible(col))
95
+ .reduce((sum, col) => sum + Math.floor(col.width || 150), 0);
47
96
  }
48
97
 
49
98
  // Selection state
@@ -52,6 +101,14 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
52
101
  isAllSelected = false;
53
102
  isIndeterminateSelection = false;
54
103
 
104
+ hasCheckboxSelection(col: Column): boolean {
105
+ return col.colId === 'ag-Grid-SelectionColumn';
106
+ }
107
+
108
+ hasHeaderCheckbox(col: Column): boolean {
109
+ return !!col.headerCheckboxSelection;
110
+ }
111
+
55
112
  trackByColumn(index: number, col: Column | ColDef<TData> | ColGroupDef<TData>): string {
56
113
  return (col as any).colId || (col as any).field?.toString() || index.toString();
57
114
  }
@@ -70,32 +127,57 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
70
127
  // Resizing state
71
128
  isResizing = false;
72
129
  resizeColumn: Column | null = null;
130
+ resizeItem: Column | ColumnGroup | null = null;
73
131
  private resizeStartX = 0;
74
132
  private resizeStartWidth = 0;
75
133
 
76
134
  // Range Selection state
77
135
  isRangeSelecting = false;
78
- private rangeStartCell: { rowIndex: number, colId: string } | null = null;
136
+ private rangeStartCell: { rowIndex: number; colId: string } | null = null;
79
137
 
80
138
  // Side Bar state
81
139
  sideBarVisible = false;
82
140
  activeToolPanel: 'columns' | 'filters' | null = null;
83
141
 
142
+ // Row Group Panel state
143
+ rowGroupPanelShow: 'always' | 'onlyWhenGrouping' | 'never' = 'never';
144
+ rowGroupColumns: Column[] = [];
145
+
84
146
  // Context Menu state
85
147
  activeContextMenu = false;
86
148
  contextMenuPosition = { x: 0, y: 0 };
87
149
  contextMenuItems: MenuItemDef[] = [];
88
- private contextMenuCell: { rowNode: IRowNode<TData>, column: Column } | null = null;
150
+ private contextMenuCell: { rowNode: IRowNode<TData>; column: Column } | null = null;
151
+
152
+ // Tooltip state
153
+ tooltipVisible = false;
154
+ tooltipText = '';
155
+ tooltipPosition = { x: 0, y: 0 };
156
+ private _tooltipTimer: any = null;
157
+
158
+ // Set Filter
159
+ activeSetFilter = false;
160
+ setFilterPosition = { x: 0, y: 0 };
161
+ setFilterValues: any[] = [];
162
+ setFilterSelectedValues: any[] | null = null;
163
+ setFilterValueFormatter?: (value: any) => string;
164
+ private activeSetFilterColumn: Column | null = null;
89
165
  private initialColumnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null = null;
90
166
 
91
- private gridApi!: GridApi<TData>;
167
+ public gridApi!: GridApi<TData>;
168
+ public isColumnVisible = isColumnVisible;
169
+ public Math = Math;
170
+ public scrollbarWidth = 0;
92
171
  private canvasRenderer!: CanvasRenderer;
93
172
  private destroy$ = new Subject<void>();
94
173
  private gridService = new GridService<TData>();
95
174
  private horizontalScrollListener?: (e: Event) => void;
96
175
  private resizeObserver?: ResizeObserver;
97
176
 
98
- constructor(@Inject(ChangeDetectorRef) private cdr: ChangeDetectorRef) {}
177
+ constructor(
178
+ @Inject(ChangeDetectorRef) private _cdr: ChangeDetectorRef,
179
+ private _elementRef: ElementRef<HTMLElement>
180
+ ) {}
99
181
 
100
182
  ngOnInit(): void {
101
183
  this.initialColumnDefs = this.columnDefs ? JSON.parse(JSON.stringify(this.columnDefs)) : null;
@@ -104,30 +186,71 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
104
186
 
105
187
  ngOnChanges(changes: SimpleChanges): void {
106
188
  // Handle rowData changes after initialization
107
- if (changes['rowData'] && !changes['rowData'].firstChange) {
108
- this.onRowDataChanged(changes['rowData'].currentValue);
189
+ if (changes.rowData && !changes.rowData.firstChange) {
190
+ this.onRowDataChanged(changes.rowData.currentValue);
109
191
  }
110
192
 
111
193
  // Handle columnDefs changes
112
- if (changes['columnDefs'] && !changes['columnDefs'].firstChange) {
113
- this.onColumnDefsChanged(changes['columnDefs'].currentValue);
194
+ if (changes.columnDefs && !changes.columnDefs.firstChange) {
195
+ this.onColumnDefsChanged(changes.columnDefs.currentValue);
114
196
  }
115
197
 
116
198
  // Handle gridOptions changes
117
- if (changes['gridOptions'] && !changes['gridOptions'].firstChange) {
118
- this.onGridOptionsChanged(changes['gridOptions'].currentValue);
199
+ if (changes.gridOptions && !changes.gridOptions.firstChange) {
200
+ this.onGridOptionsChanged(changes.gridOptions.currentValue);
201
+ }
202
+
203
+ // Handle rowSelection changes
204
+ if (changes.rowSelection && !changes.rowSelection.firstChange) {
205
+ if (this.gridApi) {
206
+ this.gridApi.setGridOption('rowSelection', changes.rowSelection.currentValue);
207
+ }
208
+ }
209
+
210
+ // Handle theme changes
211
+ if (changes.theme && !changes.theme.firstChange) {
212
+ // Apply theme CSS variables to the grid container
213
+ if (changes.theme.currentValue) {
214
+ applyThemeCSSVariables(changes.theme.currentValue, this._elementRef.nativeElement);
215
+ }
216
+
217
+ // Update canvas renderer theme if it's initialized
218
+ if (this.canvasRenderer) {
219
+ const convertedTheme = changes.theme.currentValue
220
+ ? convertThemeToGridTheme(changes.theme.currentValue)
221
+ : undefined;
222
+ this.canvasRenderer.setTheme(convertedTheme);
223
+
224
+ // Sync rowHeight and headerHeight to GridApi if provided by theme and NOT explicitly overridden by user
225
+ if (this.gridApi) {
226
+ if (convertedTheme?.rowHeight && this.rowHeight === undefined) {
227
+ this.gridApi.setGridOption('rowHeight', convertedTheme.rowHeight);
228
+ }
229
+ if (convertedTheme?.headerHeight) {
230
+ this.gridApi.setGridOption('headerHeight', convertedTheme.headerHeight);
231
+ }
232
+ }
233
+ }
119
234
  }
120
235
  }
121
236
 
122
237
  ngAfterViewInit(): void {
123
238
  // Setup canvas renderer after view is initialized
124
239
  if (this.canvasRef && !this.canvasRenderer) {
240
+ // Convert theme from ThemeBuilder format to internal GridTheme format
241
+ const convertedTheme = this.theme ? convertThemeToGridTheme(this.theme) : undefined;
242
+
243
+ // Apply theme CSS variables to the grid container
244
+ if (this.theme) {
245
+ applyThemeCSSVariables(this.theme, this._elementRef.nativeElement);
246
+ }
247
+
125
248
  this.canvasRenderer = new CanvasRenderer(
126
249
  this.canvasRef.nativeElement,
127
250
  this.gridApi,
128
- this.rowHeight
251
+ this.effectiveRowHeight,
252
+ convertedTheme
129
253
  );
130
-
131
254
  // Wire up cell editing callback
132
255
  this.canvasRenderer.onCellDoubleClick = (rowIndex, colId) => {
133
256
  this.startEditing(rowIndex, colId);
@@ -141,28 +264,28 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
141
264
  // Range Selection Logic
142
265
  this.canvasRenderer.onMouseDown = (event, rowIndex, colId) => {
143
266
  if (event.button !== 0 || !colId || rowIndex === -1) return;
144
-
267
+
145
268
  const rangeSelectionEnabled = this.gridApi?.getGridOption('enableRangeSelection');
146
269
  if (!rangeSelectionEnabled) return;
147
270
 
148
271
  this.isRangeSelecting = true;
149
272
  this.rangeStartCell = { rowIndex, colId };
150
-
273
+
151
274
  // Clear previous selection if not holding Shift/Ctrl
152
275
  if (!event.shiftKey && !event.ctrlKey && !event.metaKey) {
153
276
  this.gridApi?.clearRangeSelection();
154
277
  }
155
278
  };
156
279
 
157
- this.canvasRenderer.onMouseMove = (event, rowIndex, colId) => {
280
+ this.canvasRenderer.onMouseMove = (_event, rowIndex, colId) => {
158
281
  if (!this.isRangeSelecting || !this.rangeStartCell || !colId || rowIndex === -1) return;
159
282
 
160
283
  const start = this.rangeStartCell;
161
284
  const end = { rowIndex, colId };
162
285
 
163
286
  const columns = this.canvasRenderer.getAllColumns();
164
- const startColIdx = columns.findIndex(c => c.colId === start.colId);
165
- const endColIdx = columns.findIndex(c => c.colId === end.colId);
287
+ const startColIdx = columns.findIndex((c) => c.colId === start.colId);
288
+ const endColIdx = columns.findIndex((c) => c.colId === end.colId);
166
289
 
167
290
  if (startColIdx === -1 || endColIdx === -1) return;
168
291
 
@@ -171,7 +294,10 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
171
294
  endRow: Math.max(start.rowIndex, end.rowIndex),
172
295
  startColumn: columns[Math.min(startColIdx, endColIdx)].colId,
173
296
  endColumn: columns[Math.max(startColIdx, endColIdx)].colId,
174
- columns: columns.slice(Math.min(startColIdx, endColIdx), Math.max(startColIdx, endColIdx) + 1)
297
+ columns: columns.slice(
298
+ Math.min(startColIdx, endColIdx),
299
+ Math.max(startColIdx, endColIdx) + 1
300
+ ),
175
301
  };
176
302
 
177
303
  this.gridApi?.addCellRange(range);
@@ -186,34 +312,72 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
186
312
  if (this.viewportRef) {
187
313
  const rect = this.viewportRef.nativeElement.getBoundingClientRect();
188
314
  this.viewportHeight = rect.height || 500;
189
- this.canvasRenderer?.setViewportDimensions(rect.width, this.viewportHeight);
190
- this.canvasRenderer?.setTotalRowCount(this.rowData?.length || 0);
315
+ this.canvasRenderer?.setViewportDimensions(
316
+ rect.width,
317
+ this.viewportHeight,
318
+ this.scrollbarWidth
319
+ );
320
+
321
+ const updateScrollbar = () => {
322
+ const viewport = this.viewportRef?.nativeElement;
323
+ if (!viewport) return;
324
+ const newWidth = viewport.offsetWidth - viewport.clientWidth;
325
+ if (this.scrollbarWidth !== newWidth) {
326
+ this.scrollbarWidth = newWidth;
327
+ this._cdr.detectChanges();
328
+ }
329
+ };
191
330
 
192
331
  // Synchronize horizontal scroll with DOM header
193
332
  this.horizontalScrollListener = () => {
333
+ const viewport = this.viewportRef?.nativeElement;
334
+ if (!viewport) return;
335
+
336
+ updateScrollbar();
337
+
194
338
  if (this.headerScrollableRef) {
195
- this.headerScrollableRef.nativeElement.scrollLeft = this.viewportRef.nativeElement.scrollLeft;
339
+ this.headerScrollableRef.nativeElement.scrollLeft = viewport.scrollLeft;
196
340
  }
197
341
  if (this.headerScrollableFilterRef) {
198
- this.headerScrollableFilterRef.nativeElement.scrollLeft = this.viewportRef.nativeElement.scrollLeft;
342
+ this.headerScrollableFilterRef.nativeElement.scrollLeft = viewport.scrollLeft;
199
343
  }
200
344
  };
201
-
202
- this.viewportRef.nativeElement.addEventListener('scroll', this.horizontalScrollListener, { passive: true });
345
+
346
+ this.viewportRef.nativeElement.addEventListener('scroll', this.horizontalScrollListener, {
347
+ passive: true,
348
+ });
203
349
 
204
350
  // Add ResizeObserver to handle sidebar toggling and other size changes
205
351
  if (typeof ResizeObserver !== 'undefined') {
206
- this.resizeObserver = new ResizeObserver(entries => {
352
+ let lastWidth = 0;
353
+ let lastHeight = 0;
354
+ this.resizeObserver = new ResizeObserver((entries) => {
207
355
  for (const entry of entries) {
208
356
  const { width, height } = entry.contentRect;
357
+ if (Math.abs(width - lastWidth) < 1 && Math.abs(height - lastHeight) < 1) continue;
358
+
359
+ lastWidth = width;
360
+ lastHeight = height;
209
361
  this.viewportHeight = height;
210
- this.canvasRenderer?.setViewportDimensions(width, height);
211
- this.canvasRenderer?.render();
212
- this.cdr.detectChanges();
362
+
363
+ // Only update scrollbar if dimensions actually changed
364
+ updateScrollbar();
365
+
366
+ this.canvasRenderer?.setViewportDimensions(width, height, this.scrollbarWidth);
367
+ // setViewportDimensions → updateCanvasSize already schedules a render.
368
+ this._cdr.detectChanges();
213
369
  }
214
370
  });
215
371
  this.resizeObserver.observe(this.viewportRef.nativeElement);
216
372
  }
373
+
374
+ // Initial calculation
375
+ setTimeout(() => {
376
+ updateScrollbar();
377
+ if (this.canvasRenderer) {
378
+ this.canvasRenderer.render();
379
+ }
380
+ });
217
381
  }
218
382
  }
219
383
 
@@ -232,27 +396,78 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
232
396
 
233
397
  this.gridApi?.destroy();
234
398
  this.canvasRenderer?.destroy();
399
+ this.onCanvasMouseLeave();
235
400
  }
236
401
 
237
402
  private initializeGrid(): void {
403
+ // Merge individual inputs into grid options if provided
404
+ const options = { ...this.gridOptions };
405
+ if (this.rowSelection) {
406
+ options.rowSelection = this.rowSelection;
407
+ }
408
+
409
+ // Prioritize explicit rowHeight input if provided
410
+ if (this.rowHeight !== undefined) {
411
+ options.rowHeight = this.rowHeight;
412
+ } else if (this.theme) {
413
+ // If no explicit rowHeight, but theme is provided, use theme's rowHeight
414
+ const convertedTheme = convertThemeToGridTheme(this.theme);
415
+ if (convertedTheme.rowHeight) {
416
+ options.rowHeight = convertedTheme.rowHeight;
417
+ }
418
+ if (convertedTheme.headerHeight && options.headerHeight === undefined) {
419
+ options.headerHeight = convertedTheme.headerHeight;
420
+ }
421
+ }
422
+
238
423
  // Initialize grid API
239
- this.gridApi = this.gridService.createApi(this.columnDefs, this.rowData, this.gridOptions);
424
+ this.gridApi = this.gridService.createApi(this.columnDefs, this.rowData, options);
425
+
426
+ // Initial state sync
427
+ this.rowGroupPanelShow = this.gridApi.getGridOption('rowGroupPanelShow') || 'never';
428
+ this.updateRowGroupColumns();
240
429
 
241
430
  // Listen for grid state changes from API (filters, sorts, options)
242
- this.gridService.gridStateChanged$
243
- .pipe(takeUntil(this.destroy$))
244
- .subscribe((event) => {
245
- if (event.type === 'optionChanged' && event.key === 'sideBar') {
246
- this.sideBarVisible = !!event.value;
431
+ this.gridService.gridStateChanged$.pipe(takeUntil(this.destroy$)).subscribe((event) => {
432
+ if (event.type === 'optionChanged' && event.key === 'sideBar') {
433
+ this.sideBarVisible = !!event.value;
434
+ }
435
+ if (event.type === 'optionChanged' && event.key === 'rowGroupPanelShow') {
436
+ this.rowGroupPanelShow = event.value || 'never';
437
+ }
438
+ if (event.type === 'selectionChanged') {
439
+ this.updateSelectionState();
440
+
441
+ // Mark all rows as potentially dirty for selection change to ensure canvas redraws
442
+ // In a more optimized version, we'd only mark specific rows.
443
+ if (this.canvasRenderer) {
444
+ this.canvasRenderer.render(); // This calls markAllDirty and schedules render
247
445
  }
446
+ } else if (event.type === 'columnsChanged' || event.type === 'columnGroupExpanded') {
447
+ this.updateRowGroupColumns();
248
448
  this.canvasRenderer?.render();
249
- this.cdr.detectChanges();
250
- });
449
+ } else if (event.type === 'transactionApplied') {
450
+ // Efficient rendering: only mark changed rows as dirty instead of full redraw.
451
+ // Callers are responsible for throttling applyTransaction frequency (e.g. via RxJS).
452
+ const changedIndices = (event as any).changedRowIndices as number[] | undefined;
453
+ if (changedIndices && changedIndices.length > 0 && this.canvasRenderer) {
454
+ for (const rowIndex of changedIndices) {
455
+ this.canvasRenderer.invalidateRow(rowIndex);
456
+ }
457
+ } else {
458
+ this.canvasRenderer?.render();
459
+ }
460
+ } else {
461
+ // All other state changes (sort, filter, rangeSelection, etc.) go through the
462
+ // rAF-coalesced scheduler. Multiple rapid events (e.g. rangeSelectionChanged
463
+ // on every mousemove) collapse into at most one pending frame.
464
+ this.canvasRenderer?.render();
465
+ }
466
+ this._cdr.detectChanges();
467
+ });
251
468
 
252
- // Check if any column has checkbox selection
253
- this.showSelectionColumn = this.columnDefs?.some(col =>
254
- !('children' in col) && col.checkboxSelection
255
- ) || false;
469
+ // Selection column is now handled within the data columns
470
+ this.showSelectionColumn = false;
256
471
 
257
472
  // Canvas renderer will be initialized in ngAfterViewInit
258
473
 
@@ -277,15 +492,16 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
277
492
 
278
493
  if (this.gridApi) {
279
494
  this.gridApi.setRowData(newData || []);
280
- this.canvasRenderer?.setTotalRowCount(newData?.length || 0);
281
- this.canvasRenderer?.render();
495
+ if (this.canvasRenderer) {
496
+ this.canvasRenderer.render();
497
+ }
282
498
  }
283
499
 
284
500
  this.showOverlay = !newData || newData.length === 0;
285
501
  this.updateSelectionState();
286
502
 
287
503
  // Trigger change detection with OnPush
288
- this.cdr.detectChanges();
504
+ this._cdr.detectChanges();
289
505
  }
290
506
 
291
507
  private onColumnDefsChanged(newColumnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null): void {
@@ -295,52 +511,239 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
295
511
  this.gridApi.setColumnDefs(newColumnDefs);
296
512
  this.canvasRenderer?.render();
297
513
  }
298
-
299
- this.cdr.detectChanges();
514
+
515
+ this._cdr.detectChanges();
300
516
  }
301
517
 
302
518
  private onGridOptionsChanged(newOptions: GridOptions<TData> | null): void {
303
519
  this.gridOptions = newOptions;
304
520
  if (this.gridApi && newOptions) {
305
521
  // Update all options in the API
306
- Object.keys(newOptions).forEach(key => {
522
+ Object.keys(newOptions).forEach((key) => {
307
523
  this.gridApi.setGridOption(key as any, (newOptions as any)[key]);
308
524
  });
525
+
526
+ if (newOptions.rowGroupPanelShow) {
527
+ this.rowGroupPanelShow = newOptions.rowGroupPanelShow;
528
+ }
529
+
309
530
  this.canvasRenderer?.render();
310
531
  }
311
- this.cdr.detectChanges();
532
+ this._cdr.detectChanges();
533
+ }
534
+
535
+ getHeaderRows(): (Column | ColumnGroup)[][] {
536
+ if (!this.gridApi) return [];
537
+ return this.gridApi.getHeaderRows();
538
+ }
539
+
540
+ getPinnedLeftItems(row: (Column | ColumnGroup)[]): (Column | ColumnGroup)[] {
541
+ return row.filter((item) => item.pinned === 'left' && isColumnVisible(item));
542
+ }
543
+
544
+ getPinnedRightItems(row: (Column | ColumnGroup)[]): (Column | ColumnGroup)[] {
545
+ return row.filter((item) => item.pinned === 'right' && isColumnVisible(item));
546
+ }
547
+
548
+ getNonPinnedItems(row: (Column | ColumnGroup)[]): (Column | ColumnGroup)[] {
549
+ return row.filter((item) => !item.pinned && isColumnVisible(item));
550
+ }
551
+
552
+ getItemWidth(item: Column | ColumnGroup): number {
553
+ if ('children' in item) {
554
+ return item.children.reduce((sum, child) => {
555
+ if (isColumnVisible(child)) {
556
+ return sum + this.getItemWidth(child);
557
+ }
558
+ return sum;
559
+ }, 0);
560
+ }
561
+ return Math.floor(item.width || 150);
562
+ }
563
+
564
+ getItemRowSpan(item: Column | ColumnGroup, rowIndex: number): number {
565
+ if ('children' in item) {
566
+ return 1;
567
+ }
568
+ // Leaf node spans until the bottom
569
+ const totalRows = this.gridApi.getHeaderDepth();
570
+ return totalRows - rowIndex;
571
+ }
572
+
573
+ isColumnGroup(item: Column | ColumnGroup): item is ColumnGroup {
574
+ return 'children' in item;
575
+ }
576
+
577
+ trackByHeaderItem(
578
+ index: number,
579
+ entry: { item: Column | ColumnGroup; rowIndex: number }
580
+ ): string {
581
+ const item = entry.item;
582
+ return 'groupId' in item ? item.groupId : item.colId || index.toString();
583
+ }
584
+
585
+ getItemColSpan(item: Column | ColumnGroup): number {
586
+ if ('children' in item) {
587
+ return item.children.reduce((sum, child) => {
588
+ return sum + this.getItemColSpan(child);
589
+ }, 0);
590
+ }
591
+ return 1;
592
+ }
593
+
594
+ getScrollableHeaderWidth(): number {
595
+ return this.getNonPinnedColumns().reduce((sum, col) => sum + Math.floor(col.width || 150), 0);
312
596
  }
313
-
597
+
598
+ getGridTemplateColumns(section: 'left' | 'right' | 'none'): string {
599
+ if (!this.gridApi) return '';
600
+ const allCols = this.gridApi.getAllColumns();
601
+ const sectionCols = allCols.filter((c) => {
602
+ if (section === 'left') return c.pinned === 'left';
603
+ if (section === 'right') return c.pinned === 'right';
604
+ return !c.pinned;
605
+ });
606
+
607
+ if (sectionCols.length === 0) return '';
608
+
609
+ const indices = sectionCols.map((c) => c.colIndex || 0);
610
+ const minIndex = Math.min(...indices);
611
+ const maxIndex = Math.max(...indices);
612
+
613
+ const widths = new Array(maxIndex - minIndex + 1).fill('0px');
614
+ sectionCols.forEach((c) => {
615
+ if (isColumnVisible(c)) {
616
+ widths[(c.colIndex || 0) - minIndex] = `${Math.floor(c.width || 150)}px`;
617
+ }
618
+ });
619
+
620
+ return widths.join(' ');
621
+ }
622
+
623
+ getColGridIndex(item: Column | ColumnGroup, section: 'left' | 'right' | 'none'): number {
624
+ if (!this.gridApi) return 1;
625
+ const allCols = this.gridApi.getAllColumns();
626
+ const sectionCols = allCols.filter((c) => {
627
+ if (section === 'left') return c.pinned === 'left';
628
+ if (section === 'right') return c.pinned === 'right';
629
+ return !c.pinned;
630
+ });
631
+ if (sectionCols.length === 0) return 1;
632
+ const minIndex = Math.min(...sectionCols.map((c) => c.colIndex || 0));
633
+
634
+ return (item.colIndex || 0) - minIndex + 1;
635
+ }
636
+
637
+ getScrollableColIndex(item: Column | ColumnGroup): number {
638
+ return this.getColGridIndex(item, 'none');
639
+ }
640
+
641
+ getRightPinnedColIndex(item: Column | ColumnGroup): number {
642
+ return this.getColGridIndex(item, 'right');
643
+ }
644
+
645
+ getLeftPinnedColIndex(item: Column | ColumnGroup): number {
646
+ return this.getColGridIndex(item, 'left');
647
+ }
648
+
649
+ getSectionHeaderItems(
650
+ section: 'left' | 'right' | 'none'
651
+ ): { item: Column | ColumnGroup; rowIndex: number }[] {
652
+ const items: { item: Column | ColumnGroup; rowIndex: number }[] = [];
653
+ const rows = this.getHeaderRows();
654
+ rows.forEach((row, i) => {
655
+ let rowItems: (Column | ColumnGroup)[] = [];
656
+ if (section === 'left') rowItems = this.getPinnedLeftItems(row);
657
+ else if (section === 'right') rowItems = this.getPinnedRightItems(row);
658
+ else rowItems = this.getNonPinnedItems(row);
659
+
660
+ rowItems.forEach((item) => items.push({ item, rowIndex: i }));
661
+ });
662
+ return items;
663
+ }
664
+
665
+ hasExpansionToggle(item: ColumnGroup): boolean {
666
+ return item.children.some(
667
+ (child) => child.columnGroupShow === 'open' || child.columnGroupShow === 'closed'
668
+ );
669
+ }
670
+
671
+ isRowGroupPanelVisible(): boolean {
672
+ const show = this.rowGroupPanelShow;
673
+ if (show === 'always') return true;
674
+ if (show === 'onlyWhenGrouping') {
675
+ return this.rowGroupColumns.length > 0;
676
+ }
677
+ return false;
678
+ }
679
+
680
+ trackByRowGroup(index: number, col: Column): string {
681
+ return col.colId || index.toString();
682
+ }
683
+
684
+ private updateRowGroupColumns(): void {
685
+ if (!this.gridApi) {
686
+ this.rowGroupColumns = [];
687
+ return;
688
+ }
689
+ const groupColIds = this.gridApi.getRowGroupColumns();
690
+ this.rowGroupColumns = this.gridApi
691
+ .getAllColumns()
692
+ .filter((col) => groupColIds.includes(col.colId));
693
+ }
694
+
695
+ getRowGroupColumns(): Column[] {
696
+ return this.rowGroupColumns;
697
+ }
698
+
699
+ onRowGroupDropped(event: CdkDragDrop<any[]>): void {
700
+ const col = event.item.data as Column;
701
+ if (col?.colId && col.colId !== 'ag-Grid-SelectionColumn') {
702
+ this.gridApi.addRowGroupColumn(col.colId);
703
+ }
704
+ }
705
+ removeRowGroup(col: Column): void {
706
+ this.gridApi.removeRowGroupColumn(col.colId);
707
+ }
708
+
709
+ toggleGroup(item: ColumnGroup, event: MouseEvent): void {
710
+ event.stopPropagation();
711
+ this.gridApi.toggleColumnGroup(item.groupId, !item.expanded);
712
+ }
713
+
314
714
  getColumnWidth(col: Column | ColDef<TData> | ColGroupDef<TData>): number {
315
715
  if ('children' in col) {
316
716
  // Column group - sum children widths
317
717
  return col.children.reduce((sum, child) => sum + this.getColumnWidth(child), 0);
318
718
  }
319
- return col.width || 150;
719
+ return Math.floor(col.width || 150);
320
720
  }
321
721
 
322
722
  getLeftPinnedColumns(): Column[] {
323
723
  if (!this.gridApi) return [];
324
- return this.gridApi.getAllColumns().filter(col => {
325
- return col.visible && col.pinned === 'left';
724
+ return this.gridApi.getAllColumns().filter((col) => {
725
+ return isColumnVisible(col) && col.pinned === 'left';
326
726
  });
327
727
  }
328
728
 
329
729
  getRightPinnedColumns(): Column[] {
330
730
  if (!this.gridApi) return [];
331
- return this.gridApi.getAllColumns().filter(col => {
332
- return col.visible && col.pinned === 'right';
731
+ return this.gridApi.getAllColumns().filter((col) => {
732
+ return isColumnVisible(col) && col.pinned === 'right';
333
733
  });
334
734
  }
335
735
 
336
736
  getNonPinnedColumns(): Column[] {
337
737
  if (!this.gridApi) return [];
338
- return this.gridApi.getAllColumns().filter(col => {
339
- return col.visible && !col.pinned;
738
+ return this.gridApi.getAllColumns().filter((col) => {
739
+ return isColumnVisible(col) && !col.pinned;
340
740
  });
341
741
  }
342
-
742
+
343
743
  isSortable(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
744
+ const colId = (col as any).colId || (col as any).field?.toString();
745
+ if (colId === 'ag-Grid-SelectionColumn') return false;
746
+
344
747
  // If it has children, it's a group and cannot be sorted directly
345
748
  if ('children' in col) return false;
346
749
 
@@ -351,34 +754,44 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
351
754
 
352
755
  // It's likely a Column object, look up its ColDef
353
756
  const colDef = this.getColumnDefForColumn(col as any);
354
- return colDef ? (colDef.sortable !== false) : true;
757
+ return colDef && this.isColDef(colDef) ? colDef.sortable !== false : true;
355
758
  }
356
-
759
+
357
760
  getHeaderName(col: Column | ColDef<TData> | ColGroupDef<TData>): string {
358
761
  if ('children' in col) {
359
762
  return col.headerName || '';
360
763
  }
764
+ if ((col as any).colId === 'ag-Grid-SelectionColumn') {
765
+ return '';
766
+ }
361
767
  return col.headerName || (col as any).field?.toString() || '';
362
768
  }
363
-
769
+
364
770
  getSortIndicator(col: Column | ColDef<TData> | ColGroupDef<TData>): string {
365
771
  if ('children' in col || !col.sort) {
366
772
  return '';
367
773
  }
368
774
  return col.sort === 'asc' ? '▲' : '▼';
369
775
  }
370
-
776
+
371
777
  onHeaderClick(col: Column | ColDef<TData> | ColGroupDef<TData>): void {
778
+ if (this.isResizing) return;
779
+
780
+ if ((col as any).colId === 'ag-Grid-SelectionColumn') {
781
+ // Selection is now handled by the checkbox directly to avoid resizing interference
782
+ return;
783
+ }
784
+
372
785
  if (!this.isSortable(col) || 'children' in col) {
373
786
  return;
374
787
  }
375
-
788
+
376
789
  // Toggle sort
377
790
  const currentSort = col.sort;
378
791
  const newSort = currentSort === 'asc' ? 'desc' : currentSort === 'desc' ? null : 'asc';
379
-
792
+
380
793
  const colId = (col as any).colId || (col as any).field?.toString() || '';
381
-
794
+
382
795
  // Update the column directly if it's a Column object
383
796
  if ('colId' in col && !(col as any).children) {
384
797
  (col as any).sort = newSort;
@@ -390,44 +803,198 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
390
803
 
391
804
  // --- Header Menu Logic ---
392
805
 
806
+ headerMenuItems: MenuItemDef[] = [];
807
+
393
808
  hasHeaderMenu(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
809
+ if ((col as any).colId === 'ag-Grid-SelectionColumn') return false;
394
810
  if ('children' in col) return false;
395
811
  const colDef = this.getColumnDefForColumn(col as any);
396
- return colDef ? colDef.suppressHeaderMenuButton !== true : true;
812
+ return colDef && this.isColDef(colDef) ? colDef.suppressHeaderMenuButton !== true : true;
813
+ }
814
+
815
+ hasHeaderFilterButton(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
816
+ if ((col as any).colId === 'ag-Grid-SelectionColumn') return false;
817
+ if ('children' in col) return false;
818
+ const colDef = this.getColumnDefForColumn(col as any);
819
+ if (!colDef || !this.isColDef(colDef)) return false;
820
+ if (!colDef.filter || colDef.suppressHeaderFilterButton === true) return false;
821
+ // Don't show when floating filters are active — they already provide quick access
822
+ if (colDef.floatingFilter) return false;
823
+ return true;
824
+ }
825
+
826
+ isColumnFiltered(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
827
+ if (!this.gridApi) return false;
828
+ const column = col as Column;
829
+ const field = column.field || column.colId;
830
+ if (!field) return false;
831
+ const filterModel = this.gridApi.getFilterModel();
832
+ return !!(filterModel as any)[field];
833
+ }
834
+
835
+ onHeaderFilterClick(event: MouseEvent, col: Column | ColDef<TData> | ColGroupDef<TData>): void {
836
+ event.stopPropagation();
837
+ const column = col as Column;
838
+ const target = event.target as HTMLElement;
839
+ // Use closest ancestor that has a bounding rect useful for positioning
840
+ const iconEl = (target.closest('.argent-grid-header-filter-icon') as HTMLElement) ?? target;
841
+ const rect = iconEl.getBoundingClientRect();
842
+ const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
843
+ const position = {
844
+ x: rect.left - containerRect.left,
845
+ y: rect.bottom - containerRect.top + 4,
846
+ };
847
+ const colDef = this.getColumnDefForColumn(column);
848
+ if (colDef && this.isColDef(colDef) && colDef.filter === 'set') {
849
+ this.openSetFilter(null, column, position);
850
+ } else {
851
+ this.openFilterPopup(null, column, position);
852
+ }
397
853
  }
398
854
 
399
855
  onHeaderMenuClick(event: MouseEvent, col: Column | ColDef<TData> | ColGroupDef<TData>): void {
400
856
  event.stopPropagation();
401
-
857
+
402
858
  if (this.activeHeaderMenu === col) {
403
859
  this.closeHeaderMenu();
404
860
  return;
405
861
  }
406
862
 
407
863
  this.activeHeaderMenu = col;
408
-
409
- // Position menu below the icon using fixed (viewport) coordinates
864
+ this.headerMenuItems = this.getHeaderMenuItems(col as Column);
865
+
866
+ // Position menu below the icon using coordinates relative to the grid container
410
867
  const target = event.target as HTMLElement;
411
868
  const rect = target.getBoundingClientRect();
412
-
413
- let x = rect.right - 200; // Align right, assuming menu width ~200px
414
- let y = rect.bottom + 4;
869
+ const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
870
+
871
+ // Align left edge of menu with left edge of icon
872
+ let x = rect.left - containerRect.left;
873
+ let y = rect.bottom - containerRect.top + 4;
874
+
875
+ // Prevent menu from going off-container bounds
876
+ const containerWidth = this._elementRef.nativeElement.offsetWidth;
877
+ const containerHeight = this._elementRef.nativeElement.offsetHeight;
415
878
 
416
- // Prevent menu from going off-screen
879
+ // Assuming menu width is up to 200px for boundary checks
880
+ if (x + 200 > containerWidth) {
881
+ x = rect.right - containerRect.left - 200; // Flip to right-aligned if it overflows
882
+ }
417
883
  if (x < 0) x = 0;
418
- if (x + 200 > window.innerWidth) x = window.innerWidth - 200;
419
- if (y + 250 > window.innerHeight) { // Assuming max menu height ~250px
420
- y = rect.top - 250; // Show above if overflows bottom
884
+
885
+ // Check if menu would overflow bottom
886
+ const estimatedHeight = this.headerMenuItems.length * 30 + 20;
887
+ if (y + estimatedHeight > containerHeight) {
888
+ y = Math.max(0, rect.top - containerRect.top - estimatedHeight);
421
889
  }
422
-
890
+
423
891
  this.headerMenuPosition = { x, y };
424
-
425
- this.cdr.detectChanges();
892
+
893
+ this._cdr.detectChanges();
894
+ }
895
+
896
+ private getHeaderMenuItems(col: Column): MenuItemDef[] {
897
+ const items: MenuItemDef[] = [];
898
+
899
+ // 1. Sort items
900
+ items.push({
901
+ name: 'Sort Ascending',
902
+ icon: '↑',
903
+ action: () => this.sortColumnMenu('asc'),
904
+ });
905
+ items.push({
906
+ name: 'Sort Descending',
907
+ icon: '↓',
908
+ action: () => this.sortColumnMenu('desc'),
909
+ });
910
+ items.push({
911
+ name: 'Clear Sort',
912
+ icon: '✕',
913
+ action: () => this.sortColumnMenu(null),
914
+ });
915
+
916
+ items.push({ name: '', action: () => {}, separator: true });
917
+
918
+ // 2. Filter items
919
+ const colDef = this.getColumnDefForColumn(col);
920
+ if (colDef && this.isColDef(colDef) && colDef.filter !== false) {
921
+ const filterType = colDef.filter || 'text';
922
+
923
+ if (filterType === 'set') {
924
+ items.push({
925
+ name: 'Filter...',
926
+ icon: 'Y',
927
+ action: () => {
928
+ this.openSetFilter(null, col, { ...this.headerMenuPosition });
929
+ this.closeHeaderMenu();
930
+ },
931
+ });
932
+ } else {
933
+ items.push({
934
+ name: 'Filter...',
935
+ icon: 'Y',
936
+ action: () => {
937
+ this.openFilterPopup(null, col, { ...this.headerMenuPosition });
938
+ this.closeHeaderMenu();
939
+ },
940
+ });
941
+ }
942
+ }
943
+
944
+ items.push({ name: '', action: () => {}, separator: true });
945
+
946
+ // 3. Pinning items
947
+ items.push({
948
+ name: 'Pin Left',
949
+ icon: '«',
950
+ action: () => this.pinColumnMenu('left'),
951
+ });
952
+ items.push({
953
+ name: 'Pin Right',
954
+ icon: '»',
955
+ action: () => this.pinColumnMenu('right'),
956
+ });
957
+ items.push({
958
+ name: 'Unpin',
959
+ icon: '↺',
960
+ action: () => this.pinColumnMenu(null),
961
+ });
962
+
963
+ items.push({ name: '', action: () => {}, separator: true });
964
+
965
+ // 4. Hide item
966
+ items.push({
967
+ name: 'Hide Column',
968
+ icon: 'ø',
969
+ action: () => this.hideColumnMenu(),
970
+ });
971
+
972
+ items.push({ name: '', action: () => {}, separator: true });
973
+
974
+ // 5. Columns panel (open sidebar)
975
+ items.push({
976
+ name: 'Columns',
977
+ icon: '☰',
978
+ action: () => this.openColumnsPanel(),
979
+ });
980
+
981
+ return items;
982
+ }
983
+
984
+ public clearColumnFilter(col: Column): void {
985
+ const field = col.field;
986
+ if (!field || !this.gridApi) return;
987
+
988
+ const currentModel = this.gridApi.getFilterModel();
989
+ delete (currentModel as any)[field];
990
+ this.gridApi.setFilterModel(currentModel);
991
+ this.closeHeaderMenu();
992
+ this.closeFilterPopup();
426
993
  }
427
994
 
428
995
  closeHeaderMenu(): void {
429
996
  this.activeHeaderMenu = null;
430
- this.cdr.detectChanges();
997
+ this._cdr.detectChanges();
431
998
  }
432
999
 
433
1000
  onContainerClick(event: MouseEvent): void {
@@ -448,19 +1015,19 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
448
1015
 
449
1016
  onCanvasContextMenu(event: MouseEvent): void {
450
1017
  event.preventDefault();
451
-
1018
+
452
1019
  // Get hit test from canvas renderer to know which cell was clicked
453
1020
  const hitTest = this.canvasRenderer.getHitTestResult(event);
454
1021
  if (!hitTest || hitTest.rowIndex === -1) return;
455
-
1022
+
456
1023
  const rowNode = this.gridApi.getDisplayedRowAtIndex(hitTest.rowIndex);
457
- const columns = this.gridApi.getAllColumns().filter(col => col.visible);
1024
+ const columns = this.gridApi.getAllColumns().filter((col) => col.visible);
458
1025
  const column = columns[hitTest.columnIndex];
459
-
1026
+
460
1027
  if (!rowNode || !column) return;
461
-
1028
+
462
1029
  this.contextMenuCell = { rowNode, column };
463
-
1030
+
464
1031
  // Resolve menu items via API if provided
465
1032
  const getContextMenuItems = this.gridApi.getGridOption('getContextMenuItems');
466
1033
  if (getContextMenuItems) {
@@ -469,44 +1036,51 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
469
1036
  column: column,
470
1037
  api: this.gridApi,
471
1038
  type: 'cell',
472
- event: event
1039
+ event: event,
473
1040
  };
474
1041
  this.contextMenuItems = this.resolveContextMenuItems(getContextMenuItems(params));
475
1042
  } else {
476
1043
  // Fallback to defaults if no callback provided
477
1044
  this.contextMenuItems = this.resolveContextMenuItems([
478
- 'copy', 'copyWithHeaders', 'separator', 'export', 'separator', 'resetColumns'
1045
+ 'copy',
1046
+ 'copyWithHeaders',
1047
+ 'separator',
1048
+ 'export',
479
1049
  ]);
480
1050
  }
481
1051
 
482
1052
  if (this.contextMenuItems.length === 0) return;
483
1053
 
484
1054
  this.activeContextMenu = true;
485
-
486
- // Position menu at mouse coordinates (fixed/viewport)
487
- let x = event.clientX;
488
- let y = event.clientY;
489
1055
 
490
- // Prevent menu from going off-screen
491
- if (x + 200 > window.innerWidth) x = window.innerWidth - 200;
492
- if (y + 200 > window.innerHeight) y = window.innerHeight - 200;
1056
+ // Position menu at mouse coordinates relative to container
1057
+ const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
1058
+ let x = event.clientX - containerRect.left;
1059
+ let y = event.clientY - containerRect.top;
1060
+
1061
+ // Prevent menu from going off-container bounds
1062
+ const containerWidth = this._elementRef.nativeElement.offsetWidth;
1063
+ const containerHeight = this._elementRef.nativeElement.offsetHeight;
1064
+
1065
+ if (x + 200 > containerWidth) x = containerWidth - 200;
1066
+ if (y + 200 > containerHeight) y = containerHeight - 200;
493
1067
 
494
1068
  this.contextMenuPosition = { x, y };
495
-
1069
+
496
1070
  // Select the row
497
1071
  this.gridApi.deselectAll();
498
1072
  rowNode.selected = true;
499
1073
  this.updateSelectionState();
500
1074
  this.canvasRenderer?.render();
501
1075
  this.selectionChanged.emit(this.gridApi.getSelectedRows());
502
-
503
- this.cdr.detectChanges();
1076
+
1077
+ this._cdr.detectChanges();
504
1078
  }
505
1079
 
506
1080
  private resolveContextMenuItems(items: (DefaultMenuItem | MenuItemDef)[]): MenuItemDef[] {
507
1081
  const resolved: MenuItemDef[] = [];
508
-
509
- items.forEach(item => {
1082
+
1083
+ items.forEach((item) => {
510
1084
  if (typeof item === 'string') {
511
1085
  const defaultItem = this.getDefaultMenuItem(item);
512
1086
  if (defaultItem) resolved.push(defaultItem);
@@ -514,7 +1088,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
514
1088
  resolved.push(item);
515
1089
  }
516
1090
  });
517
-
1091
+
518
1092
  return resolved;
519
1093
  }
520
1094
 
@@ -523,17 +1097,18 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
523
1097
  case 'copy':
524
1098
  return { name: 'Copy Cell', action: () => this.copyContextMenuCell(), icon: '📋' };
525
1099
  case 'copyWithHeaders':
526
- return this.hasRangeSelection() ?
527
- { name: 'Copy with Headers', action: () => this.copyRangeWithHeaders(), icon: '📋' } : null;
1100
+ return this.hasRangeSelection()
1101
+ ? { name: 'Copy with Headers', action: () => this.copyRangeWithHeaders(), icon: '📋' }
1102
+ : null;
528
1103
  case 'export':
529
- return {
530
- name: 'Export',
531
- action: () => {},
1104
+ return {
1105
+ name: 'Export',
1106
+ action: () => {},
532
1107
  icon: '⤓',
533
1108
  subMenu: [
534
1109
  { name: 'Export to CSV', action: () => this.exportCSV() },
535
- { name: 'Export to Excel (.xlsx)', action: () => this.exportExcel() }
536
- ]
1110
+ { name: 'Export to Excel (.xlsx)', action: () => this.exportExcel() },
1111
+ ],
537
1112
  };
538
1113
  case 'resetColumns':
539
1114
  return { name: 'Reset Columns', action: () => this.resetColumns(), icon: '⟲' };
@@ -544,10 +1119,322 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
544
1119
  }
545
1120
  }
546
1121
 
1122
+ // ============================================================================
1123
+ // TOOLTIP
1124
+ // ============================================================================
1125
+
1126
+ onCanvasMouseMove(event: MouseEvent): void {
1127
+ // Cancel any pending show and hide the current tooltip on every move
1128
+ if (this._tooltipTimer) {
1129
+ clearTimeout(this._tooltipTimer);
1130
+ this._tooltipTimer = null;
1131
+ }
1132
+ this.tooltipVisible = false;
1133
+
1134
+ if (!this.canvasRenderer) return;
1135
+
1136
+ const hit = this.canvasRenderer.getHitTestResult(event);
1137
+ const { rowIndex, columnIndex } = hit;
1138
+ if (rowIndex < 0 || columnIndex < 0) return;
1139
+
1140
+ const columns = this.canvasRenderer.getAllColumns();
1141
+ const column = columns[columnIndex];
1142
+ if (!column) return;
1143
+
1144
+ const text = this.computeTooltipText(rowIndex, column);
1145
+ if (!text) return;
1146
+
1147
+ this._tooltipTimer = setTimeout(() => {
1148
+ const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
1149
+ let tx = event.clientX - containerRect.left + 14;
1150
+ let ty = event.clientY - containerRect.top + 14;
1151
+ // Keep within container bounds
1152
+ const cw = this._elementRef.nativeElement.offsetWidth;
1153
+ const ch = this._elementRef.nativeElement.offsetHeight;
1154
+ if (tx + 220 > cw) tx = Math.max(0, tx - 234);
1155
+ if (ty + 56 > ch) ty = Math.max(0, ty - 70);
1156
+ this.tooltipText = text;
1157
+ this.tooltipPosition = { x: tx, y: ty };
1158
+ this.tooltipVisible = true;
1159
+ this._cdr.detectChanges();
1160
+ }, 500);
1161
+ }
1162
+
1163
+ onCanvasMouseLeave(): void {
1164
+ if (this._tooltipTimer) {
1165
+ clearTimeout(this._tooltipTimer);
1166
+ this._tooltipTimer = null;
1167
+ }
1168
+ if (this.tooltipVisible) {
1169
+ this.tooltipVisible = false;
1170
+ this._cdr.detectChanges();
1171
+ }
1172
+ }
1173
+
1174
+ private computeTooltipText(rowIndex: number, column: Column): string | null {
1175
+ const colDef = this.getColumnDefForColumn(column) as ColDef<TData> | null;
1176
+ if (!colDef || !this.isColDef(colDef)) return null;
1177
+
1178
+ const rowNode = this.gridApi?.getDisplayedRowAtIndex(rowIndex);
1179
+ if (!rowNode?.data) return null;
1180
+
1181
+ // tooltipValueGetter takes priority (AG Grid parity)
1182
+ if (typeof colDef.tooltipValueGetter === 'function') {
1183
+ const val = colDef.field ? (rowNode.data as any)[colDef.field as string] : undefined;
1184
+ return (
1185
+ colDef.tooltipValueGetter({
1186
+ value: val,
1187
+ data: rowNode.data as TData,
1188
+ node: rowNode,
1189
+ column,
1190
+ }) ?? null
1191
+ );
1192
+ }
1193
+
1194
+ // tooltipField — show the value of that field
1195
+ if (colDef.tooltipField) {
1196
+ const val = (rowNode.data as any)[colDef.tooltipField as string];
1197
+ return val != null ? String(val) : null;
1198
+ }
1199
+
1200
+ return null;
1201
+ }
1202
+
547
1203
  closeContextMenu(): void {
548
1204
  this.activeContextMenu = false;
549
1205
  this.contextMenuCell = null;
550
- this.cdr.detectChanges();
1206
+ this._cdr.detectChanges();
1207
+ }
1208
+
1209
+ // Set Filter Methods
1210
+ isSetFilter(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
1211
+ if ('children' in col) return false;
1212
+ const colDef = col as ColDef<TData>;
1213
+ return colDef.filter === 'set';
1214
+ }
1215
+
1216
+ openSetFilter(
1217
+ event: MouseEvent | null,
1218
+ col: Column | ColDef<TData>,
1219
+ position?: { x: number; y: number }
1220
+ ): void {
1221
+ if (event) {
1222
+ event.stopPropagation();
1223
+ event.preventDefault();
1224
+ }
1225
+
1226
+ this.activeSetFilterColumn = col as Column;
1227
+
1228
+ const field = col.field;
1229
+ if (!field || !this.gridApi) return;
1230
+
1231
+ this.setFilterValues = this.gridService.getUniqueValues(field as string);
1232
+
1233
+ // Restore previously selected values from the current filter model
1234
+ const existingFilter = this.gridApi.getFilterModel()[field as string] as any;
1235
+ this.setFilterSelectedValues =
1236
+ existingFilter?.filterType === 'set' && Array.isArray(existingFilter.values)
1237
+ ? existingFilter.values
1238
+ : null;
1239
+
1240
+ const colDef = 'field' in col ? (col as ColDef<TData>) : null;
1241
+ this.setFilterValueFormatter = colDef?.valueFormatter
1242
+ ? (colDef.valueFormatter as any)
1243
+ : undefined;
1244
+
1245
+ if (position) {
1246
+ this.setFilterPosition = position;
1247
+ } else if (event) {
1248
+ const rect = (event.target as HTMLElement).getBoundingClientRect();
1249
+ const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
1250
+ this.setFilterPosition = {
1251
+ x: rect.left - containerRect.left,
1252
+ y: rect.bottom - containerRect.top + 5,
1253
+ };
1254
+ }
1255
+
1256
+ setTimeout(() => {
1257
+ this.activeSetFilter = true;
1258
+ this._cdr.detectChanges();
1259
+ });
1260
+ }
1261
+
1262
+ closeSetFilter(): void {
1263
+ this.activeSetFilter = false;
1264
+ this.activeSetFilterColumn = null;
1265
+ this._cdr.detectChanges();
1266
+ }
1267
+
1268
+ // Filter Popup state
1269
+ activeFilterPopup = false;
1270
+ activeFilterPopupColumn: Column | null = null;
1271
+ activeFilterPopupType: 'text' | 'number' | 'date' | 'boolean' | 'set' | 'multiFilter' = 'text';
1272
+ activeFilterOperator: string = 'contains';
1273
+ filterPopupPosition = { x: 0, y: 0 };
1274
+ filterValue1: string = '';
1275
+ filterValue2: string = '';
1276
+
1277
+ readonly textFilterOperators = [
1278
+ { value: 'contains', label: 'Contains' },
1279
+ { value: 'notContains', label: 'Not contains' },
1280
+ { value: 'equals', label: 'Equals' },
1281
+ { value: 'notEquals', label: 'Not equals' },
1282
+ { value: 'startsWith', label: 'Starts with' },
1283
+ { value: 'endsWith', label: 'Ends with' },
1284
+ { value: 'blank', label: 'Blank' },
1285
+ { value: 'notBlank', label: 'Not blank' },
1286
+ ];
1287
+
1288
+ readonly numberFilterOperators = [
1289
+ { value: 'equals', label: 'Equals' },
1290
+ { value: 'notEquals', label: 'Not equals' },
1291
+ { value: 'greaterThan', label: 'Greater than' },
1292
+ { value: 'greaterThanOrEqual', label: 'Greater than or equals' },
1293
+ { value: 'lessThan', label: 'Less than' },
1294
+ { value: 'lessThanOrEqual', label: 'Less than or equals' },
1295
+ { value: 'inRange', label: 'In range' },
1296
+ { value: 'blank', label: 'Blank' },
1297
+ { value: 'notBlank', label: 'Not blank' },
1298
+ ];
1299
+
1300
+ onFilterPopupOperatorChange(operator: string): void {
1301
+ this.activeFilterOperator = operator;
1302
+ this.applyPopupFilter();
1303
+ }
1304
+
1305
+ onFilterPopupInput(event: Event, isSecondValue: boolean = false): void {
1306
+ const value = (event.target as HTMLInputElement).value;
1307
+ if (isSecondValue) {
1308
+ this.filterValue2 = value;
1309
+ } else {
1310
+ this.filterValue1 = value;
1311
+ }
1312
+ this.applyPopupFilter();
1313
+ }
1314
+
1315
+ private applyPopupFilter(): void {
1316
+ if (!this.activeFilterPopupColumn || !this.gridApi) return;
1317
+
1318
+ const col = this.activeFilterPopupColumn;
1319
+ const field = col.field;
1320
+ if (!field) return;
1321
+
1322
+ const currentModel = this.gridApi.getFilterModel();
1323
+
1324
+ if (this.activeFilterOperator === 'blank' || this.activeFilterOperator === 'notBlank') {
1325
+ currentModel[col.colId] = {
1326
+ filterType: this.activeFilterPopupType,
1327
+ type: this.activeFilterOperator,
1328
+ };
1329
+ } else {
1330
+ const value = this.filterValue1;
1331
+ if (!value && this.activeFilterOperator !== 'inRange') {
1332
+ delete currentModel[col.colId];
1333
+ } else {
1334
+ const filterModel: any = {
1335
+ filterType: this.activeFilterPopupType,
1336
+ type: this.activeFilterOperator,
1337
+ filter: value,
1338
+ };
1339
+
1340
+ if (this.activeFilterOperator === 'inRange') {
1341
+ filterModel.filterTo = this.filterValue2;
1342
+ }
1343
+
1344
+ currentModel[col.colId] = filterModel;
1345
+ }
1346
+ }
1347
+
1348
+ this.gridApi.setFilterModel(currentModel);
1349
+ this.canvasRenderer?.render();
1350
+ this._cdr.detectChanges();
1351
+ }
1352
+
1353
+ openFilterPopup(
1354
+ event: MouseEvent | null,
1355
+ col: Column,
1356
+ position?: { x: number; y: number }
1357
+ ): void {
1358
+ this.activeFilterPopupColumn = col;
1359
+ const colDef = this.getColumnDefForColumn(col);
1360
+ this.activeFilterPopupType =
1361
+ colDef && this.isColDef(colDef) && colDef.filter === 'number' ? 'number' : 'text';
1362
+
1363
+ // Initialize operator and values from current model or default
1364
+ const model = this.gridApi?.getFilterModel()[col.colId] as any;
1365
+ this.activeFilterOperator =
1366
+ model?.type || (this.activeFilterPopupType === 'number' ? 'equals' : 'contains');
1367
+ this.filterValue1 = model?.filter || '';
1368
+ this.filterValue2 = model?.filterTo || '';
1369
+
1370
+ if (position) {
1371
+ this.filterPopupPosition = position;
1372
+ } else if (event) {
1373
+ const rect = (event.target as HTMLElement).getBoundingClientRect();
1374
+ const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
1375
+ this.filterPopupPosition = {
1376
+ x: rect.left - containerRect.left,
1377
+ y: rect.bottom - containerRect.top + 5,
1378
+ };
1379
+ }
1380
+
1381
+ setTimeout(() => {
1382
+ this.activeFilterPopup = true;
1383
+ this._cdr.detectChanges();
1384
+ });
1385
+ }
1386
+
1387
+ closeFilterPopup(): void {
1388
+ this.activeFilterPopup = false;
1389
+ this.activeFilterPopupColumn = null;
1390
+ this._cdr.detectChanges();
1391
+ }
1392
+
1393
+ onSetFilterChanged(values: any[]): void {
1394
+ if (!this.activeSetFilterColumn || !this.gridApi) return;
1395
+
1396
+ const field = this.activeSetFilterColumn.field;
1397
+ if (!field) return;
1398
+
1399
+ if (values.length === 0) {
1400
+ const currentModel = this.gridApi.getFilterModel();
1401
+ delete (currentModel as any)[field];
1402
+ this.gridApi.setFilterModel(currentModel);
1403
+ } else {
1404
+ this.gridApi.setFilterModel({
1405
+ ...this.gridApi.getFilterModel(),
1406
+ [field]: {
1407
+ filterType: 'set',
1408
+ values: values,
1409
+ },
1410
+ });
1411
+ }
1412
+
1413
+ this.closeSetFilter();
1414
+ this.canvasRenderer?.render();
1415
+ }
1416
+
1417
+ hasSetFilterValue(col: Column | ColDef<TData>): boolean {
1418
+ if (!this.gridApi) return false;
1419
+ const field = 'field' in col ? col.field : null;
1420
+ if (!field) return false;
1421
+
1422
+ const model = this.gridApi.getFilterModel();
1423
+ const filter = (model as any)[field];
1424
+ return filter && filter.filterType === 'set' && filter.values && filter.values.length > 0;
1425
+ }
1426
+
1427
+ getSetFilterCount(col: Column | ColDef<TData>): number {
1428
+ if (!this.gridApi) return 0;
1429
+ const field = 'field' in col ? col.field : null;
1430
+ if (!field) return 0;
1431
+
1432
+ const model = this.gridApi.getFilterModel();
1433
+ const filter = (model as any)[field];
1434
+ if (filter && filter.filterType === 'set' && Array.isArray(filter.values)) {
1435
+ return filter.values.length;
1436
+ }
1437
+ return 0;
551
1438
  }
552
1439
 
553
1440
  // Side Bar Methods
@@ -557,16 +1444,23 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
557
1444
  } else {
558
1445
  this.activeToolPanel = panel;
559
1446
  }
560
- this.cdr.detectChanges();
1447
+ this._cdr.detectChanges();
1448
+ }
1449
+
1450
+ openColumnsPanel(): void {
1451
+ this.sideBarVisible = true;
1452
+ this.activeToolPanel = 'columns';
1453
+ this.closeHeaderMenu();
1454
+ this._cdr.detectChanges();
561
1455
  }
562
1456
 
563
1457
  toggleColumnVisibility(col: Column): void {
564
1458
  const colDef = this.getColumnDefForColumn(col);
565
- if (colDef) {
1459
+ if (colDef && this.isColDef(colDef)) {
566
1460
  colDef.hide = col.visible; // Toggle
567
1461
  this.initializeGrid(); // Re-initialize to handle visibility changes correctly
568
1462
  this.canvasRenderer?.render();
569
- this.cdr.detectChanges();
1463
+ this._cdr.detectChanges();
570
1464
  }
571
1465
  }
572
1466
 
@@ -582,7 +1476,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
582
1476
 
583
1477
  // Map back to ColDefs
584
1478
  const newDefs: (ColDef<TData> | ColGroupDef<TData>)[] = [];
585
- columns.forEach(col => {
1479
+ columns.forEach((col) => {
586
1480
  const def = this.getColumnDefForColumn(col);
587
1481
  if (def) newDefs.push(def);
588
1482
  });
@@ -592,10 +1486,10 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
592
1486
 
593
1487
  copyContextMenuCell(): void {
594
1488
  if (!this.contextMenuCell || !this.contextMenuCell.column.field) return;
595
-
1489
+
596
1490
  const val = (this.contextMenuCell.rowNode.data as any)[this.contextMenuCell.column.field];
597
1491
  if (val !== undefined && val !== null) {
598
- navigator.clipboard.writeText(String(val)).catch(err => {
1492
+ navigator.clipboard.writeText(String(val)).catch((err) => {
599
1493
  console.error('Failed to copy text: ', err);
600
1494
  });
601
1495
  }
@@ -612,20 +1506,22 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
612
1506
 
613
1507
  const range = ranges[0];
614
1508
  const columns = range.columns;
615
-
616
- let text = columns.map(c => this.getHeaderName(c)).join('\t') + '\n';
1509
+
1510
+ let text = `${columns.map((c) => this.getHeaderName(c)).join('\t')}\n`;
617
1511
 
618
1512
  for (let i = range.startRow; i <= range.endRow; i++) {
619
1513
  const node = this.gridApi.getDisplayedRowAtIndex(i);
620
1514
  if (node) {
621
- text += columns.map(c => {
622
- const val = (node.data as any)[c.field || ''];
623
- return val !== null && val !== undefined ? String(val) : '';
624
- }).join('\t') + '\n';
1515
+ text += `${columns
1516
+ .map((c) => {
1517
+ const val = (node.data as any)[c.field || ''];
1518
+ return val !== null && val !== undefined ? String(val) : '';
1519
+ })
1520
+ .join('\t')}\n`;
625
1521
  }
626
1522
  }
627
1523
 
628
- navigator.clipboard.writeText(text).catch(err => {
1524
+ navigator.clipboard.writeText(text).catch((err) => {
629
1525
  console.error('Failed to copy range: ', err);
630
1526
  });
631
1527
  this.closeContextMenu();
@@ -646,7 +1542,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
646
1542
  // Deep copy back the original defs
647
1543
  const restored = JSON.parse(JSON.stringify(this.initialColumnDefs));
648
1544
  this.onColumnDefsChanged(restored);
649
-
1545
+
650
1546
  // Also clear sort model
651
1547
  this.gridApi.setSortModel([]);
652
1548
  }
@@ -655,128 +1551,96 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
655
1551
 
656
1552
  sortColumnMenu(direction: 'asc' | 'desc' | null): void {
657
1553
  if (!this.activeHeaderMenu) return;
658
-
1554
+
659
1555
  const col = this.activeHeaderMenu as any;
660
1556
  const colId = col.colId || col.field?.toString() || '';
661
-
1557
+
662
1558
  // Update original ColDef to ensure persistence
663
1559
  const colDef = this.getColumnDefForColumn(col);
664
- if (colDef) {
1560
+ if (colDef && this.isColDef(colDef)) {
665
1561
  colDef.sort = direction;
666
1562
  }
667
1563
 
668
1564
  this.gridApi.setSortModel(direction ? [{ colId, sort: direction }] : []);
669
1565
  this.canvasRenderer?.render();
670
-
1566
+
671
1567
  this.closeHeaderMenu();
672
1568
  }
673
1569
 
674
1570
  hideColumnMenu(): void {
675
1571
  if (!this.activeHeaderMenu) return;
676
-
1572
+
677
1573
  const col = this.activeHeaderMenu as any;
678
-
1574
+
679
1575
  // Update the original column definition
680
1576
  const colDef = this.getColumnDefForColumn(col);
681
- if (colDef) {
1577
+ if (colDef && this.isColDef(colDef)) {
682
1578
  colDef.hide = true;
683
1579
  }
684
-
1580
+
685
1581
  // Create new array to trigger change detection and API update
686
1582
  if (this.columnDefs) {
687
1583
  this.onColumnDefsChanged([...this.columnDefs]);
688
1584
  }
689
-
1585
+
690
1586
  this.closeHeaderMenu();
691
1587
  }
692
1588
 
693
1589
  pinColumnMenu(pin: 'left' | 'right' | null): void {
694
1590
  if (!this.activeHeaderMenu) return;
695
-
1591
+
696
1592
  const col = this.activeHeaderMenu as any;
697
-
1593
+
698
1594
  // Update the original column definition
699
1595
  const colDef = this.getColumnDefForColumn(col);
700
- if (colDef) {
1596
+ if (colDef && this.isColDef(colDef)) {
701
1597
  colDef.pinned = pin as any;
702
1598
  }
703
-
1599
+
704
1600
  if (this.columnDefs) {
705
1601
  this.onColumnDefsChanged([...this.columnDefs]);
706
1602
  }
707
-
1603
+
708
1604
  this.closeHeaderMenu();
709
1605
  }
710
1606
 
711
- onColumnDropped(event: CdkDragDrop<any[]>, pinType: 'left' | 'right' | 'none'): void {
712
- if (!this.columnDefs) return;
713
-
714
- // Get current groups (using internal Columns)
715
- const left = [...this.getLeftPinnedColumns()];
716
- const center = [...this.getNonPinnedColumns()];
717
- const right = [...this.getRightPinnedColumns()];
718
-
719
- const containerMap: { [key: string]: any[] } = {
720
- 'left-pinned': left,
721
- 'scrollable': center,
722
- 'right-pinned': right
723
- };
724
-
725
- const previousContainerData = containerMap[event.previousContainer.id];
726
- const currentContainerData = containerMap[event.container.id];
1607
+ onColumnDropped(event: CdkDragDrop<any>, pinned: 'left' | 'right' | 'none'): void {
1608
+ const col = event.item.data as Column;
1609
+ if (!col) return;
727
1610
 
728
- if (event.previousContainer === event.container) {
729
- moveItemInArray(currentContainerData, event.previousIndex, event.currentIndex);
730
- } else {
731
- transferArrayItem(
732
- previousContainerData,
733
- currentContainerData,
734
- event.previousIndex,
735
- event.currentIndex
736
- );
1611
+ const targetPinned = pinned === 'none' ? false : pinned;
737
1612
 
738
- // Update pinned state of the moved column in its original definition
739
- const movedCol = currentContainerData[event.currentIndex] as Column;
740
- const colDef = this.getColumnDefForColumn(movedCol);
741
- if (colDef) {
742
- colDef.pinned = pinType === 'none' ? null : pinType as any;
743
- }
1613
+ if (col.pinned !== targetPinned) {
1614
+ this.gridApi.setColumnPinned(col, targetPinned);
744
1615
  }
745
1616
 
746
- // Map internal Columns back to their original ColDefs in the new order
747
- const orderedVisibleColDefs: (ColDef<TData> | ColGroupDef<TData>)[] = [];
748
- [...left, ...center, ...right].forEach(col => {
749
- const def = this.getColumnDefForColumn(col);
750
- if (def) orderedVisibleColDefs.push(def);
751
- });
1617
+ this.gridApi.moveColumn(col, event.currentIndex);
752
1618
 
753
- // Reconstruct full columnDefs array, maintaining hidden columns
754
- const hidden = this.columnDefs.filter(c => {
755
- if ('children' in c) return false;
756
- return (c as ColDef).hide;
757
- });
758
-
759
- const newDefs = [...orderedVisibleColDefs, ...hidden];
760
-
761
- this.onColumnDefsChanged(newDefs);
1619
+ this.canvasRenderer?.render();
1620
+ this._cdr.detectChanges();
762
1621
  }
763
1622
 
764
1623
  // --- Column Resizing Logic ---
765
1624
 
766
- isResizable(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
767
- if ('children' in col) return false;
768
- const colDef = this.getColumnDefForColumn(col as any);
769
- return colDef ? colDef.resizable !== false : true;
1625
+ isResizable(item: Column | ColumnGroup | ColDef<TData> | ColGroupDef<TData>): boolean {
1626
+ if ('children' in item) {
1627
+ return (item as any).children.some((child: any) => this.isResizable(child));
1628
+ }
1629
+ const colId = (item as any).colId || (item as any).field?.toString();
1630
+ if (colId === 'ag-Grid-SelectionColumn') return true;
1631
+
1632
+ const colDef = this.getColumnDefForColumn(item as any);
1633
+ return colDef && this.isColDef(colDef) ? colDef.resizable !== false : true;
770
1634
  }
771
1635
 
772
- onResizeMouseDown(event: MouseEvent, col: Column): void {
1636
+ onResizeMouseDown(event: MouseEvent, item: Column | ColumnGroup): void {
773
1637
  event.stopPropagation();
774
1638
  event.preventDefault();
775
1639
 
776
1640
  this.isResizing = true;
777
- this.resizeColumn = col;
1641
+ this.resizeItem = item;
778
1642
  this.resizeStartX = event.clientX;
779
- this.resizeStartWidth = col.width;
1643
+ this.resizeStartWidth = this.getItemWidth(item);
780
1644
 
781
1645
  const mouseMoveHandler = (e: MouseEvent) => this.onResizeMouseMove(e);
782
1646
  const mouseUpHandler = () => {
@@ -790,52 +1654,71 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
790
1654
  }
791
1655
 
792
1656
  private onResizeMouseMove(event: MouseEvent): void {
793
- if (!this.isResizing || !this.resizeColumn) return;
1657
+ if (!this.isResizing || !this.resizeItem) return;
794
1658
 
795
1659
  const deltaX = event.clientX - this.resizeStartX;
796
1660
  const newWidth = Math.max(20, this.resizeStartWidth + deltaX);
797
-
798
- // Update internal column width
799
- this.resizeColumn.width = newWidth;
800
-
801
- // Update original ColDef
802
- const colDef = this.getColumnDefForColumn(this.resizeColumn);
803
- if (colDef) {
804
- colDef.width = newWidth;
805
- }
1661
+
1662
+ this.applyResize(this.resizeItem!, newWidth);
806
1663
 
807
1664
  // Force re-render
808
1665
  this.canvasRenderer?.render();
809
- this.cdr.detectChanges();
1666
+ this._cdr.detectChanges();
810
1667
  }
811
1668
 
812
1669
  private onResizeMouseUp(): void {
813
1670
  this.isResizing = false;
814
- this.resizeColumn = null;
1671
+ this.resizeItem = null;
1672
+ }
1673
+
1674
+ private applyResize(item: Column | ColumnGroup, newWidth: number): void {
1675
+ if ('children' in item) {
1676
+ const currentWidth = this.getItemWidth(item);
1677
+ if (currentWidth === 0) return;
1678
+
1679
+ const ratio = newWidth / currentWidth;
1680
+ item.children.forEach((child) => {
1681
+ if (isColumnVisible(child)) {
1682
+ const childWidth = this.getItemWidth(child);
1683
+ this.applyResize(child, childWidth * ratio);
1684
+ }
1685
+ });
1686
+ } else {
1687
+ const finalWidth = Math.floor(newWidth);
1688
+ (item as Column).width = finalWidth;
1689
+ const colDef = this.getColumnDefForColumn(item as Column);
1690
+ if (colDef && this.isColDef(colDef)) {
1691
+ colDef.width = finalWidth;
1692
+ }
1693
+ }
815
1694
  }
816
1695
 
817
1696
  // --- Floating Filter Logic ---
818
1697
 
819
1698
  hasFloatingFilters(): boolean {
820
1699
  if (this.gridApi?.getGridOption('floatingFilter')) return true;
821
-
1700
+ if (this.gridOptions?.defaultColDef?.floatingFilter) return true;
1701
+
822
1702
  if (!this.columnDefs) return false;
823
- return this.columnDefs.some(col => {
1703
+ const hasAny = this.columnDefs.some((col) => {
824
1704
  if ('children' in col) {
825
- return col.children.some(child => 'floatingFilter' in child && child.floatingFilter);
1705
+ return col.children.some((child) => (child as any).floatingFilter);
826
1706
  }
827
- return col.floatingFilter;
1707
+ return (col as any).floatingFilter;
828
1708
  });
1709
+ return hasAny;
829
1710
  }
830
1711
 
831
1712
  isFloatingFilterEnabled(col: Column | ColDef<TData> | ColGroupDef<TData>): boolean {
832
1713
  const colDef = this.getColumnDefForColumn(col as any);
833
1714
  if (!colDef || 'children' in colDef) return false;
834
- if (!colDef.filter) return false;
835
-
1715
+
1716
+ const filter = colDef.filter;
1717
+ if (!filter) return false;
1718
+
836
1719
  if (colDef.floatingFilter === true) return true;
837
1720
  if (colDef.floatingFilter === false) return false;
838
-
1721
+
839
1722
  return !!this.gridApi?.getGridOption('floatingFilter');
840
1723
  }
841
1724
 
@@ -857,26 +1740,28 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
857
1740
  private filterTimeout: any;
858
1741
  onFloatingFilterInput(event: Event, col: Column | ColDef<TData> | ColGroupDef<TData>): void {
859
1742
  const colDef = this.getColumnDefForColumn(col as any);
860
- if (!colDef || 'children' in colDef) return;
861
-
1743
+ if (!colDef || !this.isColDef(colDef)) return;
1744
+
862
1745
  const input = event.target as HTMLInputElement;
863
1746
  const value = input.value;
864
1747
  const colId = (col as any).colId || (col as any).field?.toString() || '';
865
1748
 
866
- this.cdr.detectChanges(); // Update clear button visibility immediately
1749
+ this._cdr.detectChanges(); // Update clear button visibility immediately
867
1750
 
868
1751
  clearTimeout(this.filterTimeout);
869
1752
  this.filterTimeout = setTimeout(() => {
870
1753
  const currentModel = this.gridApi.getFilterModel();
871
-
872
- if (!value) {
1754
+ const existingFilter = (currentModel[colId] || {}) as any;
1755
+
1756
+ if (!value && existingFilter.type !== 'blank' && existingFilter.type !== 'notBlank') {
873
1757
  delete currentModel[colId];
874
1758
  } else {
875
1759
  const filterType = this.getFilterTypeFromCol(colDef);
876
1760
  currentModel[colId] = {
1761
+ ...existingFilter,
877
1762
  filterType: filterType as any,
878
- type: filterType === 'text' ? 'contains' : 'equals',
879
- filter: value
1763
+ type: existingFilter.type || (filterType === 'text' ? 'contains' : 'equals'),
1764
+ filter: value,
880
1765
  };
881
1766
  }
882
1767
 
@@ -900,25 +1785,31 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
900
1785
  return model[colId]?.filter || '';
901
1786
  }
902
1787
 
903
- hasFilterValue(col: Column | ColDef<TData> | ColGroupDef<TData>, input: HTMLInputElement): boolean {
1788
+ hasFilterValue(
1789
+ col: Column | ColDef<TData> | ColGroupDef<TData>,
1790
+ _input: HTMLInputElement
1791
+ ): boolean {
904
1792
  return !!this.getFloatingFilterValue(col);
905
1793
  }
906
1794
 
907
- clearFloatingFilter(col: Column | ColDef<TData> | ColGroupDef<TData>, input: HTMLInputElement): void {
1795
+ clearFloatingFilter(
1796
+ col: Column | ColDef<TData> | ColGroupDef<TData>,
1797
+ input: HTMLInputElement
1798
+ ): void {
908
1799
  const colDef = this.getColumnDefForColumn(col as any);
909
- if (!colDef || 'children' in colDef) return;
910
-
1800
+ if (!colDef || !this.isColDef(colDef)) return;
1801
+
911
1802
  input.value = '';
912
1803
  const colId = (col as any).colId || (col as any).field?.toString() || '';
913
-
1804
+
914
1805
  const currentModel = this.gridApi.getFilterModel();
915
1806
  delete currentModel[colId];
916
-
1807
+
917
1808
  this.gridApi.setFilterModel(currentModel);
918
1809
  this.canvasRenderer?.render();
919
- this.cdr.detectChanges();
1810
+ this._cdr.detectChanges();
920
1811
  }
921
-
1812
+
922
1813
  // Public API methods
923
1814
  getApi(): GridApi<TData> {
924
1815
  return this.gridApi;
@@ -936,42 +1827,42 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
936
1827
  startEditing(rowIndex: number, colId: string): void {
937
1828
  const rowNode = this.gridApi.getDisplayedRowAtIndex(rowIndex);
938
1829
  const column = this.gridApi.getColumn(colId);
939
-
1830
+
940
1831
  // Prevent editing on group rows or missing data/column
941
1832
  if (!rowNode || rowNode.group || !column || !column.field) return;
942
-
1833
+
943
1834
  // Check if cell is editable
944
1835
  const colDef = this.getColumnDefForColumn(column);
945
- if (colDef && colDef.editable === false) return;
946
-
1836
+ if (colDef && this.isColDef(colDef) && colDef.editable === false) return;
1837
+
947
1838
  // If already editing another cell, stop it first
948
1839
  if (this.isEditing) {
949
1840
  this.stopEditing(true);
950
1841
  }
951
1842
 
952
1843
  const value = (rowNode.data as any)[column.field];
953
-
1844
+
954
1845
  this.editingRowNode = rowNode;
955
1846
  this.editingColDef = colDef;
956
1847
  this.editingValue = value !== null && value !== undefined ? String(value) : '';
957
-
1848
+
958
1849
  // Calculate editor position based on row and column
959
- const columns = this.gridApi.getAllColumns().filter(c => c.visible);
1850
+ const columns = this.gridApi.getAllColumns().filter((c) => isColumnVisible(c));
960
1851
  let x = 0;
961
1852
  for (const col of columns) {
962
1853
  if (col.colId === colId) break;
963
- x += col.width;
1854
+ x += Math.floor(col.width || 150);
964
1855
  }
965
-
1856
+
966
1857
  this.editorPosition = {
967
1858
  x: x - this.canvasRenderer.currentScrollLeft,
968
- y: (rowIndex * this.rowHeight) - this.canvasRenderer.currentScrollTop,
969
- width: column.width,
970
- height: this.rowHeight
1859
+ y: rowIndex * this.effectiveRowHeight - this.canvasRenderer.currentScrollTop,
1860
+ width: Math.floor(column.width),
1861
+ height: this.effectiveRowHeight,
971
1862
  };
972
-
1863
+
973
1864
  this.isEditing = true;
974
-
1865
+
975
1866
  // Focus input after view update
976
1867
  setTimeout(() => {
977
1868
  if (this.editorInputRef) {
@@ -984,7 +1875,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
984
1875
 
985
1876
  stopEditing(save: boolean = true): void {
986
1877
  if (!this.isEditing) return;
987
-
1878
+
988
1879
  const rowNode = this.editingRowNode;
989
1880
  const colDef = this.editingColDef;
990
1881
 
@@ -1007,7 +1898,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1007
1898
  data: rowNode.data,
1008
1899
  node: rowNode,
1009
1900
  colDef,
1010
- api: this.gridApi
1901
+ api: this.gridApi,
1011
1902
  });
1012
1903
  }
1013
1904
 
@@ -1019,7 +1910,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1019
1910
  data: rowNode.data,
1020
1911
  node: rowNode,
1021
1912
  colDef,
1022
- api: this.gridApi
1913
+ api: this.gridApi,
1023
1914
  });
1024
1915
  } else if (field) {
1025
1916
  // Default: update data directly
@@ -1028,9 +1919,9 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1028
1919
 
1029
1920
  // Update via transaction
1030
1921
  this.gridApi.applyTransaction({
1031
- update: [rowNode.data]
1922
+ update: [rowNode.data],
1032
1923
  });
1033
-
1924
+
1034
1925
  // Trigger callback
1035
1926
  if (colDef.onCellValueChanged) {
1036
1927
  const column = this.gridApi.getColumn(colDef.colId || field || '');
@@ -1040,18 +1931,18 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1040
1931
  oldValue,
1041
1932
  data: rowNode.data,
1042
1933
  node: rowNode,
1043
- column
1934
+ column,
1044
1935
  });
1045
1936
  }
1046
1937
  }
1047
-
1938
+
1048
1939
  this.canvasRenderer?.render();
1049
1940
  }
1050
-
1941
+
1051
1942
  this.isEditing = false;
1052
1943
  this.editingRowNode = null;
1053
1944
  this.editingColDef = null;
1054
- this.cdr.detectChanges();
1945
+ this._cdr.detectChanges();
1055
1946
  }
1056
1947
 
1057
1948
  onEditorInput(event: Event): void {
@@ -1070,7 +1961,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1070
1961
  event.preventDefault();
1071
1962
  const currentRowIndex = this.editingRowNode?.displayedRowIndex ?? -1;
1072
1963
  const currentColId = this.editingColDef?.colId || this.editingColDef?.field?.toString() || '';
1073
-
1964
+
1074
1965
  this.stopEditing(true);
1075
1966
 
1076
1967
  // Standard AG Grid Tab behavior: move to next cell
@@ -1081,9 +1972,9 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1081
1972
  }
1082
1973
 
1083
1974
  private moveToNextCell(rowIndex: number, colId: string, backwards: boolean): void {
1084
- const columns = this.gridApi.getAllColumns().filter(c => c.visible);
1085
- const colIndex = columns.findIndex(c => c.colId === colId);
1086
-
1975
+ const columns = this.gridApi.getAllColumns().filter((c) => c.visible);
1976
+ const colIndex = columns.findIndex((c) => c.colId === colId);
1977
+
1087
1978
  if (colIndex === -1) return;
1088
1979
 
1089
1980
  let nextColIndex = backwards ? colIndex - 1 : colIndex + 1;
@@ -1109,48 +2000,60 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1109
2000
  this.stopEditing(true);
1110
2001
  }
1111
2002
  }
1112
- private getColumnDefForColumn(column: Column | ColDef<TData> | ColGroupDef<TData>): ColDef<TData> | null {
2003
+ private getColumnDefForColumn(
2004
+ column: Column | ColumnGroup | ColDef<TData> | ColGroupDef<TData>
2005
+ ): ColDef<TData> | ColGroupDef<TData> | null {
1113
2006
  if (!this.columnDefs) return null;
1114
-
1115
- const colId = (column as any).colId || (column as any).field?.toString();
2007
+
2008
+ const colId =
2009
+ (column as any).colId || (column as any).field?.toString() || (column as any).groupId;
1116
2010
  if (!colId) return null;
1117
2011
 
1118
- for (const def of this.columnDefs) {
1119
- if ('children' in def) {
1120
- const found = def.children.find(c => {
1121
- const cDef = c as ColDef;
1122
- return cDef.colId === colId || cDef.field?.toString() === colId;
1123
- });
1124
- if (found) return found as ColDef<TData>;
1125
- } else {
1126
- const cDef = def as ColDef;
1127
- if (cDef.colId === colId || cDef.field?.toString() === colId) {
1128
- return def as ColDef<TData>;
2012
+ const defaultColDef = this.gridOptions?.defaultColDef || {};
2013
+
2014
+ const findDef = (
2015
+ defs: (ColDef<TData> | ColGroupDef<TData>)[]
2016
+ ): ColDef<TData> | ColGroupDef<TData> | null => {
2017
+ for (const def of defs) {
2018
+ const defId = (def as any).colId || (def as any).field?.toString() || (def as any).groupId;
2019
+ if (defId === colId) {
2020
+ return 'children' in def ? def : ({ ...defaultColDef, ...def } as ColDef<TData>);
2021
+ }
2022
+ if ('children' in def) {
2023
+ const found = findDef(def.children);
2024
+ if (found) return found;
1129
2025
  }
1130
2026
  }
1131
- }
1132
- return null;
2027
+ return null;
2028
+ };
2029
+
2030
+ return findDef(this.columnDefs);
2031
+ }
2032
+
2033
+ private isColDef(def: any): def is ColDef<TData> {
2034
+ return def && !('children' in def);
1133
2035
  }
1134
2036
 
1135
- // Selection Methods
1136
2037
  onRowClick(rowIndex: number, event: MouseEvent): void {
1137
2038
  const rowNode = this.gridApi.getDisplayedRowAtIndex(rowIndex);
1138
2039
  if (!rowNode) return;
1139
2040
 
1140
- // Handle multi-select with Ctrl/Cmd
1141
- if (event.ctrlKey || event.metaKey) {
1142
- rowNode.selected = !rowNode.selected;
1143
- } else if (event.shiftKey) {
1144
- // Range selection (TODO: implement)
1145
- rowNode.selected = true;
2041
+ const selectionMode = this.gridApi.getGridOption('rowSelection') || 'single';
2042
+ const isMultiSelect =
2043
+ (selectionMode as any) === 'multiple' || (selectionMode as any) === 'multiRow';
2044
+
2045
+ if (isMultiSelect && (event.ctrlKey || event.metaKey)) {
2046
+ rowNode.setSelected(!rowNode.selected);
2047
+ } else if (isMultiSelect && event.shiftKey) {
2048
+ rowNode.setSelected(true);
1146
2049
  } else {
1147
- // Single select - deselect all others
1148
- this.gridApi.deselectAll();
1149
- rowNode.selected = true;
2050
+ if (rowNode.selected) {
2051
+ rowNode.setSelected(false);
2052
+ } else {
2053
+ rowNode.setSelected(true, true);
2054
+ }
1150
2055
  }
1151
2056
 
1152
- this.updateSelectionState();
1153
- this.canvasRenderer?.render();
1154
2057
  this.selectionChanged.emit(this.gridApi.getSelectedRows());
1155
2058
  }
1156
2059
 
@@ -1181,7 +2084,7 @@ export class ArgentGridComponent<TData = any> implements OnInit, OnDestroy, Afte
1181
2084
  updateSelectionState(): void {
1182
2085
  const selectedCount = this.gridApi.getSelectedRows().length;
1183
2086
  const totalCount = this.gridApi.getDisplayedRowCount();
1184
-
2087
+
1185
2088
  this.isAllSelected = selectedCount === totalCount && totalCount > 0;
1186
2089
  this.isIndeterminateSelection = selectedCount > 0 && selectedCount < totalCount;
1187
2090
  }