argent-grid 0.2.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.
- package/AGENTS.md +70 -27
- package/e2e/advanced.spec.ts +1 -1
- package/e2e/benchmark.spec.ts +7 -7
- package/e2e/cell-renderers.spec.ts +152 -0
- package/e2e/debug-streaming.spec.ts +31 -0
- package/e2e/dnd.spec.ts +73 -0
- package/e2e/screenshots.spec.ts +1 -1
- package/e2e/visual.spec.ts +30 -9
- package/e2e/visual.spec.ts-snapshots/checkbox-renderer-mixed.png +0 -0
- package/e2e/visual.spec.ts-snapshots/debug.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-column-group-headers.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-default.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-empty-state.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-filter-popup.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-scroll-borders.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-sidebar-buttons.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-text-filter.png +0 -0
- package/e2e/visual.spec.ts-snapshots/grid-with-selection.png +0 -0
- package/e2e/visual.spec.ts-snapshots/rating-renderer-varied.png +0 -0
- package/package.json +5 -5
- package/plan.md +30 -34
- package/src/lib/components/argent-grid.component.css +258 -549
- package/src/lib/components/argent-grid.component.html +272 -306
- package/src/lib/components/argent-grid.component.ts +585 -135
- package/src/lib/components/argent-grid.regressions.spec.ts +301 -0
- package/src/lib/components/argent-grid.selection.spec.ts +2 -2
- package/src/lib/components/set-filter/set-filter.component.spec.ts +191 -0
- package/src/lib/components/set-filter/set-filter.component.ts +7 -2
- package/src/lib/rendering/canvas-renderer.spec.ts +148 -1
- package/src/lib/rendering/canvas-renderer.ts +177 -286
- package/src/lib/rendering/render/cells.ts +122 -5
- package/src/lib/rendering/render/column-utils.ts +27 -5
- package/src/lib/rendering/render/hit-test.ts +6 -11
- package/src/lib/rendering/render/index.ts +15 -6
- package/src/lib/rendering/render/lines.ts +12 -6
- package/src/lib/rendering/render/primitives.ts +269 -7
- package/src/lib/rendering/render/types.ts +2 -1
- package/src/lib/rendering/render/walk.ts +39 -19
- package/src/lib/services/grid.service.spec.ts +76 -0
- package/src/lib/services/grid.service.ts +451 -114
- package/src/lib/themes/theme-quartz.ts +2 -2
- package/src/lib/types/ag-grid-types.ts +500 -0
- package/src/stories/Advanced.stories.ts +78 -17
- package/src/stories/ArgentGrid.stories.ts +50 -26
- package/src/stories/Benchmark.stories.ts +17 -15
- package/src/stories/CellRenderers.stories.ts +205 -31
- package/src/stories/Filtering.stories.ts +56 -16
- package/src/stories/Grouping.stories.ts +86 -13
- package/src/stories/Streaming.stories.ts +57 -0
- package/src/stories/Theming.stories.ts +23 -10
- package/src/stories/Tooltips.stories.ts +381 -0
- package/src/stories/benchmark-wrapper.component.ts +69 -29
- package/src/stories/story-utils.ts +88 -0
- package/src/stories/streaming-wrapper.component.ts +441 -0
- package/tsconfig.json +1 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CdkDragDrop, moveItemInArray
|
|
1
|
+
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
|
2
2
|
import {
|
|
3
3
|
AfterViewInit,
|
|
4
4
|
ChangeDetectionStrategy,
|
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
import { Subject } from 'rxjs';
|
|
19
19
|
import { takeUntil } from 'rxjs/operators';
|
|
20
20
|
import { CanvasRenderer } from '../rendering/canvas-renderer';
|
|
21
|
+
import { isColumnVisible } from '../rendering/render/column-utils';
|
|
21
22
|
import { GridService } from '../services/grid.service';
|
|
22
23
|
import { applyThemeCSSVariables, convertThemeToGridTheme } from '../themes/theme-builder';
|
|
23
24
|
import {
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
ColDef,
|
|
26
27
|
ColGroupDef,
|
|
27
28
|
Column,
|
|
29
|
+
ColumnGroup,
|
|
28
30
|
DefaultMenuItem,
|
|
29
31
|
GetContextMenuItemsParams,
|
|
30
32
|
GridApi,
|
|
@@ -49,7 +51,7 @@ export class ArgentGridComponent<TData = any>
|
|
|
49
51
|
@Input() theme: any;
|
|
50
52
|
@Input() height = '500px';
|
|
51
53
|
@Input() width = '100%';
|
|
52
|
-
@Input() rowHeight
|
|
54
|
+
@Input() rowHeight?: number;
|
|
53
55
|
@Input() rowSelection: RowSelectionOptions | 'single' | 'multiple' | undefined;
|
|
54
56
|
|
|
55
57
|
@Output() gridReady = new EventEmitter<GridApi<TData>>();
|
|
@@ -66,17 +68,31 @@ export class ArgentGridComponent<TData = any>
|
|
|
66
68
|
showOverlay = false;
|
|
67
69
|
private viewportHeight = 500;
|
|
68
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
|
+
|
|
69
85
|
get totalHeight(): number {
|
|
70
86
|
if (this.gridApi) return this.gridApi.getTotalHeight();
|
|
71
|
-
return (this.rowData?.length || 0) * this.rowHeight;
|
|
87
|
+
return (this.rowData?.length || 0) * (this.rowHeight || 32);
|
|
72
88
|
}
|
|
73
89
|
|
|
74
90
|
get totalWidth(): number {
|
|
75
91
|
if (!this.gridApi) return 0;
|
|
76
92
|
return this.gridApi
|
|
77
93
|
.getAllColumns()
|
|
78
|
-
.filter((col) => col
|
|
79
|
-
.reduce((sum, col) => sum + col.width, 0);
|
|
94
|
+
.filter((col) => isColumnVisible(col))
|
|
95
|
+
.reduce((sum, col) => sum + Math.floor(col.width || 150), 0);
|
|
80
96
|
}
|
|
81
97
|
|
|
82
98
|
// Selection state
|
|
@@ -90,7 +106,7 @@ export class ArgentGridComponent<TData = any>
|
|
|
90
106
|
}
|
|
91
107
|
|
|
92
108
|
hasHeaderCheckbox(col: Column): boolean {
|
|
93
|
-
return col.
|
|
109
|
+
return !!col.headerCheckboxSelection;
|
|
94
110
|
}
|
|
95
111
|
|
|
96
112
|
trackByColumn(index: number, col: Column | ColDef<TData> | ColGroupDef<TData>): string {
|
|
@@ -111,6 +127,7 @@ export class ArgentGridComponent<TData = any>
|
|
|
111
127
|
// Resizing state
|
|
112
128
|
isResizing = false;
|
|
113
129
|
resizeColumn: Column | null = null;
|
|
130
|
+
resizeItem: Column | ColumnGroup | null = null;
|
|
114
131
|
private resizeStartX = 0;
|
|
115
132
|
private resizeStartWidth = 0;
|
|
116
133
|
|
|
@@ -122,21 +139,35 @@ export class ArgentGridComponent<TData = any>
|
|
|
122
139
|
sideBarVisible = false;
|
|
123
140
|
activeToolPanel: 'columns' | 'filters' | null = null;
|
|
124
141
|
|
|
142
|
+
// Row Group Panel state
|
|
143
|
+
rowGroupPanelShow: 'always' | 'onlyWhenGrouping' | 'never' = 'never';
|
|
144
|
+
rowGroupColumns: Column[] = [];
|
|
145
|
+
|
|
125
146
|
// Context Menu state
|
|
126
147
|
activeContextMenu = false;
|
|
127
148
|
contextMenuPosition = { x: 0, y: 0 };
|
|
128
149
|
contextMenuItems: MenuItemDef[] = [];
|
|
129
150
|
private contextMenuCell: { rowNode: IRowNode<TData>; column: Column } | null = null;
|
|
130
151
|
|
|
152
|
+
// Tooltip state
|
|
153
|
+
tooltipVisible = false;
|
|
154
|
+
tooltipText = '';
|
|
155
|
+
tooltipPosition = { x: 0, y: 0 };
|
|
156
|
+
private _tooltipTimer: any = null;
|
|
157
|
+
|
|
131
158
|
// Set Filter
|
|
132
159
|
activeSetFilter = false;
|
|
133
160
|
setFilterPosition = { x: 0, y: 0 };
|
|
134
161
|
setFilterValues: any[] = [];
|
|
162
|
+
setFilterSelectedValues: any[] | null = null;
|
|
135
163
|
setFilterValueFormatter?: (value: any) => string;
|
|
136
164
|
private activeSetFilterColumn: Column | null = null;
|
|
137
165
|
private initialColumnDefs: (ColDef<TData> | ColGroupDef<TData>)[] | null = null;
|
|
138
166
|
|
|
139
|
-
|
|
167
|
+
public gridApi!: GridApi<TData>;
|
|
168
|
+
public isColumnVisible = isColumnVisible;
|
|
169
|
+
public Math = Math;
|
|
170
|
+
public scrollbarWidth = 0;
|
|
140
171
|
private canvasRenderer!: CanvasRenderer;
|
|
141
172
|
private destroy$ = new Subject<void>();
|
|
142
173
|
private gridService = new GridService<TData>();
|
|
@@ -189,6 +220,16 @@ export class ArgentGridComponent<TData = any>
|
|
|
189
220
|
? convertThemeToGridTheme(changes.theme.currentValue)
|
|
190
221
|
: undefined;
|
|
191
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
|
+
}
|
|
192
233
|
}
|
|
193
234
|
}
|
|
194
235
|
}
|
|
@@ -207,10 +248,9 @@ export class ArgentGridComponent<TData = any>
|
|
|
207
248
|
this.canvasRenderer = new CanvasRenderer(
|
|
208
249
|
this.canvasRef.nativeElement,
|
|
209
250
|
this.gridApi,
|
|
210
|
-
this.
|
|
251
|
+
this.effectiveRowHeight,
|
|
211
252
|
convertedTheme
|
|
212
253
|
);
|
|
213
|
-
|
|
214
254
|
// Wire up cell editing callback
|
|
215
255
|
this.canvasRenderer.onCellDoubleClick = (rowIndex, colId) => {
|
|
216
256
|
this.startEditing(rowIndex, colId);
|
|
@@ -272,17 +312,34 @@ export class ArgentGridComponent<TData = any>
|
|
|
272
312
|
if (this.viewportRef) {
|
|
273
313
|
const rect = this.viewportRef.nativeElement.getBoundingClientRect();
|
|
274
314
|
this.viewportHeight = rect.height || 500;
|
|
275
|
-
this.canvasRenderer?.setViewportDimensions(
|
|
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
|
+
};
|
|
276
330
|
|
|
277
331
|
// Synchronize horizontal scroll with DOM header
|
|
278
332
|
this.horizontalScrollListener = () => {
|
|
333
|
+
const viewport = this.viewportRef?.nativeElement;
|
|
334
|
+
if (!viewport) return;
|
|
335
|
+
|
|
336
|
+
updateScrollbar();
|
|
337
|
+
|
|
279
338
|
if (this.headerScrollableRef) {
|
|
280
|
-
this.headerScrollableRef.nativeElement.scrollLeft =
|
|
281
|
-
this.viewportRef.nativeElement.scrollLeft;
|
|
339
|
+
this.headerScrollableRef.nativeElement.scrollLeft = viewport.scrollLeft;
|
|
282
340
|
}
|
|
283
341
|
if (this.headerScrollableFilterRef) {
|
|
284
|
-
this.headerScrollableFilterRef.nativeElement.scrollLeft =
|
|
285
|
-
this.viewportRef.nativeElement.scrollLeft;
|
|
342
|
+
this.headerScrollableFilterRef.nativeElement.scrollLeft = viewport.scrollLeft;
|
|
286
343
|
}
|
|
287
344
|
};
|
|
288
345
|
|
|
@@ -292,17 +349,35 @@ export class ArgentGridComponent<TData = any>
|
|
|
292
349
|
|
|
293
350
|
// Add ResizeObserver to handle sidebar toggling and other size changes
|
|
294
351
|
if (typeof ResizeObserver !== 'undefined') {
|
|
352
|
+
let lastWidth = 0;
|
|
353
|
+
let lastHeight = 0;
|
|
295
354
|
this.resizeObserver = new ResizeObserver((entries) => {
|
|
296
355
|
for (const entry of entries) {
|
|
297
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;
|
|
298
361
|
this.viewportHeight = height;
|
|
299
|
-
|
|
300
|
-
|
|
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.
|
|
301
368
|
this._cdr.detectChanges();
|
|
302
369
|
}
|
|
303
370
|
});
|
|
304
371
|
this.resizeObserver.observe(this.viewportRef.nativeElement);
|
|
305
372
|
}
|
|
373
|
+
|
|
374
|
+
// Initial calculation
|
|
375
|
+
setTimeout(() => {
|
|
376
|
+
updateScrollbar();
|
|
377
|
+
if (this.canvasRenderer) {
|
|
378
|
+
this.canvasRenderer.render();
|
|
379
|
+
}
|
|
380
|
+
});
|
|
306
381
|
}
|
|
307
382
|
}
|
|
308
383
|
|
|
@@ -321,6 +396,7 @@ export class ArgentGridComponent<TData = any>
|
|
|
321
396
|
|
|
322
397
|
this.gridApi?.destroy();
|
|
323
398
|
this.canvasRenderer?.destroy();
|
|
399
|
+
this.onCanvasMouseLeave();
|
|
324
400
|
}
|
|
325
401
|
|
|
326
402
|
private initializeGrid(): void {
|
|
@@ -330,14 +406,35 @@ export class ArgentGridComponent<TData = any>
|
|
|
330
406
|
options.rowSelection = this.rowSelection;
|
|
331
407
|
}
|
|
332
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
|
+
|
|
333
423
|
// Initialize grid API
|
|
334
424
|
this.gridApi = this.gridService.createApi(this.columnDefs, this.rowData, options);
|
|
335
425
|
|
|
426
|
+
// Initial state sync
|
|
427
|
+
this.rowGroupPanelShow = this.gridApi.getGridOption('rowGroupPanelShow') || 'never';
|
|
428
|
+
this.updateRowGroupColumns();
|
|
429
|
+
|
|
336
430
|
// Listen for grid state changes from API (filters, sorts, options)
|
|
337
431
|
this.gridService.gridStateChanged$.pipe(takeUntil(this.destroy$)).subscribe((event) => {
|
|
338
432
|
if (event.type === 'optionChanged' && event.key === 'sideBar') {
|
|
339
433
|
this.sideBarVisible = !!event.value;
|
|
340
434
|
}
|
|
435
|
+
if (event.type === 'optionChanged' && event.key === 'rowGroupPanelShow') {
|
|
436
|
+
this.rowGroupPanelShow = event.value || 'never';
|
|
437
|
+
}
|
|
341
438
|
if (event.type === 'selectionChanged') {
|
|
342
439
|
this.updateSelectionState();
|
|
343
440
|
|
|
@@ -346,7 +443,24 @@ export class ArgentGridComponent<TData = any>
|
|
|
346
443
|
if (this.canvasRenderer) {
|
|
347
444
|
this.canvasRenderer.render(); // This calls markAllDirty and schedules render
|
|
348
445
|
}
|
|
446
|
+
} else if (event.type === 'columnsChanged' || event.type === 'columnGroupExpanded') {
|
|
447
|
+
this.updateRowGroupColumns();
|
|
448
|
+
this.canvasRenderer?.render();
|
|
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
|
+
}
|
|
349
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.
|
|
350
464
|
this.canvasRenderer?.render();
|
|
351
465
|
}
|
|
352
466
|
this._cdr.detectChanges();
|
|
@@ -378,7 +492,9 @@ export class ArgentGridComponent<TData = any>
|
|
|
378
492
|
|
|
379
493
|
if (this.gridApi) {
|
|
380
494
|
this.gridApi.setRowData(newData || []);
|
|
381
|
-
this.canvasRenderer
|
|
495
|
+
if (this.canvasRenderer) {
|
|
496
|
+
this.canvasRenderer.render();
|
|
497
|
+
}
|
|
382
498
|
}
|
|
383
499
|
|
|
384
500
|
this.showOverlay = !newData || newData.length === 0;
|
|
@@ -406,37 +522,221 @@ export class ArgentGridComponent<TData = any>
|
|
|
406
522
|
Object.keys(newOptions).forEach((key) => {
|
|
407
523
|
this.gridApi.setGridOption(key as any, (newOptions as any)[key]);
|
|
408
524
|
});
|
|
525
|
+
|
|
526
|
+
if (newOptions.rowGroupPanelShow) {
|
|
527
|
+
this.rowGroupPanelShow = newOptions.rowGroupPanelShow;
|
|
528
|
+
}
|
|
529
|
+
|
|
409
530
|
this.canvasRenderer?.render();
|
|
410
531
|
}
|
|
411
532
|
this._cdr.detectChanges();
|
|
412
533
|
}
|
|
413
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);
|
|
596
|
+
}
|
|
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
|
+
|
|
414
714
|
getColumnWidth(col: Column | ColDef<TData> | ColGroupDef<TData>): number {
|
|
415
715
|
if ('children' in col) {
|
|
416
716
|
// Column group - sum children widths
|
|
417
717
|
return col.children.reduce((sum, child) => sum + this.getColumnWidth(child), 0);
|
|
418
718
|
}
|
|
419
|
-
return col.width || 150;
|
|
719
|
+
return Math.floor(col.width || 150);
|
|
420
720
|
}
|
|
421
721
|
|
|
422
722
|
getLeftPinnedColumns(): Column[] {
|
|
423
723
|
if (!this.gridApi) return [];
|
|
424
724
|
return this.gridApi.getAllColumns().filter((col) => {
|
|
425
|
-
return col
|
|
725
|
+
return isColumnVisible(col) && col.pinned === 'left';
|
|
426
726
|
});
|
|
427
727
|
}
|
|
428
728
|
|
|
429
729
|
getRightPinnedColumns(): Column[] {
|
|
430
730
|
if (!this.gridApi) return [];
|
|
431
731
|
return this.gridApi.getAllColumns().filter((col) => {
|
|
432
|
-
return col
|
|
732
|
+
return isColumnVisible(col) && col.pinned === 'right';
|
|
433
733
|
});
|
|
434
734
|
}
|
|
435
735
|
|
|
436
736
|
getNonPinnedColumns(): Column[] {
|
|
437
737
|
if (!this.gridApi) return [];
|
|
438
738
|
return this.gridApi.getAllColumns().filter((col) => {
|
|
439
|
-
return col
|
|
739
|
+
return isColumnVisible(col) && !col.pinned;
|
|
440
740
|
});
|
|
441
741
|
}
|
|
442
742
|
|
|
@@ -454,7 +754,7 @@ export class ArgentGridComponent<TData = any>
|
|
|
454
754
|
|
|
455
755
|
// It's likely a Column object, look up its ColDef
|
|
456
756
|
const colDef = this.getColumnDefForColumn(col as any);
|
|
457
|
-
return colDef ? colDef.sortable !== false : true;
|
|
757
|
+
return colDef && this.isColDef(colDef) ? colDef.sortable !== false : true;
|
|
458
758
|
}
|
|
459
759
|
|
|
460
760
|
getHeaderName(col: Column | ColDef<TData> | ColGroupDef<TData>): string {
|
|
@@ -509,7 +809,47 @@ export class ArgentGridComponent<TData = any>
|
|
|
509
809
|
if ((col as any).colId === 'ag-Grid-SelectionColumn') return false;
|
|
510
810
|
if ('children' in col) return false;
|
|
511
811
|
const colDef = this.getColumnDefForColumn(col as any);
|
|
512
|
-
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
|
+
}
|
|
513
853
|
}
|
|
514
854
|
|
|
515
855
|
onHeaderMenuClick(event: MouseEvent, col: Column | ColDef<TData> | ColGroupDef<TData>): void {
|
|
@@ -523,21 +863,29 @@ export class ArgentGridComponent<TData = any>
|
|
|
523
863
|
this.activeHeaderMenu = col;
|
|
524
864
|
this.headerMenuItems = this.getHeaderMenuItems(col as Column);
|
|
525
865
|
|
|
526
|
-
// Position menu below the icon using
|
|
866
|
+
// Position menu below the icon using coordinates relative to the grid container
|
|
527
867
|
const target = event.target as HTMLElement;
|
|
528
868
|
const rect = target.getBoundingClientRect();
|
|
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;
|
|
529
874
|
|
|
530
|
-
|
|
531
|
-
|
|
875
|
+
// Prevent menu from going off-container bounds
|
|
876
|
+
const containerWidth = this._elementRef.nativeElement.offsetWidth;
|
|
877
|
+
const containerHeight = this._elementRef.nativeElement.offsetHeight;
|
|
532
878
|
|
|
533
|
-
//
|
|
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
|
+
}
|
|
534
883
|
if (x < 0) x = 0;
|
|
535
|
-
if (x + 200 > window.innerWidth) x = window.innerWidth - 200;
|
|
536
884
|
|
|
537
885
|
// Check if menu would overflow bottom
|
|
538
886
|
const estimatedHeight = this.headerMenuItems.length * 30 + 20;
|
|
539
|
-
if (y + estimatedHeight >
|
|
540
|
-
y = Math.max(0, rect.top - estimatedHeight);
|
|
887
|
+
if (y + estimatedHeight > containerHeight) {
|
|
888
|
+
y = Math.max(0, rect.top - containerRect.top - estimatedHeight);
|
|
541
889
|
}
|
|
542
890
|
|
|
543
891
|
this.headerMenuPosition = { x, y };
|
|
@@ -569,7 +917,7 @@ export class ArgentGridComponent<TData = any>
|
|
|
569
917
|
|
|
570
918
|
// 2. Filter items
|
|
571
919
|
const colDef = this.getColumnDefForColumn(col);
|
|
572
|
-
if (colDef && colDef.filter !== false) {
|
|
920
|
+
if (colDef && this.isColDef(colDef) && colDef.filter !== false) {
|
|
573
921
|
const filterType = colDef.filter || 'text';
|
|
574
922
|
|
|
575
923
|
if (filterType === 'set') {
|
|
@@ -621,6 +969,15 @@ export class ArgentGridComponent<TData = any>
|
|
|
621
969
|
action: () => this.hideColumnMenu(),
|
|
622
970
|
});
|
|
623
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
|
+
|
|
624
981
|
return items;
|
|
625
982
|
}
|
|
626
983
|
|
|
@@ -689,8 +1046,6 @@ export class ArgentGridComponent<TData = any>
|
|
|
689
1046
|
'copyWithHeaders',
|
|
690
1047
|
'separator',
|
|
691
1048
|
'export',
|
|
692
|
-
'separator',
|
|
693
|
-
'resetColumns',
|
|
694
1049
|
]);
|
|
695
1050
|
}
|
|
696
1051
|
|
|
@@ -698,13 +1053,17 @@ export class ArgentGridComponent<TData = any>
|
|
|
698
1053
|
|
|
699
1054
|
this.activeContextMenu = true;
|
|
700
1055
|
|
|
701
|
-
// Position menu at mouse coordinates
|
|
702
|
-
|
|
703
|
-
let
|
|
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;
|
|
704
1060
|
|
|
705
|
-
// Prevent menu from going off-
|
|
706
|
-
|
|
707
|
-
|
|
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;
|
|
708
1067
|
|
|
709
1068
|
this.contextMenuPosition = { x, y };
|
|
710
1069
|
|
|
@@ -760,6 +1119,87 @@ export class ArgentGridComponent<TData = any>
|
|
|
760
1119
|
}
|
|
761
1120
|
}
|
|
762
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
|
+
|
|
763
1203
|
closeContextMenu(): void {
|
|
764
1204
|
this.activeContextMenu = false;
|
|
765
1205
|
this.contextMenuCell = null;
|
|
@@ -789,6 +1229,14 @@ export class ArgentGridComponent<TData = any>
|
|
|
789
1229
|
if (!field || !this.gridApi) return;
|
|
790
1230
|
|
|
791
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
|
+
|
|
792
1240
|
const colDef = 'field' in col ? (col as ColDef<TData>) : null;
|
|
793
1241
|
this.setFilterValueFormatter = colDef?.valueFormatter
|
|
794
1242
|
? (colDef.valueFormatter as any)
|
|
@@ -798,9 +1246,10 @@ export class ArgentGridComponent<TData = any>
|
|
|
798
1246
|
this.setFilterPosition = position;
|
|
799
1247
|
} else if (event) {
|
|
800
1248
|
const rect = (event.target as HTMLElement).getBoundingClientRect();
|
|
1249
|
+
const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
|
|
801
1250
|
this.setFilterPosition = {
|
|
802
|
-
x: rect.left,
|
|
803
|
-
y: rect.bottom + 5,
|
|
1251
|
+
x: rect.left - containerRect.left,
|
|
1252
|
+
y: rect.bottom - containerRect.top + 5,
|
|
804
1253
|
};
|
|
805
1254
|
}
|
|
806
1255
|
|
|
@@ -908,7 +1357,8 @@ export class ArgentGridComponent<TData = any>
|
|
|
908
1357
|
): void {
|
|
909
1358
|
this.activeFilterPopupColumn = col;
|
|
910
1359
|
const colDef = this.getColumnDefForColumn(col);
|
|
911
|
-
this.activeFilterPopupType =
|
|
1360
|
+
this.activeFilterPopupType =
|
|
1361
|
+
colDef && this.isColDef(colDef) && colDef.filter === 'number' ? 'number' : 'text';
|
|
912
1362
|
|
|
913
1363
|
// Initialize operator and values from current model or default
|
|
914
1364
|
const model = this.gridApi?.getFilterModel()[col.colId] as any;
|
|
@@ -921,7 +1371,11 @@ export class ArgentGridComponent<TData = any>
|
|
|
921
1371
|
this.filterPopupPosition = position;
|
|
922
1372
|
} else if (event) {
|
|
923
1373
|
const rect = (event.target as HTMLElement).getBoundingClientRect();
|
|
924
|
-
|
|
1374
|
+
const containerRect = this._elementRef.nativeElement.getBoundingClientRect();
|
|
1375
|
+
this.filterPopupPosition = {
|
|
1376
|
+
x: rect.left - containerRect.left,
|
|
1377
|
+
y: rect.bottom - containerRect.top + 5,
|
|
1378
|
+
};
|
|
925
1379
|
}
|
|
926
1380
|
|
|
927
1381
|
setTimeout(() => {
|
|
@@ -993,9 +1447,16 @@ export class ArgentGridComponent<TData = any>
|
|
|
993
1447
|
this._cdr.detectChanges();
|
|
994
1448
|
}
|
|
995
1449
|
|
|
1450
|
+
openColumnsPanel(): void {
|
|
1451
|
+
this.sideBarVisible = true;
|
|
1452
|
+
this.activeToolPanel = 'columns';
|
|
1453
|
+
this.closeHeaderMenu();
|
|
1454
|
+
this._cdr.detectChanges();
|
|
1455
|
+
}
|
|
1456
|
+
|
|
996
1457
|
toggleColumnVisibility(col: Column): void {
|
|
997
1458
|
const colDef = this.getColumnDefForColumn(col);
|
|
998
|
-
if (colDef) {
|
|
1459
|
+
if (colDef && this.isColDef(colDef)) {
|
|
999
1460
|
colDef.hide = col.visible; // Toggle
|
|
1000
1461
|
this.initializeGrid(); // Re-initialize to handle visibility changes correctly
|
|
1001
1462
|
this.canvasRenderer?.render();
|
|
@@ -1096,7 +1557,7 @@ export class ArgentGridComponent<TData = any>
|
|
|
1096
1557
|
|
|
1097
1558
|
// Update original ColDef to ensure persistence
|
|
1098
1559
|
const colDef = this.getColumnDefForColumn(col);
|
|
1099
|
-
if (colDef) {
|
|
1560
|
+
if (colDef && this.isColDef(colDef)) {
|
|
1100
1561
|
colDef.sort = direction;
|
|
1101
1562
|
}
|
|
1102
1563
|
|
|
@@ -1113,7 +1574,7 @@ export class ArgentGridComponent<TData = any>
|
|
|
1113
1574
|
|
|
1114
1575
|
// Update the original column definition
|
|
1115
1576
|
const colDef = this.getColumnDefForColumn(col);
|
|
1116
|
-
if (colDef) {
|
|
1577
|
+
if (colDef && this.isColDef(colDef)) {
|
|
1117
1578
|
colDef.hide = true;
|
|
1118
1579
|
}
|
|
1119
1580
|
|
|
@@ -1132,7 +1593,7 @@ export class ArgentGridComponent<TData = any>
|
|
|
1132
1593
|
|
|
1133
1594
|
// Update the original column definition
|
|
1134
1595
|
const colDef = this.getColumnDefForColumn(col);
|
|
1135
|
-
if (colDef) {
|
|
1596
|
+
if (colDef && this.isColDef(colDef)) {
|
|
1136
1597
|
colDef.pinned = pin as any;
|
|
1137
1598
|
}
|
|
1138
1599
|
|
|
@@ -1143,76 +1604,43 @@ export class ArgentGridComponent<TData = any>
|
|
|
1143
1604
|
this.closeHeaderMenu();
|
|
1144
1605
|
}
|
|
1145
1606
|
|
|
1146
|
-
onColumnDropped(event: CdkDragDrop<any
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
// Get current groups (using internal Columns)
|
|
1150
|
-
const left = [...this.getLeftPinnedColumns()];
|
|
1151
|
-
const center = [...this.getNonPinnedColumns()];
|
|
1152
|
-
const right = [...this.getRightPinnedColumns()];
|
|
1607
|
+
onColumnDropped(event: CdkDragDrop<any>, pinned: 'left' | 'right' | 'none'): void {
|
|
1608
|
+
const col = event.item.data as Column;
|
|
1609
|
+
if (!col) return;
|
|
1153
1610
|
|
|
1154
|
-
const
|
|
1155
|
-
'left-pinned': left,
|
|
1156
|
-
scrollable: center,
|
|
1157
|
-
'right-pinned': right,
|
|
1158
|
-
};
|
|
1159
|
-
|
|
1160
|
-
const previousContainerData = containerMap[event.previousContainer.id];
|
|
1161
|
-
const currentContainerData = containerMap[event.container.id];
|
|
1611
|
+
const targetPinned = pinned === 'none' ? false : pinned;
|
|
1162
1612
|
|
|
1163
|
-
if (
|
|
1164
|
-
|
|
1165
|
-
} else {
|
|
1166
|
-
transferArrayItem(
|
|
1167
|
-
previousContainerData,
|
|
1168
|
-
currentContainerData,
|
|
1169
|
-
event.previousIndex,
|
|
1170
|
-
event.currentIndex
|
|
1171
|
-
);
|
|
1172
|
-
|
|
1173
|
-
// Update pinned state of the moved column in its original definition
|
|
1174
|
-
const movedCol = currentContainerData[event.currentIndex] as Column;
|
|
1175
|
-
const colDef = this.getColumnDefForColumn(movedCol);
|
|
1176
|
-
if (colDef) {
|
|
1177
|
-
colDef.pinned = pinType === 'none' ? null : (pinType as any);
|
|
1178
|
-
}
|
|
1613
|
+
if (col.pinned !== targetPinned) {
|
|
1614
|
+
this.gridApi.setColumnPinned(col, targetPinned);
|
|
1179
1615
|
}
|
|
1180
1616
|
|
|
1181
|
-
|
|
1182
|
-
const orderedVisibleColDefs: (ColDef<TData> | ColGroupDef<TData>)[] = [];
|
|
1183
|
-
[...left, ...center, ...right].forEach((col) => {
|
|
1184
|
-
const def = this.getColumnDefForColumn(col);
|
|
1185
|
-
if (def) orderedVisibleColDefs.push(def);
|
|
1186
|
-
});
|
|
1187
|
-
|
|
1188
|
-
// Reconstruct full columnDefs array, maintaining hidden columns
|
|
1189
|
-
const hidden = this.columnDefs.filter((c) => {
|
|
1190
|
-
if ('children' in c) return false;
|
|
1191
|
-
return (c as ColDef).hide;
|
|
1192
|
-
});
|
|
1617
|
+
this.gridApi.moveColumn(col, event.currentIndex);
|
|
1193
1618
|
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
this.onColumnDefsChanged(newDefs);
|
|
1619
|
+
this.canvasRenderer?.render();
|
|
1620
|
+
this._cdr.detectChanges();
|
|
1197
1621
|
}
|
|
1198
1622
|
|
|
1199
1623
|
// --- Column Resizing Logic ---
|
|
1200
1624
|
|
|
1201
|
-
isResizable(
|
|
1202
|
-
if (
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
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;
|
|
1206
1634
|
}
|
|
1207
1635
|
|
|
1208
|
-
onResizeMouseDown(event: MouseEvent,
|
|
1636
|
+
onResizeMouseDown(event: MouseEvent, item: Column | ColumnGroup): void {
|
|
1209
1637
|
event.stopPropagation();
|
|
1210
1638
|
event.preventDefault();
|
|
1211
1639
|
|
|
1212
1640
|
this.isResizing = true;
|
|
1213
|
-
this.
|
|
1641
|
+
this.resizeItem = item;
|
|
1214
1642
|
this.resizeStartX = event.clientX;
|
|
1215
|
-
this.resizeStartWidth =
|
|
1643
|
+
this.resizeStartWidth = this.getItemWidth(item);
|
|
1216
1644
|
|
|
1217
1645
|
const mouseMoveHandler = (e: MouseEvent) => this.onResizeMouseMove(e);
|
|
1218
1646
|
const mouseUpHandler = () => {
|
|
@@ -1226,19 +1654,12 @@ export class ArgentGridComponent<TData = any>
|
|
|
1226
1654
|
}
|
|
1227
1655
|
|
|
1228
1656
|
private onResizeMouseMove(event: MouseEvent): void {
|
|
1229
|
-
if (!this.isResizing || !this.
|
|
1657
|
+
if (!this.isResizing || !this.resizeItem) return;
|
|
1230
1658
|
|
|
1231
1659
|
const deltaX = event.clientX - this.resizeStartX;
|
|
1232
1660
|
const newWidth = Math.max(20, this.resizeStartWidth + deltaX);
|
|
1233
1661
|
|
|
1234
|
-
|
|
1235
|
-
this.resizeColumn.width = newWidth;
|
|
1236
|
-
|
|
1237
|
-
// Update original ColDef
|
|
1238
|
-
const colDef = this.getColumnDefForColumn(this.resizeColumn);
|
|
1239
|
-
if (colDef) {
|
|
1240
|
-
colDef.width = newWidth;
|
|
1241
|
-
}
|
|
1662
|
+
this.applyResize(this.resizeItem!, newWidth);
|
|
1242
1663
|
|
|
1243
1664
|
// Force re-render
|
|
1244
1665
|
this.canvasRenderer?.render();
|
|
@@ -1247,7 +1668,29 @@ export class ArgentGridComponent<TData = any>
|
|
|
1247
1668
|
|
|
1248
1669
|
private onResizeMouseUp(): void {
|
|
1249
1670
|
this.isResizing = false;
|
|
1250
|
-
this.
|
|
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
|
+
}
|
|
1251
1694
|
}
|
|
1252
1695
|
|
|
1253
1696
|
// --- Floating Filter Logic ---
|
|
@@ -1297,7 +1740,7 @@ export class ArgentGridComponent<TData = any>
|
|
|
1297
1740
|
private filterTimeout: any;
|
|
1298
1741
|
onFloatingFilterInput(event: Event, col: Column | ColDef<TData> | ColGroupDef<TData>): void {
|
|
1299
1742
|
const colDef = this.getColumnDefForColumn(col as any);
|
|
1300
|
-
if (!colDef ||
|
|
1743
|
+
if (!colDef || !this.isColDef(colDef)) return;
|
|
1301
1744
|
|
|
1302
1745
|
const input = event.target as HTMLInputElement;
|
|
1303
1746
|
const value = input.value;
|
|
@@ -1354,7 +1797,7 @@ export class ArgentGridComponent<TData = any>
|
|
|
1354
1797
|
input: HTMLInputElement
|
|
1355
1798
|
): void {
|
|
1356
1799
|
const colDef = this.getColumnDefForColumn(col as any);
|
|
1357
|
-
if (!colDef ||
|
|
1800
|
+
if (!colDef || !this.isColDef(colDef)) return;
|
|
1358
1801
|
|
|
1359
1802
|
input.value = '';
|
|
1360
1803
|
const colId = (col as any).colId || (col as any).field?.toString() || '';
|
|
@@ -1390,7 +1833,7 @@ export class ArgentGridComponent<TData = any>
|
|
|
1390
1833
|
|
|
1391
1834
|
// Check if cell is editable
|
|
1392
1835
|
const colDef = this.getColumnDefForColumn(column);
|
|
1393
|
-
if (colDef && colDef.editable === false) return;
|
|
1836
|
+
if (colDef && this.isColDef(colDef) && colDef.editable === false) return;
|
|
1394
1837
|
|
|
1395
1838
|
// If already editing another cell, stop it first
|
|
1396
1839
|
if (this.isEditing) {
|
|
@@ -1404,18 +1847,18 @@ export class ArgentGridComponent<TData = any>
|
|
|
1404
1847
|
this.editingValue = value !== null && value !== undefined ? String(value) : '';
|
|
1405
1848
|
|
|
1406
1849
|
// Calculate editor position based on row and column
|
|
1407
|
-
const columns = this.gridApi.getAllColumns().filter((c) => c
|
|
1850
|
+
const columns = this.gridApi.getAllColumns().filter((c) => isColumnVisible(c));
|
|
1408
1851
|
let x = 0;
|
|
1409
1852
|
for (const col of columns) {
|
|
1410
1853
|
if (col.colId === colId) break;
|
|
1411
|
-
x += col.width;
|
|
1854
|
+
x += Math.floor(col.width || 150);
|
|
1412
1855
|
}
|
|
1413
1856
|
|
|
1414
1857
|
this.editorPosition = {
|
|
1415
1858
|
x: x - this.canvasRenderer.currentScrollLeft,
|
|
1416
|
-
y: rowIndex * this.
|
|
1417
|
-
width: column.width,
|
|
1418
|
-
height: this.
|
|
1859
|
+
y: rowIndex * this.effectiveRowHeight - this.canvasRenderer.currentScrollTop,
|
|
1860
|
+
width: Math.floor(column.width),
|
|
1861
|
+
height: this.effectiveRowHeight,
|
|
1419
1862
|
};
|
|
1420
1863
|
|
|
1421
1864
|
this.isEditing = true;
|
|
@@ -1558,30 +2001,37 @@ export class ArgentGridComponent<TData = any>
|
|
|
1558
2001
|
}
|
|
1559
2002
|
}
|
|
1560
2003
|
private getColumnDefForColumn(
|
|
1561
|
-
column: Column | ColDef<TData> | ColGroupDef<TData>
|
|
1562
|
-
): ColDef<TData> | null {
|
|
2004
|
+
column: Column | ColumnGroup | ColDef<TData> | ColGroupDef<TData>
|
|
2005
|
+
): ColDef<TData> | ColGroupDef<TData> | null {
|
|
1563
2006
|
if (!this.columnDefs) return null;
|
|
1564
2007
|
|
|
1565
|
-
const colId =
|
|
2008
|
+
const colId =
|
|
2009
|
+
(column as any).colId || (column as any).field?.toString() || (column as any).groupId;
|
|
1566
2010
|
if (!colId) return null;
|
|
1567
2011
|
|
|
1568
2012
|
const defaultColDef = this.gridOptions?.defaultColDef || {};
|
|
1569
2013
|
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
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;
|
|
1581
2025
|
}
|
|
1582
2026
|
}
|
|
1583
|
-
|
|
1584
|
-
|
|
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);
|
|
1585
2035
|
}
|
|
1586
2036
|
|
|
1587
2037
|
onRowClick(rowIndex: number, event: MouseEvent): void {
|