@cocoar/data-grid 0.1.0 → 0.2.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.
@@ -1,7 +1,9 @@
1
- import { Subject, takeUntil } from 'rxjs';
2
- import { themeQuartz } from 'ag-grid-community';
1
+ import { Subject, takeUntil, Observable } from 'rxjs';
3
2
  import * as i0 from '@angular/core';
4
- import { inject, Input, Directive } from '@angular/core';
3
+ import { inject, ChangeDetectionStrategy, Component, HostListener, Input, Directive } from '@angular/core';
4
+ import { CoarTagComponent, CoarIconComponent } from '@cocoar/ui/components';
5
+ import { COAR_I18N_PROVIDER, CoarDatePipe } from '@cocoar/localization';
6
+ import { themeQuartz } from 'ag-grid-community';
5
7
  import { AgGridAngular } from 'ag-grid-angular';
6
8
 
7
9
  /**
@@ -192,6 +194,44 @@ class CoarGridColumnBuilder {
192
194
  return this;
193
195
  }
194
196
  // ============================================================
197
+ // Quick Filter
198
+ // ============================================================
199
+ /** Set quick filter text extractor or disable quick filtering for this column */
200
+ quickFilter(fn) {
201
+ if (typeof fn === 'boolean') {
202
+ this.#colDef.getQuickFilterText = fn ? undefined : () => '';
203
+ }
204
+ else {
205
+ this.#colDef.getQuickFilterText = fn;
206
+ }
207
+ return this;
208
+ }
209
+ // ============================================================
210
+ // Sorting
211
+ // ============================================================
212
+ /** Set custom sort comparator */
213
+ comparator(fn) {
214
+ this.#colDef.comparator = fn;
215
+ return this;
216
+ }
217
+ // ============================================================
218
+ // Row Drag
219
+ // ============================================================
220
+ /** Enable row drag on this column */
221
+ rowDrag(value = true) {
222
+ this.#colDef.rowDrag = value;
223
+ return this;
224
+ }
225
+ // ============================================================
226
+ // Cell Renderer (config pattern)
227
+ // ============================================================
228
+ /** Set cell renderer with config object (params wrapped in `config` key) */
229
+ cellRendererConfig(component, config) {
230
+ this.#colDef.cellRenderer = component;
231
+ this.#colDef.cellRendererParams = { ...this.#colDef.cellRendererParams, config };
232
+ return this;
233
+ }
234
+ // ============================================================
195
235
  // Advanced Options
196
236
  // ============================================================
197
237
  /** Set any AG Grid ColDef option directly */
@@ -213,6 +253,158 @@ class CoarGridColumnBuilder {
213
253
  }
214
254
  }
215
255
 
256
+ class CoarTagCellRendererComponent {
257
+ tags = [];
258
+ size = 's';
259
+ config = {};
260
+ i18n = inject(COAR_I18N_PROVIDER, { optional: true });
261
+ agInit(params) {
262
+ this.config = params.config ?? {};
263
+ this.size = this.config.size ?? 's';
264
+ this.updateTags(params.value);
265
+ }
266
+ refresh(params) {
267
+ this.updateTags(params.value);
268
+ return true;
269
+ }
270
+ updateTags(value) {
271
+ const rawLabels = this.extractLabels(value);
272
+ this.tags = rawLabels.map((label) => ({
273
+ label: this.translateLabel(label),
274
+ variant: this.resolveVariant(label),
275
+ }));
276
+ }
277
+ extractLabels(value) {
278
+ if (value == null)
279
+ return [];
280
+ if (Array.isArray(value)) {
281
+ return value.map((item) => this.labelFromItem(item));
282
+ }
283
+ if (typeof value === 'string') {
284
+ const separator = this.config.separator ?? ',';
285
+ return value
286
+ .split(separator)
287
+ .map((s) => s.trim())
288
+ .filter(Boolean);
289
+ }
290
+ return [String(value)];
291
+ }
292
+ labelFromItem(item) {
293
+ if (item != null && typeof item === 'object' && this.config.labelProperty) {
294
+ return String(item[this.config.labelProperty] ?? '');
295
+ }
296
+ return String(item ?? '');
297
+ }
298
+ translateLabel(label) {
299
+ if (this.config.i18nPrefix && this.i18n) {
300
+ return this.i18n.t(this.config.i18nPrefix + label);
301
+ }
302
+ return label;
303
+ }
304
+ resolveVariant(label) {
305
+ return this.config.variantMap?.[label] ?? this.config.variant ?? 'neutral';
306
+ }
307
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarTagCellRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
308
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: CoarTagCellRendererComponent, isStandalone: true, selector: "coar-tag-cell-renderer", ngImport: i0, template: `
309
+ @for (tag of tags; track tag.label) {
310
+ <coar-tag [variant]="tag.variant" [size]="size">{{ tag.label }}</coar-tag>
311
+ }
312
+ `, isInline: true, styles: [":host{display:flex;align-items:center;gap:var(--coar-spacing-xs, 4px);flex-wrap:wrap;height:100%}\n"], dependencies: [{ kind: "component", type: CoarTagComponent, selector: "coar-tag", inputs: ["elevated", "borderless", "variant", "size", "closable"], outputs: ["closed"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
313
+ }
314
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarTagCellRendererComponent, decorators: [{
315
+ type: Component,
316
+ args: [{ selector: 'coar-tag-cell-renderer', standalone: true, imports: [CoarTagComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
317
+ @for (tag of tags; track tag.label) {
318
+ <coar-tag [variant]="tag.variant" [size]="size">{{ tag.label }}</coar-tag>
319
+ }
320
+ `, styles: [":host{display:flex;align-items:center;gap:var(--coar-spacing-xs, 4px);flex-wrap:wrap;height:100%}\n"] }]
321
+ }] });
322
+
323
+ class CoarIconCellRendererComponent {
324
+ iconName = '';
325
+ size = 's';
326
+ source;
327
+ color = 'inherit';
328
+ onClick;
329
+ params;
330
+ agInit(params) {
331
+ this.params = params;
332
+ const config = params.config ?? {};
333
+ this.size = config.size ?? 's';
334
+ this.source = config.source;
335
+ this.color = config.color ?? 'inherit';
336
+ this.onClick = config.onClick;
337
+ this.iconName = params.value ?? '';
338
+ }
339
+ refresh(params) {
340
+ this.params = params;
341
+ this.iconName = params.value ?? '';
342
+ return true;
343
+ }
344
+ handleClick() {
345
+ this.onClick?.(this.params);
346
+ }
347
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarIconCellRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
348
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.6", type: CoarIconCellRendererComponent, isStandalone: true, selector: "coar-icon-cell-renderer", host: { listeners: { "click": "handleClick()" }, properties: { "class.clickable": "!!onClick" } }, ngImport: i0, template: `
349
+ @if (iconName) {
350
+ <coar-icon
351
+ [name]="iconName"
352
+ [size]="size"
353
+ [source]="source"
354
+ [color]="color"
355
+ />
356
+ }
357
+ `, isInline: true, styles: [":host{display:flex;align-items:center;justify-content:center;height:100%}:host(.clickable){cursor:pointer}\n"], dependencies: [{ kind: "component", type: CoarIconComponent, selector: "coar-icon", inputs: ["name", "source", "size", "rotate", "rotateTransition", "spin", "color", "label"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
358
+ }
359
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarIconCellRendererComponent, decorators: [{
360
+ type: Component,
361
+ args: [{ selector: 'coar-icon-cell-renderer', standalone: true, imports: [CoarIconComponent], changeDetection: ChangeDetectionStrategy.OnPush, template: `
362
+ @if (iconName) {
363
+ <coar-icon
364
+ [name]="iconName"
365
+ [size]="size"
366
+ [source]="source"
367
+ [color]="color"
368
+ />
369
+ }
370
+ `, host: {
371
+ '[class.clickable]': '!!onClick',
372
+ '(click)': 'handleClick()',
373
+ }, styles: [":host{display:flex;align-items:center;justify-content:center;height:100%}:host(.clickable){cursor:pointer}\n"] }]
374
+ }] });
375
+
376
+ class CoarDateCellRendererComponent {
377
+ dateValue = null;
378
+ agInit(params) {
379
+ this.updateValue(params.value);
380
+ }
381
+ refresh(params) {
382
+ this.updateValue(params.value);
383
+ return true;
384
+ }
385
+ updateValue(value) {
386
+ if (!value) {
387
+ this.dateValue = null;
388
+ return;
389
+ }
390
+ if (value instanceof Date) {
391
+ this.dateValue = value;
392
+ }
393
+ else if (typeof value === 'string') {
394
+ this.dateValue = value;
395
+ }
396
+ else {
397
+ this.dateValue = null;
398
+ }
399
+ }
400
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarDateCellRendererComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
401
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: CoarDateCellRendererComponent, isStandalone: true, selector: "coar-date-cell-renderer", ngImport: i0, template: `{{ dateValue | coarDate }}`, isInline: true, styles: [":host{display:flex;align-items:center;height:100%}\n"], dependencies: [{ kind: "pipe", type: CoarDatePipe, name: "coarDate" }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
402
+ }
403
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarDateCellRendererComponent, decorators: [{
404
+ type: Component,
405
+ args: [{ selector: 'coar-date-cell-renderer', standalone: true, imports: [CoarDatePipe], changeDetection: ChangeDetectionStrategy.OnPush, template: `{{ dateValue | coarDate }}`, styles: [":host{display:flex;align-items:center;height:100%}\n"] }]
406
+ }] });
407
+
216
408
  /**
217
409
  * Factory for creating typed column builders.
218
410
  * Provides convenient methods for common column types.
@@ -224,6 +416,8 @@ class CoarGridColumnBuilder {
224
416
  * .columns([
225
417
  * col => col.field('name').header('Name').flex(1),
226
418
  * col => col.field('createdAt').header('Created').width(150),
419
+ * col => col.tag('status', { variantMap: { active: 'success' } }),
420
+ * col => col.icon('type', { size: 's' }),
227
421
  * ])
228
422
  * ```
229
423
  */
@@ -235,11 +429,12 @@ class CoarGridColumnFactory {
235
429
  return new CoarGridColumnBuilder(fieldName);
236
430
  }
237
431
  /**
238
- * Create a date column with standard formatting
432
+ * Create a date column with standard or custom formatting.
433
+ *
434
+ * @param format - Preset name ('short' | 'long' | 'datetime'), Intl options object, or custom formatter function
239
435
  */
240
436
  date(fieldName, format = 'short') {
241
437
  const builder = new CoarGridColumnBuilder(fieldName);
242
- // Add basic date formatting
243
438
  builder.valueFormatter((params) => {
244
439
  const value = params.value;
245
440
  if (!value)
@@ -247,7 +442,15 @@ class CoarGridColumnFactory {
247
442
  const date = value instanceof Date ? value : new Date(value);
248
443
  if (isNaN(date.getTime()))
249
444
  return String(value);
250
- // Use Intl for locale-aware formatting
445
+ // Custom formatter function
446
+ if (typeof format === 'function') {
447
+ return format(date);
448
+ }
449
+ // Intl options object
450
+ if (typeof format === 'object') {
451
+ return new Intl.DateTimeFormat(undefined, format).format(date);
452
+ }
453
+ // Preset string
251
454
  switch (format) {
252
455
  case 'short':
253
456
  return date.toLocaleDateString();
@@ -314,6 +517,56 @@ class CoarGridColumnFactory {
314
517
  });
315
518
  return builder;
316
519
  }
520
+ /**
521
+ * Create a tag column that renders values as `<coar-tag>` elements.
522
+ *
523
+ * Supports string (split by separator), array, and object array values.
524
+ *
525
+ * @param config - Tag rendering configuration (variantMap, size, i18nPrefix, etc.)
526
+ */
527
+ tag(fieldName, config) {
528
+ const builder = new CoarGridColumnBuilder(fieldName);
529
+ builder.cellRendererConfig(CoarTagCellRendererComponent, config ?? {});
530
+ builder.sortable();
531
+ // Alphabetical comparator on joined tag labels
532
+ const separator = config?.separator ?? ',';
533
+ builder.comparator((valueA, valueB) => {
534
+ const normalize = (v) => {
535
+ if (Array.isArray(v))
536
+ return v.map(String).sort().join(',');
537
+ if (typeof v === 'string')
538
+ return v.split(separator).map((s) => s.trim()).sort().join(',');
539
+ return String(v ?? '');
540
+ };
541
+ return normalize(valueA).localeCompare(normalize(valueB));
542
+ });
543
+ return builder;
544
+ }
545
+ /**
546
+ * Create an icon column that renders values as `<coar-icon>` elements.
547
+ *
548
+ * The cell value is used as the icon name.
549
+ *
550
+ * @param config - Icon rendering configuration (size, source, color, onClick)
551
+ */
552
+ icon(fieldName, config) {
553
+ const builder = new CoarGridColumnBuilder(fieldName);
554
+ builder.cellRendererConfig(CoarIconCellRendererComponent, config ?? {});
555
+ return builder;
556
+ }
557
+ /**
558
+ * Create a date column with locale-aware rendering via `CoarDatePipe`.
559
+ *
560
+ * Unlike `date()`, this uses a cell renderer component for full locale integration.
561
+ *
562
+ * @param config - Date rendering configuration (showSeconds, customFormat)
563
+ */
564
+ localDate(fieldName, config) {
565
+ const builder = new CoarGridColumnBuilder(fieldName);
566
+ builder.cellRendererConfig(CoarDateCellRendererComponent, config ?? {});
567
+ builder.sortable();
568
+ return builder;
569
+ }
317
570
  }
318
571
 
319
572
  /**
@@ -402,6 +655,14 @@ class CoarGridBuilder {
402
655
  #columnDefs = [];
403
656
  #rowData = null;
404
657
  #rowData$;
658
+ // Deferred state (applied after grid ready)
659
+ #columnState;
660
+ #openRows$;
661
+ #sortFilterTriggers = [];
662
+ #externalFilterTriggers = [];
663
+ // Viewport event handlers (wired by directive)
664
+ #viewportClickHandler;
665
+ #viewportContextMenuHandler;
405
666
  /** Observable that emits when grid is ready */
406
667
  gridReady$ = this.#gridReady$.asObservable();
407
668
  constructor() {
@@ -421,7 +682,6 @@ class CoarGridBuilder {
421
682
  #createDefaultOptions() {
422
683
  return {
423
684
  theme: cocoarTheme,
424
- suppressPropertyNamesCheck: true,
425
685
  animateRows: true,
426
686
  rowSelection: undefined,
427
687
  };
@@ -429,6 +689,15 @@ class CoarGridBuilder {
429
689
  #mergeOptions(options) {
430
690
  this.#gridOptions = { ...this.#gridOptions, ...options };
431
691
  }
692
+ #composeHandler(existing, handler) {
693
+ if (existing) {
694
+ return (event) => {
695
+ existing(event);
696
+ handler(event);
697
+ };
698
+ }
699
+ return handler;
700
+ }
432
701
  // ============================================================
433
702
  // Column Configuration
434
703
  // ============================================================
@@ -479,7 +748,8 @@ class CoarGridBuilder {
479
748
  // ============================================================
480
749
  /** Enable row selection */
481
750
  rowSelection(mode) {
482
- this.#mergeOptions({ rowSelection: mode });
751
+ const mapped = mode === 'single' ? 'singleRow' : 'multiRow';
752
+ this.#mergeOptions({ rowSelection: { mode: mapped } });
483
753
  return this;
484
754
  }
485
755
  // ============================================================
@@ -496,15 +766,71 @@ class CoarGridBuilder {
496
766
  return this;
497
767
  }
498
768
  // ============================================================
769
+ // Sorting
770
+ // ============================================================
771
+ /** Set initial sort column and direction */
772
+ defaultSort(field, direction) {
773
+ this.#gridOptions.initialState = {
774
+ ...this.#gridOptions.initialState,
775
+ sort: {
776
+ sortModel: [{ colId: field, sort: direction }],
777
+ },
778
+ };
779
+ return this;
780
+ }
781
+ /** Set custom post-sort function to reorder rows after AG Grid sorts */
782
+ sortFunction(fn) {
783
+ this.#mergeOptions({ postSortRows: fn });
784
+ return this;
785
+ }
786
+ /** Re-trigger sort and filter when the given observable emits */
787
+ updateSortAndFilterWhen(trigger) {
788
+ this.#sortFilterTriggers.push(trigger);
789
+ return this;
790
+ }
791
+ // ============================================================
792
+ // Column State
793
+ // ============================================================
794
+ /** Merge column state to restore column widths, order, visibility */
795
+ columnState(state) {
796
+ this.#columnState = state;
797
+ return this;
798
+ }
799
+ // ============================================================
800
+ // Tree / Group Data
801
+ // ============================================================
802
+ /** Set which parent rows are expanded (observable of row IDs) */
803
+ openRows(openRows$) {
804
+ this.#openRows$ = openRows$;
805
+ return this;
806
+ }
807
+ // ============================================================
808
+ // Editing
809
+ // ============================================================
810
+ /** Enable full-row editing mode */
811
+ fullRowEdit(value = true) {
812
+ this.#mergeOptions({ editType: value ? 'fullRow' : undefined });
813
+ return this;
814
+ }
815
+ /** Stop cell editing when cells lose focus */
816
+ stopEditingWhenCellsLoseFocus(value = true) {
817
+ this.#mergeOptions({ stopEditingWhenCellsLoseFocus: value });
818
+ return this;
819
+ }
820
+ // ============================================================
821
+ // Resize
822
+ // ============================================================
823
+ /** Enable shift-key column resize mode */
824
+ shiftResizeMode(value = true) {
825
+ this.#mergeOptions({ colResizeDefault: value ? 'shift' : undefined });
826
+ return this;
827
+ }
828
+ // ============================================================
499
829
  // Event Handlers
500
830
  // ============================================================
501
831
  /** Handle grid ready event */
502
832
  onGridReady(handler) {
503
- const existing = this.#gridOptions.onGridReady;
504
- this.#gridOptions.onGridReady = (event) => {
505
- existing?.(event);
506
- handler(event);
507
- };
833
+ this.#gridOptions.onGridReady = this.#composeHandler(this.#gridOptions.onGridReady, handler);
508
834
  return this;
509
835
  }
510
836
  /** Handle row click */
@@ -527,6 +853,39 @@ class CoarGridBuilder {
527
853
  this.#mergeOptions({ onCellDoubleClicked: handler });
528
854
  return this;
529
855
  }
856
+ /** Handle grid size changed event */
857
+ onGridSizeChanged(handler) {
858
+ this.#gridOptions.onGridSizeChanged = this.#composeHandler(this.#gridOptions.onGridSizeChanged, handler);
859
+ return this;
860
+ }
861
+ /** Handle cell context menu (right-click). Ctrl+click is passed through to the browser. */
862
+ onCellContextMenu(handler) {
863
+ this.#mergeOptions({
864
+ onCellContextMenu: (event) => {
865
+ const mouseEvent = event.event;
866
+ if (mouseEvent?.ctrlKey)
867
+ return;
868
+ handler(event);
869
+ },
870
+ });
871
+ return this;
872
+ }
873
+ /**
874
+ * Handle click on the grid viewport (empty area outside cells).
875
+ * Wired by the directive via HostListener.
876
+ */
877
+ onViewportClick(handler) {
878
+ this.#viewportClickHandler = handler;
879
+ return this;
880
+ }
881
+ /**
882
+ * Handle context menu on the grid viewport (empty area outside cells).
883
+ * Wired by the directive via HostListener.
884
+ */
885
+ onViewportContextMenu(handler) {
886
+ this.#viewportContextMenuHandler = handler;
887
+ return this;
888
+ }
530
889
  // ============================================================
531
890
  // External Filtering
532
891
  // ============================================================
@@ -538,6 +897,11 @@ class CoarGridBuilder {
538
897
  });
539
898
  return this;
540
899
  }
900
+ /** Re-trigger external filter when the given observable emits */
901
+ updateExternalFilterWhen(trigger) {
902
+ this.#externalFilterTriggers.push(trigger);
903
+ return this;
904
+ }
541
905
  // ============================================================
542
906
  // Grid Options
543
907
  // ============================================================
@@ -585,6 +949,47 @@ class CoarGridBuilder {
585
949
  }
586
950
  });
587
951
  }
952
+ // Apply column state
953
+ if (this.#columnState) {
954
+ if (this.#columnState instanceof Observable) {
955
+ this.#columnState
956
+ .pipe(takeUntil(this.#destroy$))
957
+ .subscribe((state) => {
958
+ if (state) {
959
+ event.api.applyColumnState({ state, applyOrder: true });
960
+ }
961
+ });
962
+ }
963
+ else {
964
+ event.api.applyColumnState({ state: this.#columnState, applyOrder: true });
965
+ }
966
+ }
967
+ // Subscribe to sort/filter triggers
968
+ for (const trigger of this.#sortFilterTriggers) {
969
+ trigger.pipe(takeUntil(this.#destroy$)).subscribe(() => {
970
+ event.api.onSortChanged();
971
+ event.api.onFilterChanged();
972
+ });
973
+ }
974
+ // Subscribe to external filter triggers
975
+ for (const trigger of this.#externalFilterTriggers) {
976
+ trigger.pipe(takeUntil(this.#destroy$)).subscribe(() => {
977
+ event.api.onFilterChanged();
978
+ });
979
+ }
980
+ // Subscribe to open rows
981
+ if (this.#openRows$) {
982
+ this.#openRows$.pipe(takeUntil(this.#destroy$)).subscribe((openRowIds) => {
983
+ event.api.forEachNode((node) => {
984
+ if (node.group || node.master) {
985
+ const shouldBeOpen = openRowIds.includes(node.key ?? node.id ?? '');
986
+ if (node.expanded !== shouldBeOpen) {
987
+ node.setExpanded(shouldBeOpen);
988
+ }
989
+ }
990
+ });
991
+ });
992
+ }
588
993
  });
589
994
  }
590
995
  /** @internal Called by the directive on destroy */
@@ -593,6 +998,18 @@ class CoarGridBuilder {
593
998
  this.#destroy$.complete();
594
999
  this.#gridReady$.complete();
595
1000
  }
1001
+ /** @internal Get viewport click handler (for directive) */
1002
+ _getViewportClickHandler() {
1003
+ return this.#viewportClickHandler;
1004
+ }
1005
+ /** @internal Get viewport context menu handler (for directive) */
1006
+ _getViewportContextMenuHandler() {
1007
+ return this.#viewportContextMenuHandler;
1008
+ }
1009
+ /** @internal Check if a cell context menu handler is registered (for directive) */
1010
+ _hasCellContextMenuHandler() {
1011
+ return this.#gridOptions.onCellContextMenu != null;
1012
+ }
596
1013
  /** Get column definitions (for directive) */
597
1014
  _getColumnDefs() {
598
1015
  return this.#columnDefs;
@@ -607,6 +1024,11 @@ class CoarGridBuilder {
607
1024
  }
608
1025
  }
609
1026
 
1027
+ const VIEWPORT_CLASSES = ['ag-body-viewport', 'ag-center-cols-viewport'];
1028
+ function isViewportTarget(event) {
1029
+ const target = event.target;
1030
+ return VIEWPORT_CLASSES.some((cls) => target.classList.contains(cls));
1031
+ }
610
1032
  /**
611
1033
  * Directive that binds a CoarGridBuilder to an AG Grid instance.
612
1034
  *
@@ -649,8 +1071,32 @@ class CoarDataGridDirective {
649
1071
  ngOnDestroy() {
650
1072
  this.#gridBuilder?._destroy();
651
1073
  }
1074
+ onHostClick($event) {
1075
+ if ($event.ctrlKey)
1076
+ return;
1077
+ const handler = this.#gridBuilder?._getViewportClickHandler();
1078
+ if (handler && isViewportTarget($event)) {
1079
+ $event.preventDefault();
1080
+ handler($event, this.#agGrid.api);
1081
+ }
1082
+ }
1083
+ onHostContextMenu($event) {
1084
+ if ($event.ctrlKey)
1085
+ return;
1086
+ if (isViewportTarget($event)) {
1087
+ const handler = this.#gridBuilder?._getViewportContextMenuHandler();
1088
+ if (handler) {
1089
+ $event.preventDefault();
1090
+ handler($event, this.#agGrid.api);
1091
+ }
1092
+ }
1093
+ else if (this.#gridBuilder?._hasCellContextMenuHandler()) {
1094
+ // Suppress the browser context menu — AG Grid's onCellContextMenu handles it
1095
+ $event.preventDefault();
1096
+ }
1097
+ }
652
1098
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarDataGridDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
653
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: CoarDataGridDirective, isStandalone: true, selector: "ag-grid-angular[coarDataGrid]", inputs: { coarDataGrid: "coarDataGrid" }, host: { styleAttribute: "display: flex; flex-direction: column; flex-grow: 1;", classAttribute: "ag-theme-cocoar" }, exportAs: ["coarDataGrid"], ngImport: i0 });
1099
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: CoarDataGridDirective, isStandalone: true, selector: "ag-grid-angular[coarDataGrid]", inputs: { coarDataGrid: "coarDataGrid" }, host: { listeners: { "click": "onHostClick($event)", "contextmenu": "onHostContextMenu($event)" }, styleAttribute: "display: flex; flex-direction: column; flex-grow: 1;", classAttribute: "ag-theme-cocoar" }, exportAs: ["coarDataGrid"], ngImport: i0 });
654
1100
  }
655
1101
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: CoarDataGridDirective, decorators: [{
656
1102
  type: Directive,
@@ -665,6 +1111,12 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
665
1111
  }]
666
1112
  }], propDecorators: { coarDataGrid: [{
667
1113
  type: Input
1114
+ }], onHostClick: [{
1115
+ type: HostListener,
1116
+ args: ['click', ['$event']]
1117
+ }], onHostContextMenu: [{
1118
+ type: HostListener,
1119
+ args: ['contextmenu', ['$event']]
668
1120
  }] } });
669
1121
 
670
1122
  /**