@angular-bootstrap/ngbootstrap 0.0.12 → 0.0.13

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.
@@ -4,7 +4,7 @@ import * as i1 from '@angular/common';
4
4
  import { CommonModule } from '@angular/common';
5
5
  import * as i2 from '@angular/forms';
6
6
  import { FormBuilder, FormControl, Validators, FormsModule, ReactiveFormsModule, NG_VALUE_ACCESSOR } from '@angular/forms';
7
- import { firstValueFrom, isObservable } from 'rxjs';
7
+ import { isObservable, firstValueFrom } from 'rxjs';
8
8
 
9
9
  class NgbCellTemplate {
10
10
  template;
@@ -377,6 +377,7 @@ const MIN_WIDTH = 40;
377
377
  const MAX_WIDTH = 320;
378
378
  const MAX_SAMPLE_ROWS = 50;
379
379
  const CHAR_WIDTH_FACTOR = 0.6;
380
+ const CHROME_TOLERANCE = 12; // small buffer to ignore reapplication of padding/borders
380
381
  class NgbSyncColgroupDirective {
381
382
  el;
382
383
  syncId;
@@ -431,6 +432,8 @@ class NgbSyncColgroupDirective {
431
432
  if (!widths.length)
432
433
  return;
433
434
  const entry = this.ensureEntry();
435
+ if (entry.widths && this.areWidthsEqual(entry.widths, widths))
436
+ return;
434
437
  entry.widths = widths;
435
438
  this.apply(widths);
436
439
  entry.bodies.forEach(b => b.apply(widths));
@@ -480,14 +483,30 @@ class NgbSyncColgroupDirective {
480
483
  measure() {
481
484
  const bodyTable = this.firstBodyForSync();
482
485
  const cols = this.colElements();
486
+ const entry = NgbSyncColgroupDirective.registry.get(this.syncId);
483
487
  if (!cols.length)
484
488
  return [];
485
489
  if (!bodyTable) {
486
- const entry = NgbSyncColgroupDirective.registry.get(this.syncId);
487
490
  return entry?.widths ?? [];
488
491
  }
489
492
  const bodyRows = Array.from(bodyTable.querySelectorAll('tbody tr')).slice(0, MAX_SAMPLE_ROWS);
490
- return cols.map((_col, colIndex) => {
493
+ const headerTable = this.syncRole === 'header' ? this.tableEl() : null;
494
+ const headerRow = headerTable?.querySelector('thead tr');
495
+ return cols.map((col, colIndex) => {
496
+ const fixedWidth = (col.getAttribute('data-fixed') ?? '').toLowerCase() === 'true';
497
+ if (fixedWidth) {
498
+ const explicit = parseFloat(col.style.width || '') || MIN_WIDTH;
499
+ return Math.min(Math.max(explicit, MIN_WIDTH), MAX_WIDTH);
500
+ }
501
+ // Measure header content width if available
502
+ let headerContentWidth = 0;
503
+ if (headerRow) {
504
+ const th = headerRow.querySelectorAll('th')[colIndex];
505
+ if (th) {
506
+ headerContentWidth = this.measureCellContent(th);
507
+ }
508
+ }
509
+ // Measure body content width
491
510
  let maxContent = 0;
492
511
  for (const tr of bodyRows) {
493
512
  const td = tr.querySelectorAll('td')[colIndex];
@@ -497,9 +516,20 @@ class NgbSyncColgroupDirective {
497
516
  if (maxContent >= MAX_WIDTH)
498
517
  break;
499
518
  }
500
- if (maxContent === 0)
501
- maxContent = MIN_WIDTH;
502
- return Math.min(Math.max(maxContent, MIN_WIDTH), MAX_WIDTH);
519
+ // Use the maximum of header and body content widths
520
+ let finalContent = Math.max(headerContentWidth, maxContent);
521
+ if (finalContent === 0)
522
+ finalContent = MIN_WIDTH;
523
+ let width = Math.min(Math.max(finalContent, MIN_WIDTH), MAX_WIDTH);
524
+ const prev = entry?.widths?.[colIndex];
525
+ if (prev && width > prev) {
526
+ // If the only change is re-adding cell chrome (common when interactive elements span the cell),
527
+ // keep the previous stable width to avoid incremental growth.
528
+ const delta = width - prev;
529
+ if (delta <= CHROME_TOLERANCE)
530
+ width = prev;
531
+ }
532
+ return width;
503
533
  });
504
534
  }
505
535
  apply(widths) {
@@ -513,14 +543,50 @@ class NgbSyncColgroupDirective {
513
543
  }
514
544
  measureCellContent(cell) {
515
545
  const style = getComputedStyle(cell);
546
+ const cellClientWidth = cell.clientWidth;
547
+ const cellRect = cell.getBoundingClientRect();
516
548
  const chrome = (parseFloat(style.paddingLeft) || 0) +
517
549
  (parseFloat(style.paddingRight) || 0) +
518
550
  (parseFloat(style.borderLeftWidth) || 0) +
519
551
  (parseFloat(style.borderRightWidth) || 0);
520
- const interactive = cell.querySelector('input, select, textarea, button');
521
- if (interactive)
522
- return (interactive.getBoundingClientRect().width || 0) + chrome;
552
+ const interactiveEls = Array.from(cell.querySelectorAll('input, select, textarea, button'));
553
+ let interactiveWidth = null;
554
+ if (interactiveEls.length) {
555
+ interactiveWidth = interactiveEls.reduce((total, el) => {
556
+ const rect = el.getBoundingClientRect();
557
+ const elStyle = getComputedStyle(el);
558
+ const widthStr = (el.style.width || elStyle.width || '').toString().trim();
559
+ const usesPercentWidth = widthStr.endsWith('%');
560
+ const matchesCellWidth = (!!cellClientWidth && rect.width && Math.abs(rect.width - cellClientWidth) <= 1) ||
561
+ (cellRect.width > 0 && rect.width > 0 && Math.abs(rect.width - cellRect.width) <= 1);
562
+ const spansCell = usesPercentWidth || matchesCellWidth;
563
+ const margin = (parseFloat(elStyle.marginLeft) || 0) + (parseFloat(elStyle.marginRight) || 0);
564
+ if (spansCell) {
565
+ const textWidth = this.measureTextWidth((el.textContent ?? '').trim(), elStyle);
566
+ const paddingBorder = (parseFloat(elStyle.paddingLeft) || 0) +
567
+ (parseFloat(elStyle.paddingRight) || 0) +
568
+ (parseFloat(elStyle.borderLeftWidth) || 0) +
569
+ (parseFloat(elStyle.borderRightWidth) || 0);
570
+ return total + Math.ceil(textWidth + paddingBorder + margin);
571
+ }
572
+ const rectWidth = rect.width || parseFloat(elStyle.width) || ((el.textContent ?? '').length * ((parseFloat(elStyle.fontSize) || 14) * CHAR_WIDTH_FACTOR));
573
+ return total + rectWidth + margin;
574
+ }, 0);
575
+ interactiveWidth = Math.ceil((interactiveWidth || 0) + chrome);
576
+ }
523
577
  const text = (cell.textContent ?? '').trim();
578
+ const textWidth = Math.ceil(this.measureTextWidth(text, style) + chrome);
579
+ if (interactiveWidth != null) {
580
+ return Math.max(textWidth, interactiveWidth);
581
+ }
582
+ return textWidth;
583
+ }
584
+ areWidthsEqual(prev, next) {
585
+ if (prev.length !== next.length)
586
+ return false;
587
+ return prev.every((v, i) => v === next[i]);
588
+ }
589
+ measureTextWidth(text, style) {
524
590
  const fontSize = parseFloat(style.fontSize) || 14;
525
591
  const font = style.font || `${style.fontStyle} ${style.fontVariant} ${style.fontWeight} ${style.fontSize} / ${style.lineHeight} ${style.fontFamily}`;
526
592
  let contentWidth = 0;
@@ -543,7 +609,7 @@ class NgbSyncColgroupDirective {
543
609
  if (!contentWidth) {
544
610
  contentWidth = text.length * (fontSize * CHAR_WIDTH_FACTOR);
545
611
  }
546
- return Math.ceil(contentWidth + chrome);
612
+ return contentWidth;
547
613
  }
548
614
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: NgbSyncColgroupDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive });
549
615
  static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.9", type: NgbSyncColgroupDirective, isStandalone: true, selector: "[ngbSyncColgroup]", inputs: { syncId: ["ngbSyncColgroup", "syncId"], syncRole: "syncRole" }, host: { listeners: { "window:resize": "onResize()" } }, ngImport: i0 });
@@ -611,7 +677,7 @@ class Datagrid {
611
677
  stickyHeader = false;
612
678
  /** Enables sticky footer when scrolling. */
613
679
  stickyFooter = false;
614
- /** Enables scroll container (used when pagination is off). */
680
+ /** Enables scroll table body container */
615
681
  scrollable = true;
616
682
  /** Row height used to stack multiple sticky rows without overlap (px). */
617
683
  stickyRowHeight = 40;
@@ -639,7 +705,6 @@ class Datagrid {
639
705
  highlightRowKey = null;
640
706
  /** Column key for highlighting. */
641
707
  highlightColKey = null;
642
- scrollbarWidth = 0;
643
708
  /** Accessible label for the global filter input. */
644
709
  globalFilterAriaLabel = 'Search all columns';
645
710
  /** Accessible label announced when expanding a row. */
@@ -904,6 +969,19 @@ class Datagrid {
904
969
  headerText(col) {
905
970
  return (col.header ?? col.field ?? '').toString();
906
971
  }
972
+ headerTitle(col) {
973
+ const t = col?.title;
974
+ return (t ?? this.headerText(col)) ?? '';
975
+ }
976
+ cellTitle(row, col) {
977
+ const def = col?.cellTitle;
978
+ if (typeof def === 'function')
979
+ return def(row) ?? '';
980
+ if (typeof def === 'string')
981
+ return def;
982
+ const val = row?.[col.field];
983
+ return val === undefined || val === null ? '' : String(val);
984
+ }
907
985
  columnFilterAriaLabel(col) {
908
986
  return `${this.headerText(col)} filter`;
909
987
  }
@@ -1037,9 +1115,6 @@ class Datagrid {
1037
1115
  this.filterTplQ?.changes.subscribe(rebuild);
1038
1116
  this.globalTplQ?.changes.subscribe(rebuild);
1039
1117
  }
1040
- ngAfterViewInit() {
1041
- queueMicrotask(() => this.syncScrollbarWidth());
1042
- }
1043
1118
  ngOnChanges(ch) {
1044
1119
  if (ch['columns'])
1045
1120
  this.rebuildFilterForm();
@@ -1457,35 +1532,18 @@ class Datagrid {
1457
1532
  return;
1458
1533
  this.toggleSelection(pagedIndex, ev);
1459
1534
  }
1460
- onHeaderScroll() {
1461
- const body = this.bodyScroller?.nativeElement;
1462
- const head = this.headerScroller?.nativeElement;
1463
- if (body && head && body.scrollLeft !== head.scrollLeft) {
1464
- body.scrollLeft = head.scrollLeft;
1465
- }
1466
- }
1467
- syncScrollbarWidth() {
1468
- const el = this.bodyScroller?.nativeElement;
1469
- if (!el)
1470
- return;
1471
- const width = el.offsetWidth - el.clientWidth;
1472
- if (width !== this.scrollbarWidth) {
1473
- this.scrollbarWidth = width;
1474
- this.cdr.markForCheck();
1475
- }
1476
- }
1477
1535
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: Datagrid, deps: [], target: i0.ɵɵFactoryTarget.Component });
1478
1536
  static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.9", type: Datagrid, isStandalone: true, selector: "ngb-datagrid", inputs: { columns: "columns", data: "data", enableSorting: "enableSorting", enableFiltering: "enableFiltering", enableGlobalFilter: "enableGlobalFilter", enablePagination: "enablePagination", enableEdit: "enableEdit", enableDelete: "enableDelete", pageSizeOptions: "pageSizeOptions", enableAdd: "enableAdd", addButtonAriaLabel: "addButtonAriaLabel", addButtonText: "addButtonText", stickyRows: "stickyRows", stickyHeader: "stickyHeader", stickyFooter: "stickyFooter", scrollable: "scrollable", stickyRowHeight: "stickyRowHeight", stickyHeaderHeight: "stickyHeaderHeight", stickyFooterHeight: "stickyFooterHeight", tableOptions: "tableOptions", selectionMode: "selectionMode", selectionBehavior: "selectionBehavior", selectionKeyMode: "selectionKeyMode", selectAllEnabled: "selectAllEnabled", selectionA11yLabels: "selectionA11yLabels", selectionDisabledFn: "selectionDisabledFn", highlightedIndex: "highlightedIndex", highlightRowKey: "highlightRowKey", highlightColKey: "highlightColKey", globalFilterAriaLabel: "globalFilterAriaLabel", expandRowAriaLabel: "expandRowAriaLabel", collapseRowAriaLabel: "collapseRowAriaLabel", exportPdfAriaLabel: "exportPdfAriaLabel", exportExcelAriaLabel: "exportExcelAriaLabel", newRowDefaults: "newRowDefaults", strictEmail: "strictEmail", editOnRowClick: "editOnRowClick", singleExpand: "singleExpand", exportOptions: "exportOptions", theme: "theme", responsive: "responsive", trackBy: "trackBy", editService: "editService", dataProviderAll: "dataProviderAll", dataProviderSelection: "dataProviderSelection", pageSize: "pageSize" }, outputs: { rowAdd: "rowAdd", rowEdit: "rowEdit", rowSave: "rowSave", rowCancel: "rowCancel", rowDelete: "rowDelete", sortChange: "sortChange", filtersChange: "filtersChange", pageChange: "pageChange", selectionChange: "selectionChange" }, providers: [
1479
1537
  { provide: PdfExportAdapter, useClass: JsPdfAdapter },
1480
1538
  { provide: ExcelExportAdapter, useClass: XlsxAdapter }
1481
- ], queries: [{ propertyName: "exportButtonDir", first: true, predicate: ExportButtonDirective, descendants: true }, { propertyName: "rowDetailTpl", first: true, predicate: NgbRowDetailTemplate, descendants: true }, { propertyName: "cellTplQ", predicate: NgbCellTemplate }, { propertyName: "editTplQ", predicate: NgbEditorTemplate }, { propertyName: "filterTplQ", predicate: NgbFilterTemplate }, { propertyName: "globalTplQ", predicate: NgbGlobalFilterTemplate }], viewQueries: [{ propertyName: "bodyScroller", first: true, predicate: ["bodyScroller"], descendants: true }, { propertyName: "headerScroller", first: true, predicate: ["headerScroller"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\n<div class=\"ngb-grid\" [attr.data-theme]=\"theme\" [class.ngb-responsive]=\"isResponsiveEnabled()\">\n <!-- Export toolbar -->\n <div class=\"d-flex justify-content-end mb-2\" *ngIf=\"exportOptions?.enabled\">\n <ng-container *ngIf=\"exportButtonTpl; else defaultExportBtns\"\n [ngTemplateOutlet]=\"exportButtonTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: triggerExport }\">\n </ng-container>\n\n <ng-template #defaultExportBtns>\n <button type=\"button\"\n class=\"btn btn-sm btn-outline-secondary me-2\"\n *ngIf=\"exportOptions.type === 'pdf' || exportOptions.type === 'both'\"\n [disabled]=\"exporting\"\n (click)=\"export('pdf')\"\n [attr.aria-label]=\"exportAriaLabel('pdf')\">\n Export to PDF\n </button>\n <button type=\"button\"\n class=\"btn btn-sm btn-outline-secondary\"\n *ngIf=\"exportOptions.type === 'excel' || exportOptions.type === 'both'\"\n [disabled]=\"exporting\"\n (click)=\"export('excel')\"\n [attr.aria-label]=\"exportAriaLabel('excel')\">\n Export to Excel\n </button>\n </ng-template>\n </div>\n <div class=\"d-flex justify-content-end mb-2\" *ngIf=\"enableAdd\">\n <button\n type=\"button\"\n class=\"btn btn-sm btn-primary\"\n (click)=\"startAdd()\"\n [disabled]=\"addingNew\"\n [attr.aria-label]=\"addButtonAriaLabel || addButtonText\"\n >\n {{ addButtonText }}\n </button>\n </div>\n <!-- Global filter -->\n <div class=\"mb-2\" *ngIf=\"enableFiltering && enableGlobalFilter\">\n <ng-container\n *ngIf=\"globalTpl; else defaultGlobalFilter\"\n [ngTemplateOutlet]=\"globalTpl?.template\"\n [ngTemplateOutletContext]=\"{ $implicit: globalFilterCtrl }\"\n >\n </ng-container>\n\n <ng-template #defaultGlobalFilter>\n <input\n type=\"search\"\n class=\"form-control form-control-sm\"\n placeholder=\"Search all columns...\"\n [formControl]=\"globalFilterCtrl\"\n [attr.aria-label]=\"globalFilterAriaLabel\"\n />\n </ng-template>\n </div>\n\n <div class=\"table-wrapper\"\n [class.scrollable]=\"shouldEnableScroll\"\n [ngClass]=\"responsiveWrapperClasses\">\n <div class=\"table-header\" [style.paddingRight.px]=\"scrollbarWidth\" #headerScroller (scroll)=\"onHeaderScroll()\">\n <table [ngClass]=\"tableClassList\">\n <caption *ngIf=\"tableOptions?.caption\"\n [class.caption-top]=\"(tableOptions?.captionSide ?? 'top') === 'top'\">\n {{ tableOptions?.caption }}\n </caption>\n <colgroup [ngbSyncColgroup]=\"colgroupSyncId\" syncRole=\"header\">\n <col *ngIf=\"isSelectionEnabled()\" style=\"width:1%\">\n <col *ngIf=\"rowDetailTpl\" style=\"width:1%\">\n <col *ngIf=\"stickyRowsEnabled\" style=\"width:1%\">\n <col *ngFor=\"let col of columns\">\n <col *ngIf=\"enableEdit || enableDelete\" style=\"width:1%\">\n </colgroup>\n <thead class=\"thead-light\">\n <tr>\n <th *ngIf=\"isSelectionEnabled()\" class=\"text-center\" scope=\"col\" style=\"width:1%\">\n <ng-container *ngIf=\"selectionMode === 'multiple' && selectAllEnabled\">\n <input type=\"checkbox\"\n [checked]=\"isPageAllSelected()\"\n [indeterminate]=\"isPageIndeterminate()\"\n (change)=\"toggleSelectAllCurrentPage()\"\n [attr.aria-label]=\"selectAllLabel()\">\n </ng-container>\n </th>\n <th *ngIf=\"rowDetailTpl\" style=\"width:1%\" scope=\"col\" aria-hidden=\"true\"></th>\n <th *ngIf=\"stickyRowsEnabled\" style=\"width:1%\" scope=\"col\" class=\"text-center\" data-title=\"Sticky\"></th>\n <th\n *ngFor=\"let col of columns\"\n [attr.data-title]=\"col.header\"\n [class.sortable]=\"enableSorting && col.sortable\"\n scope=\"col\"\n [attr.aria-sort]=\"enableSorting && col.sortable ? ariaSortFor(col.field) : null\"\n >\n <ng-container *ngIf=\"enableSorting && col.sortable; else plainHeader\">\n <button\n type=\"button\"\n class=\"btn btn-link p-0 text-start w-100\"\n (click)=\"toggleSort(col.field)\"\n [attr.aria-label]=\"sortButtonAriaLabel(col)\"\n >\n <span>{{ col.header }}</span>\n <span\n class=\"sort-indicator\"\n *ngIf=\"enableSorting && sort.active === col.field\"\n aria-hidden=\"true\"\n >\n {{\n sort.direction === 'asc'\n ? '\u25B2'\n : sort.direction === 'desc'\n ? '\u25BC'\n : ''\n }}\n </span>\n </button>\n </ng-container>\n <ng-template #plainHeader>\n <span>{{ col.header }}</span>\n </ng-template>\n </th>\n <th\n *ngIf=\"enableEdit || enableDelete\"\n class=\"text-center\"\n scope=\"col\"\n style=\"width: 1%\"\n >\n Actions\n </th>\n </tr>\n\n <!-- Per-column filters -->\n <tr *ngIf=\"anyFilterable\" [formGroup]=\"filterForm\">\n <th *ngIf=\"isSelectionEnabled()\"></th>\n <th *ngIf=\"stickyRowsEnabled\"></th>\n <th *ngFor=\"let col of columns\" [attr.data-title]=\"col.header\">\n <ng-container *ngIf=\"col.filterable\">\n <ng-container *ngIf=\"filterTpls[col.field] as ft; else defaultFilter\">\n <ng-container\n [ngTemplateOutlet]=\"ft.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: filterForm.get(col.field),\n control: filterForm.get(col.field),\n col: col\n }\"\n >\n </ng-container>\n </ng-container>\n <ng-template #defaultFilter>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"columnFilterAriaLabel(col)\"\n />\n </ng-template>\n </ng-container>\n </th>\n <th *ngIf=\"enableEdit || enableDelete\"></th>\n </tr>\n </thead>\n </table>\n </div>\n\n <div class=\"table-body\" #bodyScroller>\n <table [ngClass]=\"tableClassList\"\n role=\"grid\"\n [attr.aria-readonly]=\"(enableEdit || enableAdd) ? 'false' : 'true'\">\n <colgroup [ngbSyncColgroup]=\"colgroupSyncId\" syncRole=\"body\">\n <col *ngIf=\"isSelectionEnabled()\" style=\"width:1%\">\n <col *ngIf=\"rowDetailTpl\" style=\"width:1%\">\n <col *ngIf=\"stickyRowsEnabled\" style=\"width:1%\">\n <col *ngFor=\"let col of columns\">\n <col *ngIf=\"enableEdit || enableDelete\" style=\"width:1%\">\n </colgroup>\n <tbody>\n <!-- ADD NEW ROW -->\n <tr *ngIf=\"addingNew\" [formGroup]=\"addForm\">\n <!-- blank caret cell to keep alignment when detail column is present -->\n <td *ngIf=\"isSelectionEnabled()\"></td>\n <td *ngIf=\"rowDetailTpl\"></td>\n <td *ngIf=\"stickyRowsEnabled\" class=\"text-center\"></td>\n\n <td *ngFor=\"let col of columns\" [attr.data-title]=\"col.header\">\n <ng-container [ngSwitch]=\"col.type\">\n <!-- boolean -->\n <div\n *ngSwitchCase=\"'boolean'\"\n class=\"form-check m-0\"\n [class.is-invalid]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid\">\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid ? 'true' : null\"\n [attr.aria-describedby]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid ? 'add-error-' + col.field : null\"\n />\n </div>\n\n <!-- number/email/date/text -->\n <input\n *ngSwitchDefault\n class=\"form-control form-control-sm\"\n [class.is-invalid]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid\"\n [attr.type]=\"col.type === 'number' ? 'number' :\n col.type === 'email' ? 'email' :\n col.type === 'date' ? 'date' : 'text'\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid ? 'true' : null\"\n [attr.aria-describedby]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid ? 'add-error-' + col.field : null\"\n />\n </ng-container>\n\n <div class=\"invalid-feedback d-block\"\n *ngIf=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.errors as e\"\n [attr.id]=\"'add-error-' + col.field\"\n role=\"alert\">\n <ng-container *ngIf=\"e['required']\">Required</ng-container>\n <ng-container *ngIf=\"e['email']\">Invalid email</ng-container>\n <ng-container *ngIf=\"e['number']\">Invalid number</ng-container>\n <ng-container *ngIf=\"e['date']\">Invalid date</ng-container>\n </div>\n </td>\n\n <td *ngIf=\"enableEdit || enableDelete\" class=\"text-nowrap text-center\">\n <button type=\"button\" class=\"btn btn-sm btn-success me-1\" (click)=\"saveAdd()\" [disabled]=\"addForm.invalid\">Save</button>\n <button type=\"button\" class=\"btn btn-sm btn-secondary\" (click)=\"cancelAdd()\">Cancel</button>\n </td>\n </tr>\n\n <!-- DATA ROWS + DETAIL ROWS -->\n <ng-container *ngFor=\"let row of paged; let i = index; trackBy: trackRow\">\n\n <!-- DATA ROW (click-to-edit supported) -->\n <tr (click)=\"onRowSelect($event, i); onRowClick($event, i)\" [formGroup]=\"editForm\"\n [class.sticky-row]=\"isRowSticky(row, i)\"\n [class.row-selected]=\"isRowSelected(row, i)\"\n [class.row-highlight]=\"isRowHighlighted(row, i)\"\n [attr.aria-selected]=\"isRowSelected(row, i) ? 'true' : null\"\n [style.top.px]=\"stickyTop(row, i)\"\n [style.z-index]=\"isRowSticky(row, i) ? 3 : null\">\n\n <td *ngIf=\"isSelectionEnabled()\" class=\"text-center align-middle\">\n <input type=\"checkbox\"\n [checked]=\"isRowSelected(row, i)\"\n [disabled]=\"selectionMode === 'none' || selectionBehavior === 'row' || isSelectionDisabled(row, i)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleSelection(i, $event)\"\n [attr.aria-label]=\"rowSelectionLabel(i)\">\n </td>\n\n <!-- caret/expander cell (left-most) -->\n <td *ngIf=\"rowDetailTpl\" class=\"text-center align-middle\">\n <button\n type=\"button\"\n class=\"expand btn btn-link p-0\"\n (click)=\"$event.stopPropagation(); toggleExpand(i)\"\n [attr.aria-expanded]=\"isExpanded(i)\"\n [attr.aria-controls]=\"'dg-row-detail-' + i\"\n [attr.aria-label]=\"isExpanded(i) ? collapseRowAriaLabel : expandRowAriaLabel\">\n {{ isExpanded(i) ? '-' : '+' }}\n </button>\n </td>\n\n <td *ngIf=\"stickyRowsEnabled\" class=\"text-center align-middle\" data-title=\"Sticky\">\n <button\n type=\"button\"\n class=\"btn btn-link p-0 no-edit-trigger sticky-toggle\"\n (click)=\"$event.stopPropagation(); toggleStickyRow(i)\"\n [attr.aria-pressed]=\"isRowSticky(row, i)\"\n aria-label=\"Toggle sticky row\">\n <ng-container *ngIf=\"stickyIcon(row, i) as icon\">\n <span class=\"bi\" [ngClass]=\"'bi-' + icon\" aria-hidden=\"true\"></span>\n </ng-container>\n </button>\n </td>\n\n <!-- DATA CELLS -->\n <td *ngFor=\"let col of columns; let ci = index\"\n [attr.data-title]=\"col.header\"\n [class.cell-highlight]=\"isCellHighlighted(row, i, col, ci)\">\n <!-- EDIT MODE -->\n <ng-container *ngIf=\"editingIndex === i && (col.editable ?? true); else readCell\">\n <!-- editor template override -->\n <ng-container *ngIf=\"editTpls[col.field] as et; else defaultEditor\"\n [ngTemplateOutlet]=\"et.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: editForm.get(col.field),\n control: editForm.get(col.field),\n row: row, col: col, form: editForm, index: i, isNew: false\n }\">\n </ng-container>\n\n <!-- default editors -->\n <ng-template #defaultEditor>\n <ng-container [ngSwitch]=\"col.type\">\n <div *ngSwitchCase=\"'boolean'\" class=\"form-check m-0\">\n <input type=\"checkbox\"\n class=\"form-check-input\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.invalid ? 'true' : null\"\n [attr.aria-describedby]=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.invalid ? 'edit-error-' + col.field : null\" />\n </div>\n <select *ngSwitchCase=\"'select'\"\n class=\"form-select form-select-sm\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\">\n <option *ngFor=\"let o of col.options ?? []\" [ngValue]=\"o.value\">{{ o.label }}</option>\n </select>\n <input *ngSwitchDefault\n class=\"form-control form-control-sm\"\n [attr.type]=\"col.type === 'number' ? 'number' :\n col.type === 'email' ? 'email' :\n col.type === 'date' ? 'date' : 'text'\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.invalid ? 'true' : null\"\n [attr.aria-describedby]=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.invalid ? 'edit-error-' + col.field : null\" />\n </ng-container>\n\n <div class=\"invalid-feedback d-block\"\n *ngIf=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.errors as e\"\n [attr.id]=\"'edit-error-' + col.field\"\n role=\"alert\">\n <span *ngIf=\"e['required']\">Required</span>\n <span *ngIf=\"e['email']\">Invalid email</span>\n <span *ngIf=\"e['number']\">Invalid number</span>\n <span *ngIf=\"e['date']\">Invalid date</span>\n </div>\n </ng-template>\n </ng-container>\n\n <!-- READ MODE (with optional cell template) -->\n <ng-template #readCell>\n <ng-container *ngIf=\"cellTpls[col.field] as ct; else defaultCell\"\n [ngTemplateOutlet]=\"ct.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row[col.field], row: row, col: col, index: i }\">\n </ng-container>\n <ng-template #defaultCell>\n <ng-container [ngSwitch]=\"col.type\">\n <span *ngSwitchCase=\"'boolean'\">{{ row[col.field] ? 'Yes' : 'No' }}</span>\n <span *ngSwitchDefault>{{ row[col.field] }}</span>\n </ng-container>\n </ng-template>\n </ng-template>\n </td>\n\n <!-- ACTIONS -->\n <td *ngIf=\"enableEdit || enableDelete\" class=\"text-nowrap text-center\">\n <ng-container *ngIf=\"editingIndex !== i; else editBtns\">\n <button type=\"button\" *ngIf=\"enableEdit\" class=\"btn btn-sm btn-outline-primary me-1 no-edit-trigger\" (click)=\"startEdit(i)\">Edit</button>\n <button type=\"button\" *ngIf=\"enableDelete\" class=\"btn btn-sm btn-outline-danger no-edit-trigger\" (click)=\"deleteRow(i)\">Delete</button>\n </ng-container>\n <ng-template #editBtns>\n <button type=\"button\" class=\"btn btn-sm btn-success me-1\" (click)=\"saveEdit(i)\" [disabled]=\"editForm.invalid\">Save</button>\n <button type=\"button\" class=\"btn btn-sm btn-secondary\" (click)=\"cancelEdit(i)\">Cancel</button>\n </ng-template>\n </td>\n </tr>\n\n <!-- DETAIL ROW (spans all columns) -->\n <tr *ngIf=\"rowDetailTpl && isExpanded(i)\" [attr.id]=\"'dg-row-detail-' + i\">\n <td [attr.colspan]=\"detailColspan\" role=\"region\">\n <ng-container [ngTemplateOutlet]=\"rowDetailTpl.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row, index: i }\">\n </ng-container>\n </td>\n </tr>\n\n </ng-container>\n </tbody>\n\n </table>\n </div>\n </div>\n</div>\n\n<!-- Footer (left = count, center = pagination, right = page size) -->\n<div *ngIf=\"enablePagination\"\n class=\"d-flex align-items-center justify-content-between mt-2 grid-footer\"\n [class.sticky-footer]=\"isFooterSticky\">\n <!-- left: count -->\n <div class=\"small text-muted\" aria-live=\"polite\">\n {{ startIndex }} - {{ endIndex }} of {{ sorted.length }}\n\n </div>\n\n <!-- center: pager -->\n <div class=\"flex-grow-1 d-flex justify-content-center\">\n <ngb-pagination\n [page]=\"page\"\n [pageSize]=\"pageSize\"\n [collectionSize]=\"sorted.length\"\n [maxSize]=\"5\"\n (pageChange)=\"onPage($event)\">\n </ngb-pagination>\n </div>\n\n <!-- right: rows per page -->\n <div class=\"d-flex align-items-center gap-2\">\n <label class=\"form-label form-label-sm mb-0\" for=\"pageSize\">Rows:</label>\n <select\n id=\"pageSize\"\n class=\"form-select form-select-sm\"\n [(ngModel)]=\"pageSize\"\n (ngModelChange)=\"onPageSize($event)\"\n aria-label=\"Rows per page\">\n <option *ngFor=\"let s of pageSizeOptions\" [value]=\"s\">{{ s }}</option>\n </select>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.table{margin-bottom:.5rem}th.sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.sort-indicator{margin-left:.35rem;font-size:.75rem}.expand{cursor:pointer;font-size:24px;line-height:18px;display:block}.sticky-toggle{display:inline-flex;align-items:center;justify-content:center;gap:.15rem}.table-wrapper{position:relative}.table-wrapper{display:flex;flex-direction:column}.table-wrapper .table{table-layout:fixed;width:100%;margin-bottom:0}.table-header{overflow-x:auto;overflow-y:hidden}.table-body{max-height:60vh;overflow:auto;scrollbar-gutter:stable}.table-header th,.table-body td{min-width:40px;max-width:320px;word-break:break-word}.table-header th{white-space:nowrap}.table-body td{white-space:normal}.table-header col,.table-body col{min-width:40px;max-width:320px}.table-wrapper.sticky-header thead th{position:sticky;top:0;z-index:5;background:#ccc}.table-wrapper.sticky-footer .grid-footer{position:sticky;bottom:0;z-index:5;background:#fff;border-top:1px solid #e2e2e2;border-left:1px solid #e2e2e2;border-right:1px solid #e2e2e2;padding:5px 20px}.sticky-row{position:sticky;background:#ccc;z-index:4}.sticky-row td{background:#ccc}.row-selected{background-color:#e7f1ff}.row-highlight{background-color:var(--bs-warning-bg-subtle, #fff3cd)}.row-highlight td{background-color:inherit}.cell-highlight{background-color:var(--bs-info-bg-subtle, #cff4fc)}.ngb-grid[data-theme=material]{--ngb-primary: #3f51b5}.ngb-grid[data-theme=material] .btn{border-color:var(--ngb-primary)}.ngb-grid[data-theme=material] table.table th,.ngb-grid[data-theme=material] table.table td{border-bottom:1px solid #e5e7eb}.ngb-grid[data-theme=tailwind]{--ngb-primary: rgb(29 78 216)}.ngb-grid[data-theme=tailwind] .btn{border-color:var(--ngb-primary);border-radius:.75rem}.ngb-grid[data-theme=tailwind] table.table th,.ngb-grid[data-theme=tailwind] table.table td{border-bottom:1px solid #e5e7eb;padding:.5rem}.ngb-responsive .table{width:100%}@media(max-width:768px){.ngb-responsive thead{display:none}.ngb-responsive tbody tr{display:grid;grid-template-columns:1fr;gap:.5rem;border:1px solid #e5e7eb;border-radius:.75rem;padding:.75rem;margin-bottom:.75rem}.ngb-responsive tbody td{display:grid;grid-template-columns:8rem 1fr}.ngb-responsive tbody td:before{content:attr(data-title);font-weight:600;opacity:.75;padding-right:.5rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: NgbPaginationComponent, selector: "ngb-pagination", inputs: ["page", "pageSize", "collectionSize", "maxSize"], outputs: ["pageChange"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: NgbSyncColgroupDirective, selector: "[ngbSyncColgroup]", inputs: ["ngbSyncColgroup", "syncRole"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1539
+ ], queries: [{ propertyName: "exportButtonDir", first: true, predicate: ExportButtonDirective, descendants: true }, { propertyName: "rowDetailTpl", first: true, predicate: NgbRowDetailTemplate, descendants: true }, { propertyName: "cellTplQ", predicate: NgbCellTemplate }, { propertyName: "editTplQ", predicate: NgbEditorTemplate }, { propertyName: "filterTplQ", predicate: NgbFilterTemplate }, { propertyName: "globalTplQ", predicate: NgbGlobalFilterTemplate }], viewQueries: [{ propertyName: "bodyScroller", first: true, predicate: ["bodyScroller"], descendants: true }, { propertyName: "headerScroller", first: true, predicate: ["headerScroller"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\n<div\n class=\"ngb-grid\"\n [attr.data-theme]=\"theme\"\n [class.ngb-responsive]=\"isResponsiveEnabled()\"\n>\n <!-- Export toolbar -->\n <div class=\"d-flex justify-content-end mb-2\" *ngIf=\"exportOptions?.enabled\">\n <ng-container\n *ngIf=\"exportButtonTpl; else defaultExportBtns\"\n [ngTemplateOutlet]=\"exportButtonTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: triggerExport }\"\n >\n </ng-container>\n\n <ng-template #defaultExportBtns>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-secondary me-2\"\n *ngIf=\"exportOptions.type === 'pdf' || exportOptions.type === 'both'\"\n [disabled]=\"exporting\"\n (click)=\"export('pdf')\"\n [attr.aria-label]=\"exportAriaLabel('pdf')\"\n >\n Export to PDF\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-secondary\"\n *ngIf=\"exportOptions.type === 'excel' || exportOptions.type === 'both'\"\n [disabled]=\"exporting\"\n (click)=\"export('excel')\"\n [attr.aria-label]=\"exportAriaLabel('excel')\"\n >\n Export to Excel\n </button>\n </ng-template>\n </div>\n <div class=\"d-flex justify-content-end mb-2\" *ngIf=\"enableAdd\">\n <button\n type=\"button\"\n class=\"btn btn-sm btn-primary\"\n (click)=\"startAdd()\"\n [disabled]=\"addingNew\"\n [attr.aria-label]=\"addButtonAriaLabel || addButtonText\"\n >\n {{ addButtonText }}\n </button>\n </div>\n <!-- Global filter -->\n <div class=\"mb-2\" *ngIf=\"enableFiltering && enableGlobalFilter\">\n <ng-container\n *ngIf=\"globalTpl; else defaultGlobalFilter\"\n [ngTemplateOutlet]=\"globalTpl?.template\"\n [ngTemplateOutletContext]=\"{ $implicit: globalFilterCtrl }\"\n >\n </ng-container>\n\n <ng-template #defaultGlobalFilter>\n <input\n type=\"search\"\n class=\"form-control form-control-sm\"\n placeholder=\"Search all columns...\"\n [formControl]=\"globalFilterCtrl\"\n [attr.aria-label]=\"globalFilterAriaLabel\"\n />\n </ng-template>\n </div>\n\n <div\n class=\"table-wrapper\"\n [ngClass]=\"responsiveWrapperClasses\"\n >\n <!-- <div class=\"table-header\"> -->\n <table class=\"grid-table grid-header mb-0\" [ngClass]=\"tableClassList\">\n <caption\n *ngIf=\"tableOptions?.caption\"\n [class.caption-top]=\"(tableOptions?.captionSide ?? 'top') === 'top'\"\n >\n {{\n tableOptions?.caption\n }}\n </caption>\n <colgroup [ngbSyncColgroup]=\"colgroupSyncId\" syncRole=\"header\">\n <col *ngIf=\"isSelectionEnabled()\" style=\"width: 1%\" />\n <col *ngIf=\"rowDetailTpl\" style=\"width: 1%\" data-fixed=\"true\" />\n <col *ngIf=\"stickyRowsEnabled\" style=\"width: 1%\" />\n <col *ngFor=\"let col of columns\" />\n <col *ngIf=\"enableEdit || enableDelete\" style=\"width: 1%\" />\n </colgroup>\n <thead class=\"table-light\">\n <tr>\n <th\n *ngIf=\"isSelectionEnabled()\"\n class=\"text-center\"\n scope=\"col\"\n style=\"width: 1%\"\n >\n <ng-container\n *ngIf=\"selectionMode === 'multiple' && selectAllEnabled\"\n >\n <input\n type=\"checkbox\"\n [checked]=\"isPageAllSelected()\"\n [indeterminate]=\"isPageIndeterminate()\"\n (change)=\"toggleSelectAllCurrentPage()\"\n [attr.aria-label]=\"selectAllLabel()\"\n />\n </ng-container>\n </th>\n <th\n *ngIf=\"rowDetailTpl\"\n style=\"width: 1%\"\n scope=\"col\"\n aria-hidden=\"true\"\n ></th>\n <th\n *ngIf=\"stickyRowsEnabled\"\n style=\"width: 1%\"\n scope=\"col\"\n class=\"text-center\"\n data-title=\"Sticky\"\n ></th>\n <th\n *ngFor=\"let col of columns\"\n [attr.data-title]=\"col.header\"\n [class.sortable]=\"enableSorting && col.sortable\"\n scope=\"col\"\n [attr.title]=\"headerTitle(col)\"\n [attr.aria-sort]=\"\n enableSorting && col.sortable ? ariaSortFor(col.field) : null\n \"\n >\n <ng-container\n *ngIf=\"enableSorting && col.sortable; else plainHeader\"\n >\n <button\n type=\"button\"\n class=\"btn btn-link p-0 text-start w-100\"\n (click)=\"toggleSort(col.field)\"\n [attr.aria-label]=\"sortButtonAriaLabel(col)\"\n >\n <span>{{ col.header }}</span>\n <span\n class=\"sort-indicator\"\n *ngIf=\"enableSorting && sort.active === col.field\"\n aria-hidden=\"true\"\n >\n {{\n sort.direction === \"asc\"\n ? \"\u25B2\"\n : sort.direction === \"desc\"\n ? \"\u25BC\"\n : \"\"\n }}\n </span>\n </button>\n </ng-container>\n <ng-template #plainHeader>\n <span>{{ col.header }}</span>\n </ng-template>\n </th>\n <th\n *ngIf=\"enableEdit || enableDelete\"\n class=\"text-center\"\n scope=\"col\"\n style=\"width: 1%\"\n >\n Actions\n </th>\n </tr>\n\n <!-- Per-column filters -->\n <tr *ngIf=\"anyFilterable\" class=\"filter-row\" [formGroup]=\"filterForm\">\n <th *ngIf=\"isSelectionEnabled()\"></th>\n <th *ngIf=\"stickyRowsEnabled\"></th>\n <th *ngFor=\"let col of columns\" [attr.data-title]=\"col.header\">\n <ng-container *ngIf=\"col.filterable\">\n <ng-container\n *ngIf=\"filterTpls[col.field] as ft; else defaultFilter\"\n >\n <ng-container\n [ngTemplateOutlet]=\"ft.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: filterForm.get(col.field),\n control: filterForm.get(col.field),\n col: col,\n }\"\n >\n </ng-container>\n </ng-container>\n <ng-template #defaultFilter>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"columnFilterAriaLabel(col)\"\n />\n </ng-template>\n </ng-container>\n </th>\n <th *ngIf=\"enableEdit || enableDelete\"></th>\n </tr>\n </thead>\n </table>\n <!-- </div> -->\n\n <div [class.table-body-scroll]=\"shouldEnableScroll\" #bodyScroller>\n <table\n class=\"grid-table grid-body mb-0\"\n [ngClass]=\"tableClassList\"\n role=\"grid\"\n [attr.aria-readonly]=\"enableEdit || enableAdd ? 'false' : 'true'\"\n >\n <colgroup [ngbSyncColgroup]=\"colgroupSyncId\" syncRole=\"body\">\n <col *ngIf=\"isSelectionEnabled()\" style=\"width: 1%\" />\n <col *ngIf=\"rowDetailTpl\" style=\"width: 1%\" data-fixed=\"true\" />\n <col *ngIf=\"stickyRowsEnabled\" style=\"width: 1%\" />\n <col *ngFor=\"let col of columns\" />\n <col *ngIf=\"enableEdit || enableDelete\" style=\"width: 1%\" />\n </colgroup>\n <tbody>\n <!-- ADD NEW ROW -->\n <tr *ngIf=\"addingNew\" [formGroup]=\"addForm\">\n <!-- blank caret cell to keep alignment when detail column is present -->\n <td *ngIf=\"isSelectionEnabled()\"></td>\n <td *ngIf=\"rowDetailTpl\"></td>\n <td *ngIf=\"stickyRowsEnabled\" class=\"text-center\"></td>\n\n <td *ngFor=\"let col of columns\" [attr.data-title]=\"col.header\">\n <ng-container [ngSwitch]=\"col.type\">\n <!-- boolean -->\n <div\n *ngSwitchCase=\"'boolean'\"\n class=\"form-check m-0\"\n [class.is-invalid]=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.invalid\n \"\n >\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.invalid\n ? 'true'\n : null\n \"\n [attr.aria-describedby]=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.invalid\n ? 'add-error-' + col.field\n : null\n \"\n />\n </div>\n\n <!-- number/email/date/text -->\n <input\n *ngSwitchDefault\n class=\"form-control form-control-sm\"\n [class.is-invalid]=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.invalid\n \"\n [attr.type]=\"\n col.type === 'number'\n ? 'number'\n : col.type === 'email'\n ? 'email'\n : col.type === 'date'\n ? 'date'\n : 'text'\n \"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.invalid\n ? 'true'\n : null\n \"\n [attr.aria-describedby]=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.invalid\n ? 'add-error-' + col.field\n : null\n \"\n />\n </ng-container>\n\n <div\n class=\"invalid-feedback d-block\"\n *ngIf=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.errors as e\n \"\n [attr.id]=\"'add-error-' + col.field\"\n role=\"alert\"\n >\n <ng-container *ngIf=\"e['required']\">Required</ng-container>\n <ng-container *ngIf=\"e['email']\">Invalid email</ng-container>\n <ng-container *ngIf=\"e['number']\">Invalid number</ng-container>\n <ng-container *ngIf=\"e['date']\">Invalid date</ng-container>\n </div>\n </td>\n\n <td\n *ngIf=\"enableEdit || enableDelete\"\n class=\"text-nowrap text-center\"\n >\n <button\n type=\"button\"\n class=\"btn btn-sm btn-success me-1\"\n (click)=\"saveAdd()\"\n [disabled]=\"addForm.invalid\"\n >\n Save\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-secondary\"\n (click)=\"cancelAdd()\"\n >\n Cancel\n </button>\n </td>\n </tr>\n\n <!-- DATA ROWS + DETAIL ROWS -->\n <ng-container\n *ngFor=\"let row of paged; let i = index; trackBy: trackRow\"\n >\n <!-- DATA ROW (click-to-edit supported) -->\n <tr\n (click)=\"onRowSelect($event, i); onRowClick($event, i)\"\n [formGroup]=\"editForm\"\n [class.sticky-row]=\"isRowSticky(row, i)\"\n [class.row-selected]=\"isRowSelected(row, i)\"\n [class.row-highlight]=\"isRowHighlighted(row, i)\"\n [attr.aria-selected]=\"isRowSelected(row, i) ? 'true' : null\"\n [style.top.px]=\"stickyTop(row, i)\"\n [style.z-index]=\"isRowSticky(row, i) ? 3 : null\"\n >\n <td *ngIf=\"isSelectionEnabled()\" class=\"text-center align-middle\">\n <input\n type=\"checkbox\"\n [checked]=\"isRowSelected(row, i)\"\n [disabled]=\"\n selectionMode === 'none' ||\n selectionBehavior === 'row' ||\n isSelectionDisabled(row, i)\n \"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleSelection(i, $event)\"\n [attr.aria-label]=\"rowSelectionLabel(i)\"\n />\n </td>\n\n <!-- caret/expander cell (left-most) -->\n <td *ngIf=\"rowDetailTpl\" class=\"text-center align-middle\">\n <button\n type=\"button\"\n class=\"expand btn btn-link p-0\"\n (click)=\"$event.stopPropagation(); toggleExpand(i)\"\n [attr.aria-expanded]=\"isExpanded(i)\"\n [attr.aria-controls]=\"'dg-row-detail-' + i\"\n [attr.aria-label]=\"\n isExpanded(i) ? collapseRowAriaLabel : expandRowAriaLabel\n \"\n >\n {{ isExpanded(i) ? \"-\" : \"+\" }}\n </button>\n </td>\n\n <td\n *ngIf=\"stickyRowsEnabled\"\n class=\"text-center align-middle\"\n data-title=\"Sticky\"\n >\n <button\n type=\"button\"\n class=\"btn btn-link p-0 no-edit-trigger sticky-toggle\"\n (click)=\"$event.stopPropagation(); toggleStickyRow(i)\"\n [attr.aria-pressed]=\"isRowSticky(row, i)\"\n aria-label=\"Toggle sticky row\"\n >\n <ng-container *ngIf=\"stickyIcon(row, i) as icon\">\n <span\n class=\"bi\"\n [ngClass]=\"'bi-' + icon\"\n aria-hidden=\"true\"\n ></span>\n </ng-container>\n </button>\n </td>\n\n <!-- DATA CELLS -->\n <td\n *ngFor=\"let col of columns; let ci = index\"\n [attr.data-title]=\"col.header\"\n [attr.title]=\"cellTitle(row, col)\"\n [class.cell-highlight]=\"isCellHighlighted(row, i, col, ci)\"\n >\n <!-- EDIT MODE -->\n <ng-container\n *ngIf=\"\n editingIndex === i && (col.editable ?? true);\n else readCell\n \"\n >\n <!-- editor template override -->\n <ng-container\n *ngIf=\"editTpls[col.field] as et; else defaultEditor\"\n [ngTemplateOutlet]=\"et.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: editForm.get(col.field),\n control: editForm.get(col.field),\n row: row,\n col: col,\n form: editForm,\n index: i,\n isNew: false,\n }\"\n >\n </ng-container>\n\n <!-- default editors -->\n <ng-template #defaultEditor>\n <ng-container [ngSwitch]=\"col.type\">\n <div *ngSwitchCase=\"'boolean'\" class=\"form-check m-0\">\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"\n (editForm?.touched || saveAttemptedEdit) &&\n editForm?.get(col.field)?.invalid\n ? 'true'\n : null\n \"\n [attr.aria-describedby]=\"\n (editForm?.touched || saveAttemptedEdit) &&\n editForm?.get(col.field)?.invalid\n ? 'edit-error-' + col.field\n : null\n \"\n />\n </div>\n <select\n *ngSwitchCase=\"'select'\"\n class=\"form-select form-select-sm\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n >\n <option\n *ngFor=\"let o of col.options ?? []\"\n [ngValue]=\"o.value\"\n >\n {{ o.label }}\n </option>\n </select>\n <input\n *ngSwitchDefault\n class=\"form-control form-control-sm\"\n [attr.type]=\"\n col.type === 'number'\n ? 'number'\n : col.type === 'email'\n ? 'email'\n : col.type === 'date'\n ? 'date'\n : 'text'\n \"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"\n (editForm?.touched || saveAttemptedEdit) &&\n editForm?.get(col.field)?.invalid\n ? 'true'\n : null\n \"\n [attr.aria-describedby]=\"\n (editForm?.touched || saveAttemptedEdit) &&\n editForm?.get(col.field)?.invalid\n ? 'edit-error-' + col.field\n : null\n \"\n />\n </ng-container>\n\n <div\n class=\"invalid-feedback d-block\"\n *ngIf=\"\n (editForm?.touched || saveAttemptedEdit) &&\n editForm?.get(col.field)?.errors as e\n \"\n [attr.id]=\"'edit-error-' + col.field\"\n role=\"alert\"\n >\n <span *ngIf=\"e['required']\">Required</span>\n <span *ngIf=\"e['email']\">Invalid email</span>\n <span *ngIf=\"e['number']\">Invalid number</span>\n <span *ngIf=\"e['date']\">Invalid date</span>\n </div>\n </ng-template>\n </ng-container>\n\n <!-- READ MODE (with optional cell template) -->\n <ng-template #readCell>\n <ng-container\n *ngIf=\"cellTpls[col.field] as ct; else defaultCell\"\n [ngTemplateOutlet]=\"ct.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: row[col.field],\n row: row,\n col: col,\n index: i,\n }\"\n >\n </ng-container>\n <ng-template #defaultCell>\n <ng-container [ngSwitch]=\"col.type\">\n <span *ngSwitchCase=\"'boolean'\">{{\n row[col.field] ? \"Yes\" : \"No\"\n }}</span>\n <span *ngSwitchDefault>{{ row[col.field] }}</span>\n </ng-container>\n </ng-template>\n </ng-template>\n </td>\n\n <!-- ACTIONS -->\n <td\n *ngIf=\"enableEdit || enableDelete\"\n class=\"text-nowrap text-center\"\n >\n <ng-container *ngIf=\"editingIndex !== i; else editBtns\">\n <button\n type=\"button\"\n *ngIf=\"enableEdit\"\n class=\"btn btn-sm btn-outline-primary me-1 no-edit-trigger\"\n (click)=\"startEdit(i)\"\n >\n Edit\n </button>\n <button\n type=\"button\"\n *ngIf=\"enableDelete\"\n class=\"btn btn-sm btn-outline-danger no-edit-trigger\"\n (click)=\"deleteRow(i)\"\n >\n Delete\n </button>\n </ng-container>\n <ng-template #editBtns>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-success me-1\"\n (click)=\"saveEdit(i)\"\n [disabled]=\"editForm.invalid\"\n >\n Save\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-secondary\"\n (click)=\"cancelEdit(i)\"\n >\n Cancel\n </button>\n </ng-template>\n </td>\n </tr>\n\n <!-- DETAIL ROW (spans all columns) -->\n <tr\n *ngIf=\"rowDetailTpl && isExpanded(i)\"\n [attr.id]=\"'dg-row-detail-' + i\"\n >\n <td [attr.colspan]=\"detailColspan\" role=\"region\">\n <ng-container\n [ngTemplateOutlet]=\"rowDetailTpl.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row, index: i }\"\n >\n </ng-container>\n </td>\n </tr>\n </ng-container>\n </tbody>\n </table>\n </div>\n <table class=\"table grid-table grid-foot mb-0\">\n <colgroup>\n <ng-container *ngFor=\"let col of columns\">\n <col [style.width.px]=\"col?.width\" />\n </ng-container>\n </colgroup>\n <tbody>\n <tr>\n <td [attr.colspan]=\"columns.length\" width=\"100%\">\n <div\n *ngIf=\"enablePagination\"\n class=\"d-flex align-items-center justify-content-between mt-2 grid-footer\"\n [class.sticky-footer]=\"isFooterSticky\"\n >\n <!-- left: count -->\n <div class=\"small text-muted\" aria-live=\"polite\">\n {{ startIndex }} - {{ endIndex }} of {{ sorted.length }}\n </div>\n\n <!-- center: pager -->\n <div class=\"flex-grow-1 d-flex justify-content-center\">\n <ngb-pagination\n [page]=\"page\"\n [pageSize]=\"pageSize\"\n [collectionSize]=\"sorted.length\"\n [maxSize]=\"5\"\n (pageChange)=\"onPage($event)\"\n >\n </ngb-pagination>\n </div>\n\n <!-- right: rows per page -->\n <div class=\"d-flex align-items-center gap-2\">\n <label class=\"form-label form-label-sm mb-0\" for=\"pageSize\"\n >Rows:</label\n >\n <select\n id=\"pageSize\"\n class=\"form-select form-select-sm\"\n [(ngModel)]=\"pageSize\"\n (ngModelChange)=\"onPageSize($event)\"\n aria-label=\"Rows per page\"\n >\n <option *ngFor=\"let s of pageSizeOptions\" [value]=\"s\">\n {{ s }}\n </option>\n </select>\n </div>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n</div>\n\n<!-- Footer (left = count, center = pagination, right = page size) -->\n", styles: ["@charset \"UTF-8\";:host{display:block}.table{margin-bottom:.5rem}th.sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.sort-indicator{margin-left:.35rem;font-size:.75rem}.expand{cursor:pointer;font-size:24px;line-height:18px;display:block}.sticky-toggle{display:inline-flex;align-items:center;justify-content:center;gap:.15rem}.table-wrapper{position:relative}.table-wrapper{overflow-x:auto;border:1px solid #dee2e6;border-radius:12px;background:#fff}.table-body-scroll{max-height:320px;overflow-y:auto;overflow-x:hidden;border-top:1px solid #e9ecef;border-bottom:1px solid #e9ecef;scrollbar-gutter:stable both-edges;display:inline-block;width:fit-content;vertical-align:top}.grid-head{border-bottom:0}.grid-table{width:100%;min-width:1200px;table-layout:fixed;margin:0;border-collapse:separate;border-spacing:0}.grid-head{position:relative;z-index:5}.grid-head thead tr:first-child th:first-child{border-top-left-radius:12px}.grid-head thead th{background:var(--bs-table-bg, #f8f9fa);border-bottom:1px solid #dee2e6;font-weight:600}.grid-table th,.grid-table td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;border-bottom:1px solid #e9ecef;padding:.55rem .65rem}.filter-row td{background:#fff;border-bottom:1px solid #dee2e6;padding-top:.45rem;padding-bottom:.45rem}.sticky-row{position:sticky;background:#ccc;z-index:4}.sticky-row td{background:#ccc}.row-selected{background-color:#e7f1ff}.row-highlight{background-color:var(--bs-warning-bg-subtle, #fff3cd)}.row-highlight td{background-color:inherit}.cell-highlight{background-color:var(--bs-info-bg-subtle, #cff4fc)}.ngb-grid[data-theme=material]{--ngb-primary: #3f51b5}.ngb-grid[data-theme=material] .btn{border-color:var(--ngb-primary)}.ngb-grid[data-theme=material] table.table th,.ngb-grid[data-theme=material] table.table td{border-bottom:1px solid #e5e7eb}.ngb-grid[data-theme=tailwind]{--ngb-primary: rgb(29 78 216)}.ngb-grid[data-theme=tailwind] .btn{border-color:var(--ngb-primary);border-radius:.75rem}.ngb-grid[data-theme=tailwind] table.table th,.ngb-grid[data-theme=tailwind] table.table td{border-bottom:1px solid #e5e7eb;padding:.5rem}.ngb-responsive .table{width:100%}@media(max-width:768px){.ngb-responsive thead{display:none}.ngb-responsive tbody tr{display:grid;grid-template-columns:1fr;gap:.5rem;border:1px solid #e5e7eb;border-radius:.75rem;padding:.75rem;margin-bottom:.75rem}.ngb-responsive tbody td{display:grid;grid-template-columns:8rem 1fr}.ngb-responsive tbody td:before{content:attr(data-title);font-weight:600;opacity:.75;padding-right:.5rem}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.NgSelectOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.ɵNgSelectMultipleOption, selector: "option", inputs: ["ngValue", "value"] }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.CheckboxControlValueAccessor, selector: "input[type=checkbox][formControlName],input[type=checkbox][formControl],input[type=checkbox][ngModel]" }, { kind: "directive", type: i2.SelectControlValueAccessor, selector: "select:not([multiple])[formControlName],select:not([multiple])[formControl],select:not([multiple])[ngModel]", inputs: ["compareWith"] }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],[formArray],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "component", type: NgbPaginationComponent, selector: "ngb-pagination", inputs: ["page", "pageSize", "collectionSize", "maxSize"], outputs: ["pageChange"] }, { kind: "ngmodule", type: ReactiveFormsModule }, { kind: "directive", type: i2.FormControlDirective, selector: "[formControl]", inputs: ["formControl", "disabled", "ngModel"], outputs: ["ngModelChange"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormGroupDirective, selector: "[formGroup]", inputs: ["formGroup"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }, { kind: "directive", type: i2.FormControlName, selector: "[formControlName]", inputs: ["formControlName", "disabled", "ngModel"], outputs: ["ngModelChange"] }, { kind: "directive", type: NgbSyncColgroupDirective, selector: "[ngbSyncColgroup]", inputs: ["ngbSyncColgroup", "syncRole"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
1482
1540
  }
1483
1541
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.9", ngImport: i0, type: Datagrid, decorators: [{
1484
1542
  type: Component,
1485
1543
  args: [{ selector: 'ngb-datagrid', imports: [CommonModule, FormsModule, NgbPaginationComponent, ReactiveFormsModule, NgbSyncColgroupDirective, NgbGridHighlightDirective], providers: [
1486
1544
  { provide: PdfExportAdapter, useClass: JsPdfAdapter },
1487
1545
  { provide: ExcelExportAdapter, useClass: XlsxAdapter }
1488
- ], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\n<div class=\"ngb-grid\" [attr.data-theme]=\"theme\" [class.ngb-responsive]=\"isResponsiveEnabled()\">\n <!-- Export toolbar -->\n <div class=\"d-flex justify-content-end mb-2\" *ngIf=\"exportOptions?.enabled\">\n <ng-container *ngIf=\"exportButtonTpl; else defaultExportBtns\"\n [ngTemplateOutlet]=\"exportButtonTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: triggerExport }\">\n </ng-container>\n\n <ng-template #defaultExportBtns>\n <button type=\"button\"\n class=\"btn btn-sm btn-outline-secondary me-2\"\n *ngIf=\"exportOptions.type === 'pdf' || exportOptions.type === 'both'\"\n [disabled]=\"exporting\"\n (click)=\"export('pdf')\"\n [attr.aria-label]=\"exportAriaLabel('pdf')\">\n Export to PDF\n </button>\n <button type=\"button\"\n class=\"btn btn-sm btn-outline-secondary\"\n *ngIf=\"exportOptions.type === 'excel' || exportOptions.type === 'both'\"\n [disabled]=\"exporting\"\n (click)=\"export('excel')\"\n [attr.aria-label]=\"exportAriaLabel('excel')\">\n Export to Excel\n </button>\n </ng-template>\n </div>\n <div class=\"d-flex justify-content-end mb-2\" *ngIf=\"enableAdd\">\n <button\n type=\"button\"\n class=\"btn btn-sm btn-primary\"\n (click)=\"startAdd()\"\n [disabled]=\"addingNew\"\n [attr.aria-label]=\"addButtonAriaLabel || addButtonText\"\n >\n {{ addButtonText }}\n </button>\n </div>\n <!-- Global filter -->\n <div class=\"mb-2\" *ngIf=\"enableFiltering && enableGlobalFilter\">\n <ng-container\n *ngIf=\"globalTpl; else defaultGlobalFilter\"\n [ngTemplateOutlet]=\"globalTpl?.template\"\n [ngTemplateOutletContext]=\"{ $implicit: globalFilterCtrl }\"\n >\n </ng-container>\n\n <ng-template #defaultGlobalFilter>\n <input\n type=\"search\"\n class=\"form-control form-control-sm\"\n placeholder=\"Search all columns...\"\n [formControl]=\"globalFilterCtrl\"\n [attr.aria-label]=\"globalFilterAriaLabel\"\n />\n </ng-template>\n </div>\n\n <div class=\"table-wrapper\"\n [class.scrollable]=\"shouldEnableScroll\"\n [ngClass]=\"responsiveWrapperClasses\">\n <div class=\"table-header\" [style.paddingRight.px]=\"scrollbarWidth\" #headerScroller (scroll)=\"onHeaderScroll()\">\n <table [ngClass]=\"tableClassList\">\n <caption *ngIf=\"tableOptions?.caption\"\n [class.caption-top]=\"(tableOptions?.captionSide ?? 'top') === 'top'\">\n {{ tableOptions?.caption }}\n </caption>\n <colgroup [ngbSyncColgroup]=\"colgroupSyncId\" syncRole=\"header\">\n <col *ngIf=\"isSelectionEnabled()\" style=\"width:1%\">\n <col *ngIf=\"rowDetailTpl\" style=\"width:1%\">\n <col *ngIf=\"stickyRowsEnabled\" style=\"width:1%\">\n <col *ngFor=\"let col of columns\">\n <col *ngIf=\"enableEdit || enableDelete\" style=\"width:1%\">\n </colgroup>\n <thead class=\"thead-light\">\n <tr>\n <th *ngIf=\"isSelectionEnabled()\" class=\"text-center\" scope=\"col\" style=\"width:1%\">\n <ng-container *ngIf=\"selectionMode === 'multiple' && selectAllEnabled\">\n <input type=\"checkbox\"\n [checked]=\"isPageAllSelected()\"\n [indeterminate]=\"isPageIndeterminate()\"\n (change)=\"toggleSelectAllCurrentPage()\"\n [attr.aria-label]=\"selectAllLabel()\">\n </ng-container>\n </th>\n <th *ngIf=\"rowDetailTpl\" style=\"width:1%\" scope=\"col\" aria-hidden=\"true\"></th>\n <th *ngIf=\"stickyRowsEnabled\" style=\"width:1%\" scope=\"col\" class=\"text-center\" data-title=\"Sticky\"></th>\n <th\n *ngFor=\"let col of columns\"\n [attr.data-title]=\"col.header\"\n [class.sortable]=\"enableSorting && col.sortable\"\n scope=\"col\"\n [attr.aria-sort]=\"enableSorting && col.sortable ? ariaSortFor(col.field) : null\"\n >\n <ng-container *ngIf=\"enableSorting && col.sortable; else plainHeader\">\n <button\n type=\"button\"\n class=\"btn btn-link p-0 text-start w-100\"\n (click)=\"toggleSort(col.field)\"\n [attr.aria-label]=\"sortButtonAriaLabel(col)\"\n >\n <span>{{ col.header }}</span>\n <span\n class=\"sort-indicator\"\n *ngIf=\"enableSorting && sort.active === col.field\"\n aria-hidden=\"true\"\n >\n {{\n sort.direction === 'asc'\n ? '\u25B2'\n : sort.direction === 'desc'\n ? '\u25BC'\n : ''\n }}\n </span>\n </button>\n </ng-container>\n <ng-template #plainHeader>\n <span>{{ col.header }}</span>\n </ng-template>\n </th>\n <th\n *ngIf=\"enableEdit || enableDelete\"\n class=\"text-center\"\n scope=\"col\"\n style=\"width: 1%\"\n >\n Actions\n </th>\n </tr>\n\n <!-- Per-column filters -->\n <tr *ngIf=\"anyFilterable\" [formGroup]=\"filterForm\">\n <th *ngIf=\"isSelectionEnabled()\"></th>\n <th *ngIf=\"stickyRowsEnabled\"></th>\n <th *ngFor=\"let col of columns\" [attr.data-title]=\"col.header\">\n <ng-container *ngIf=\"col.filterable\">\n <ng-container *ngIf=\"filterTpls[col.field] as ft; else defaultFilter\">\n <ng-container\n [ngTemplateOutlet]=\"ft.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: filterForm.get(col.field),\n control: filterForm.get(col.field),\n col: col\n }\"\n >\n </ng-container>\n </ng-container>\n <ng-template #defaultFilter>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"columnFilterAriaLabel(col)\"\n />\n </ng-template>\n </ng-container>\n </th>\n <th *ngIf=\"enableEdit || enableDelete\"></th>\n </tr>\n </thead>\n </table>\n </div>\n\n <div class=\"table-body\" #bodyScroller>\n <table [ngClass]=\"tableClassList\"\n role=\"grid\"\n [attr.aria-readonly]=\"(enableEdit || enableAdd) ? 'false' : 'true'\">\n <colgroup [ngbSyncColgroup]=\"colgroupSyncId\" syncRole=\"body\">\n <col *ngIf=\"isSelectionEnabled()\" style=\"width:1%\">\n <col *ngIf=\"rowDetailTpl\" style=\"width:1%\">\n <col *ngIf=\"stickyRowsEnabled\" style=\"width:1%\">\n <col *ngFor=\"let col of columns\">\n <col *ngIf=\"enableEdit || enableDelete\" style=\"width:1%\">\n </colgroup>\n <tbody>\n <!-- ADD NEW ROW -->\n <tr *ngIf=\"addingNew\" [formGroup]=\"addForm\">\n <!-- blank caret cell to keep alignment when detail column is present -->\n <td *ngIf=\"isSelectionEnabled()\"></td>\n <td *ngIf=\"rowDetailTpl\"></td>\n <td *ngIf=\"stickyRowsEnabled\" class=\"text-center\"></td>\n\n <td *ngFor=\"let col of columns\" [attr.data-title]=\"col.header\">\n <ng-container [ngSwitch]=\"col.type\">\n <!-- boolean -->\n <div\n *ngSwitchCase=\"'boolean'\"\n class=\"form-check m-0\"\n [class.is-invalid]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid\">\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid ? 'true' : null\"\n [attr.aria-describedby]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid ? 'add-error-' + col.field : null\"\n />\n </div>\n\n <!-- number/email/date/text -->\n <input\n *ngSwitchDefault\n class=\"form-control form-control-sm\"\n [class.is-invalid]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid\"\n [attr.type]=\"col.type === 'number' ? 'number' :\n col.type === 'email' ? 'email' :\n col.type === 'date' ? 'date' : 'text'\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid ? 'true' : null\"\n [attr.aria-describedby]=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.invalid ? 'add-error-' + col.field : null\"\n />\n </ng-container>\n\n <div class=\"invalid-feedback d-block\"\n *ngIf=\"(addForm.get(col.field)?.touched || saveAttemptedNew) && addForm.get(col.field)?.errors as e\"\n [attr.id]=\"'add-error-' + col.field\"\n role=\"alert\">\n <ng-container *ngIf=\"e['required']\">Required</ng-container>\n <ng-container *ngIf=\"e['email']\">Invalid email</ng-container>\n <ng-container *ngIf=\"e['number']\">Invalid number</ng-container>\n <ng-container *ngIf=\"e['date']\">Invalid date</ng-container>\n </div>\n </td>\n\n <td *ngIf=\"enableEdit || enableDelete\" class=\"text-nowrap text-center\">\n <button type=\"button\" class=\"btn btn-sm btn-success me-1\" (click)=\"saveAdd()\" [disabled]=\"addForm.invalid\">Save</button>\n <button type=\"button\" class=\"btn btn-sm btn-secondary\" (click)=\"cancelAdd()\">Cancel</button>\n </td>\n </tr>\n\n <!-- DATA ROWS + DETAIL ROWS -->\n <ng-container *ngFor=\"let row of paged; let i = index; trackBy: trackRow\">\n\n <!-- DATA ROW (click-to-edit supported) -->\n <tr (click)=\"onRowSelect($event, i); onRowClick($event, i)\" [formGroup]=\"editForm\"\n [class.sticky-row]=\"isRowSticky(row, i)\"\n [class.row-selected]=\"isRowSelected(row, i)\"\n [class.row-highlight]=\"isRowHighlighted(row, i)\"\n [attr.aria-selected]=\"isRowSelected(row, i) ? 'true' : null\"\n [style.top.px]=\"stickyTop(row, i)\"\n [style.z-index]=\"isRowSticky(row, i) ? 3 : null\">\n\n <td *ngIf=\"isSelectionEnabled()\" class=\"text-center align-middle\">\n <input type=\"checkbox\"\n [checked]=\"isRowSelected(row, i)\"\n [disabled]=\"selectionMode === 'none' || selectionBehavior === 'row' || isSelectionDisabled(row, i)\"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleSelection(i, $event)\"\n [attr.aria-label]=\"rowSelectionLabel(i)\">\n </td>\n\n <!-- caret/expander cell (left-most) -->\n <td *ngIf=\"rowDetailTpl\" class=\"text-center align-middle\">\n <button\n type=\"button\"\n class=\"expand btn btn-link p-0\"\n (click)=\"$event.stopPropagation(); toggleExpand(i)\"\n [attr.aria-expanded]=\"isExpanded(i)\"\n [attr.aria-controls]=\"'dg-row-detail-' + i\"\n [attr.aria-label]=\"isExpanded(i) ? collapseRowAriaLabel : expandRowAriaLabel\">\n {{ isExpanded(i) ? '-' : '+' }}\n </button>\n </td>\n\n <td *ngIf=\"stickyRowsEnabled\" class=\"text-center align-middle\" data-title=\"Sticky\">\n <button\n type=\"button\"\n class=\"btn btn-link p-0 no-edit-trigger sticky-toggle\"\n (click)=\"$event.stopPropagation(); toggleStickyRow(i)\"\n [attr.aria-pressed]=\"isRowSticky(row, i)\"\n aria-label=\"Toggle sticky row\">\n <ng-container *ngIf=\"stickyIcon(row, i) as icon\">\n <span class=\"bi\" [ngClass]=\"'bi-' + icon\" aria-hidden=\"true\"></span>\n </ng-container>\n </button>\n </td>\n\n <!-- DATA CELLS -->\n <td *ngFor=\"let col of columns; let ci = index\"\n [attr.data-title]=\"col.header\"\n [class.cell-highlight]=\"isCellHighlighted(row, i, col, ci)\">\n <!-- EDIT MODE -->\n <ng-container *ngIf=\"editingIndex === i && (col.editable ?? true); else readCell\">\n <!-- editor template override -->\n <ng-container *ngIf=\"editTpls[col.field] as et; else defaultEditor\"\n [ngTemplateOutlet]=\"et.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: editForm.get(col.field),\n control: editForm.get(col.field),\n row: row, col: col, form: editForm, index: i, isNew: false\n }\">\n </ng-container>\n\n <!-- default editors -->\n <ng-template #defaultEditor>\n <ng-container [ngSwitch]=\"col.type\">\n <div *ngSwitchCase=\"'boolean'\" class=\"form-check m-0\">\n <input type=\"checkbox\"\n class=\"form-check-input\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.invalid ? 'true' : null\"\n [attr.aria-describedby]=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.invalid ? 'edit-error-' + col.field : null\" />\n </div>\n <select *ngSwitchCase=\"'select'\"\n class=\"form-select form-select-sm\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\">\n <option *ngFor=\"let o of col.options ?? []\" [ngValue]=\"o.value\">{{ o.label }}</option>\n </select>\n <input *ngSwitchDefault\n class=\"form-control form-control-sm\"\n [attr.type]=\"col.type === 'number' ? 'number' :\n col.type === 'email' ? 'email' :\n col.type === 'date' ? 'date' : 'text'\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.invalid ? 'true' : null\"\n [attr.aria-describedby]=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.invalid ? 'edit-error-' + col.field : null\" />\n </ng-container>\n\n <div class=\"invalid-feedback d-block\"\n *ngIf=\"(editForm?.touched || saveAttemptedEdit) && editForm?.get(col.field)?.errors as e\"\n [attr.id]=\"'edit-error-' + col.field\"\n role=\"alert\">\n <span *ngIf=\"e['required']\">Required</span>\n <span *ngIf=\"e['email']\">Invalid email</span>\n <span *ngIf=\"e['number']\">Invalid number</span>\n <span *ngIf=\"e['date']\">Invalid date</span>\n </div>\n </ng-template>\n </ng-container>\n\n <!-- READ MODE (with optional cell template) -->\n <ng-template #readCell>\n <ng-container *ngIf=\"cellTpls[col.field] as ct; else defaultCell\"\n [ngTemplateOutlet]=\"ct.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row[col.field], row: row, col: col, index: i }\">\n </ng-container>\n <ng-template #defaultCell>\n <ng-container [ngSwitch]=\"col.type\">\n <span *ngSwitchCase=\"'boolean'\">{{ row[col.field] ? 'Yes' : 'No' }}</span>\n <span *ngSwitchDefault>{{ row[col.field] }}</span>\n </ng-container>\n </ng-template>\n </ng-template>\n </td>\n\n <!-- ACTIONS -->\n <td *ngIf=\"enableEdit || enableDelete\" class=\"text-nowrap text-center\">\n <ng-container *ngIf=\"editingIndex !== i; else editBtns\">\n <button type=\"button\" *ngIf=\"enableEdit\" class=\"btn btn-sm btn-outline-primary me-1 no-edit-trigger\" (click)=\"startEdit(i)\">Edit</button>\n <button type=\"button\" *ngIf=\"enableDelete\" class=\"btn btn-sm btn-outline-danger no-edit-trigger\" (click)=\"deleteRow(i)\">Delete</button>\n </ng-container>\n <ng-template #editBtns>\n <button type=\"button\" class=\"btn btn-sm btn-success me-1\" (click)=\"saveEdit(i)\" [disabled]=\"editForm.invalid\">Save</button>\n <button type=\"button\" class=\"btn btn-sm btn-secondary\" (click)=\"cancelEdit(i)\">Cancel</button>\n </ng-template>\n </td>\n </tr>\n\n <!-- DETAIL ROW (spans all columns) -->\n <tr *ngIf=\"rowDetailTpl && isExpanded(i)\" [attr.id]=\"'dg-row-detail-' + i\">\n <td [attr.colspan]=\"detailColspan\" role=\"region\">\n <ng-container [ngTemplateOutlet]=\"rowDetailTpl.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row, index: i }\">\n </ng-container>\n </td>\n </tr>\n\n </ng-container>\n </tbody>\n\n </table>\n </div>\n </div>\n</div>\n\n<!-- Footer (left = count, center = pagination, right = page size) -->\n<div *ngIf=\"enablePagination\"\n class=\"d-flex align-items-center justify-content-between mt-2 grid-footer\"\n [class.sticky-footer]=\"isFooterSticky\">\n <!-- left: count -->\n <div class=\"small text-muted\" aria-live=\"polite\">\n {{ startIndex }} - {{ endIndex }} of {{ sorted.length }}\n\n </div>\n\n <!-- center: pager -->\n <div class=\"flex-grow-1 d-flex justify-content-center\">\n <ngb-pagination\n [page]=\"page\"\n [pageSize]=\"pageSize\"\n [collectionSize]=\"sorted.length\"\n [maxSize]=\"5\"\n (pageChange)=\"onPage($event)\">\n </ngb-pagination>\n </div>\n\n <!-- right: rows per page -->\n <div class=\"d-flex align-items-center gap-2\">\n <label class=\"form-label form-label-sm mb-0\" for=\"pageSize\">Rows:</label>\n <select\n id=\"pageSize\"\n class=\"form-select form-select-sm\"\n [(ngModel)]=\"pageSize\"\n (ngModelChange)=\"onPageSize($event)\"\n aria-label=\"Rows per page\">\n <option *ngFor=\"let s of pageSizeOptions\" [value]=\"s\">{{ s }}</option>\n </select>\n </div>\n</div>\n", styles: ["@charset \"UTF-8\";:host{display:block}.table{margin-bottom:.5rem}th.sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.sort-indicator{margin-left:.35rem;font-size:.75rem}.expand{cursor:pointer;font-size:24px;line-height:18px;display:block}.sticky-toggle{display:inline-flex;align-items:center;justify-content:center;gap:.15rem}.table-wrapper{position:relative}.table-wrapper{display:flex;flex-direction:column}.table-wrapper .table{table-layout:fixed;width:100%;margin-bottom:0}.table-header{overflow-x:auto;overflow-y:hidden}.table-body{max-height:60vh;overflow:auto;scrollbar-gutter:stable}.table-header th,.table-body td{min-width:40px;max-width:320px;word-break:break-word}.table-header th{white-space:nowrap}.table-body td{white-space:normal}.table-header col,.table-body col{min-width:40px;max-width:320px}.table-wrapper.sticky-header thead th{position:sticky;top:0;z-index:5;background:#ccc}.table-wrapper.sticky-footer .grid-footer{position:sticky;bottom:0;z-index:5;background:#fff;border-top:1px solid #e2e2e2;border-left:1px solid #e2e2e2;border-right:1px solid #e2e2e2;padding:5px 20px}.sticky-row{position:sticky;background:#ccc;z-index:4}.sticky-row td{background:#ccc}.row-selected{background-color:#e7f1ff}.row-highlight{background-color:var(--bs-warning-bg-subtle, #fff3cd)}.row-highlight td{background-color:inherit}.cell-highlight{background-color:var(--bs-info-bg-subtle, #cff4fc)}.ngb-grid[data-theme=material]{--ngb-primary: #3f51b5}.ngb-grid[data-theme=material] .btn{border-color:var(--ngb-primary)}.ngb-grid[data-theme=material] table.table th,.ngb-grid[data-theme=material] table.table td{border-bottom:1px solid #e5e7eb}.ngb-grid[data-theme=tailwind]{--ngb-primary: rgb(29 78 216)}.ngb-grid[data-theme=tailwind] .btn{border-color:var(--ngb-primary);border-radius:.75rem}.ngb-grid[data-theme=tailwind] table.table th,.ngb-grid[data-theme=tailwind] table.table td{border-bottom:1px solid #e5e7eb;padding:.5rem}.ngb-responsive .table{width:100%}@media(max-width:768px){.ngb-responsive thead{display:none}.ngb-responsive tbody tr{display:grid;grid-template-columns:1fr;gap:.5rem;border:1px solid #e5e7eb;border-radius:.75rem;padding:.75rem;margin-bottom:.75rem}.ngb-responsive tbody td{display:grid;grid-template-columns:8rem 1fr}.ngb-responsive tbody td:before{content:attr(data-title);font-weight:600;opacity:.75;padding-right:.5rem}}\n"] }]
1546
+ ], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\n<div\n class=\"ngb-grid\"\n [attr.data-theme]=\"theme\"\n [class.ngb-responsive]=\"isResponsiveEnabled()\"\n>\n <!-- Export toolbar -->\n <div class=\"d-flex justify-content-end mb-2\" *ngIf=\"exportOptions?.enabled\">\n <ng-container\n *ngIf=\"exportButtonTpl; else defaultExportBtns\"\n [ngTemplateOutlet]=\"exportButtonTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: triggerExport }\"\n >\n </ng-container>\n\n <ng-template #defaultExportBtns>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-secondary me-2\"\n *ngIf=\"exportOptions.type === 'pdf' || exportOptions.type === 'both'\"\n [disabled]=\"exporting\"\n (click)=\"export('pdf')\"\n [attr.aria-label]=\"exportAriaLabel('pdf')\"\n >\n Export to PDF\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-secondary\"\n *ngIf=\"exportOptions.type === 'excel' || exportOptions.type === 'both'\"\n [disabled]=\"exporting\"\n (click)=\"export('excel')\"\n [attr.aria-label]=\"exportAriaLabel('excel')\"\n >\n Export to Excel\n </button>\n </ng-template>\n </div>\n <div class=\"d-flex justify-content-end mb-2\" *ngIf=\"enableAdd\">\n <button\n type=\"button\"\n class=\"btn btn-sm btn-primary\"\n (click)=\"startAdd()\"\n [disabled]=\"addingNew\"\n [attr.aria-label]=\"addButtonAriaLabel || addButtonText\"\n >\n {{ addButtonText }}\n </button>\n </div>\n <!-- Global filter -->\n <div class=\"mb-2\" *ngIf=\"enableFiltering && enableGlobalFilter\">\n <ng-container\n *ngIf=\"globalTpl; else defaultGlobalFilter\"\n [ngTemplateOutlet]=\"globalTpl?.template\"\n [ngTemplateOutletContext]=\"{ $implicit: globalFilterCtrl }\"\n >\n </ng-container>\n\n <ng-template #defaultGlobalFilter>\n <input\n type=\"search\"\n class=\"form-control form-control-sm\"\n placeholder=\"Search all columns...\"\n [formControl]=\"globalFilterCtrl\"\n [attr.aria-label]=\"globalFilterAriaLabel\"\n />\n </ng-template>\n </div>\n\n <div\n class=\"table-wrapper\"\n [ngClass]=\"responsiveWrapperClasses\"\n >\n <!-- <div class=\"table-header\"> -->\n <table class=\"grid-table grid-header mb-0\" [ngClass]=\"tableClassList\">\n <caption\n *ngIf=\"tableOptions?.caption\"\n [class.caption-top]=\"(tableOptions?.captionSide ?? 'top') === 'top'\"\n >\n {{\n tableOptions?.caption\n }}\n </caption>\n <colgroup [ngbSyncColgroup]=\"colgroupSyncId\" syncRole=\"header\">\n <col *ngIf=\"isSelectionEnabled()\" style=\"width: 1%\" />\n <col *ngIf=\"rowDetailTpl\" style=\"width: 1%\" data-fixed=\"true\" />\n <col *ngIf=\"stickyRowsEnabled\" style=\"width: 1%\" />\n <col *ngFor=\"let col of columns\" />\n <col *ngIf=\"enableEdit || enableDelete\" style=\"width: 1%\" />\n </colgroup>\n <thead class=\"table-light\">\n <tr>\n <th\n *ngIf=\"isSelectionEnabled()\"\n class=\"text-center\"\n scope=\"col\"\n style=\"width: 1%\"\n >\n <ng-container\n *ngIf=\"selectionMode === 'multiple' && selectAllEnabled\"\n >\n <input\n type=\"checkbox\"\n [checked]=\"isPageAllSelected()\"\n [indeterminate]=\"isPageIndeterminate()\"\n (change)=\"toggleSelectAllCurrentPage()\"\n [attr.aria-label]=\"selectAllLabel()\"\n />\n </ng-container>\n </th>\n <th\n *ngIf=\"rowDetailTpl\"\n style=\"width: 1%\"\n scope=\"col\"\n aria-hidden=\"true\"\n ></th>\n <th\n *ngIf=\"stickyRowsEnabled\"\n style=\"width: 1%\"\n scope=\"col\"\n class=\"text-center\"\n data-title=\"Sticky\"\n ></th>\n <th\n *ngFor=\"let col of columns\"\n [attr.data-title]=\"col.header\"\n [class.sortable]=\"enableSorting && col.sortable\"\n scope=\"col\"\n [attr.title]=\"headerTitle(col)\"\n [attr.aria-sort]=\"\n enableSorting && col.sortable ? ariaSortFor(col.field) : null\n \"\n >\n <ng-container\n *ngIf=\"enableSorting && col.sortable; else plainHeader\"\n >\n <button\n type=\"button\"\n class=\"btn btn-link p-0 text-start w-100\"\n (click)=\"toggleSort(col.field)\"\n [attr.aria-label]=\"sortButtonAriaLabel(col)\"\n >\n <span>{{ col.header }}</span>\n <span\n class=\"sort-indicator\"\n *ngIf=\"enableSorting && sort.active === col.field\"\n aria-hidden=\"true\"\n >\n {{\n sort.direction === \"asc\"\n ? \"\u25B2\"\n : sort.direction === \"desc\"\n ? \"\u25BC\"\n : \"\"\n }}\n </span>\n </button>\n </ng-container>\n <ng-template #plainHeader>\n <span>{{ col.header }}</span>\n </ng-template>\n </th>\n <th\n *ngIf=\"enableEdit || enableDelete\"\n class=\"text-center\"\n scope=\"col\"\n style=\"width: 1%\"\n >\n Actions\n </th>\n </tr>\n\n <!-- Per-column filters -->\n <tr *ngIf=\"anyFilterable\" class=\"filter-row\" [formGroup]=\"filterForm\">\n <th *ngIf=\"isSelectionEnabled()\"></th>\n <th *ngIf=\"stickyRowsEnabled\"></th>\n <th *ngFor=\"let col of columns\" [attr.data-title]=\"col.header\">\n <ng-container *ngIf=\"col.filterable\">\n <ng-container\n *ngIf=\"filterTpls[col.field] as ft; else defaultFilter\"\n >\n <ng-container\n [ngTemplateOutlet]=\"ft.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: filterForm.get(col.field),\n control: filterForm.get(col.field),\n col: col,\n }\"\n >\n </ng-container>\n </ng-container>\n <ng-template #defaultFilter>\n <input\n type=\"text\"\n class=\"form-control form-control-sm\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"columnFilterAriaLabel(col)\"\n />\n </ng-template>\n </ng-container>\n </th>\n <th *ngIf=\"enableEdit || enableDelete\"></th>\n </tr>\n </thead>\n </table>\n <!-- </div> -->\n\n <div [class.table-body-scroll]=\"shouldEnableScroll\" #bodyScroller>\n <table\n class=\"grid-table grid-body mb-0\"\n [ngClass]=\"tableClassList\"\n role=\"grid\"\n [attr.aria-readonly]=\"enableEdit || enableAdd ? 'false' : 'true'\"\n >\n <colgroup [ngbSyncColgroup]=\"colgroupSyncId\" syncRole=\"body\">\n <col *ngIf=\"isSelectionEnabled()\" style=\"width: 1%\" />\n <col *ngIf=\"rowDetailTpl\" style=\"width: 1%\" data-fixed=\"true\" />\n <col *ngIf=\"stickyRowsEnabled\" style=\"width: 1%\" />\n <col *ngFor=\"let col of columns\" />\n <col *ngIf=\"enableEdit || enableDelete\" style=\"width: 1%\" />\n </colgroup>\n <tbody>\n <!-- ADD NEW ROW -->\n <tr *ngIf=\"addingNew\" [formGroup]=\"addForm\">\n <!-- blank caret cell to keep alignment when detail column is present -->\n <td *ngIf=\"isSelectionEnabled()\"></td>\n <td *ngIf=\"rowDetailTpl\"></td>\n <td *ngIf=\"stickyRowsEnabled\" class=\"text-center\"></td>\n\n <td *ngFor=\"let col of columns\" [attr.data-title]=\"col.header\">\n <ng-container [ngSwitch]=\"col.type\">\n <!-- boolean -->\n <div\n *ngSwitchCase=\"'boolean'\"\n class=\"form-check m-0\"\n [class.is-invalid]=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.invalid\n \"\n >\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.invalid\n ? 'true'\n : null\n \"\n [attr.aria-describedby]=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.invalid\n ? 'add-error-' + col.field\n : null\n \"\n />\n </div>\n\n <!-- number/email/date/text -->\n <input\n *ngSwitchDefault\n class=\"form-control form-control-sm\"\n [class.is-invalid]=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.invalid\n \"\n [attr.type]=\"\n col.type === 'number'\n ? 'number'\n : col.type === 'email'\n ? 'email'\n : col.type === 'date'\n ? 'date'\n : 'text'\n \"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.invalid\n ? 'true'\n : null\n \"\n [attr.aria-describedby]=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.invalid\n ? 'add-error-' + col.field\n : null\n \"\n />\n </ng-container>\n\n <div\n class=\"invalid-feedback d-block\"\n *ngIf=\"\n (addForm.get(col.field)?.touched || saveAttemptedNew) &&\n addForm.get(col.field)?.errors as e\n \"\n [attr.id]=\"'add-error-' + col.field\"\n role=\"alert\"\n >\n <ng-container *ngIf=\"e['required']\">Required</ng-container>\n <ng-container *ngIf=\"e['email']\">Invalid email</ng-container>\n <ng-container *ngIf=\"e['number']\">Invalid number</ng-container>\n <ng-container *ngIf=\"e['date']\">Invalid date</ng-container>\n </div>\n </td>\n\n <td\n *ngIf=\"enableEdit || enableDelete\"\n class=\"text-nowrap text-center\"\n >\n <button\n type=\"button\"\n class=\"btn btn-sm btn-success me-1\"\n (click)=\"saveAdd()\"\n [disabled]=\"addForm.invalid\"\n >\n Save\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-secondary\"\n (click)=\"cancelAdd()\"\n >\n Cancel\n </button>\n </td>\n </tr>\n\n <!-- DATA ROWS + DETAIL ROWS -->\n <ng-container\n *ngFor=\"let row of paged; let i = index; trackBy: trackRow\"\n >\n <!-- DATA ROW (click-to-edit supported) -->\n <tr\n (click)=\"onRowSelect($event, i); onRowClick($event, i)\"\n [formGroup]=\"editForm\"\n [class.sticky-row]=\"isRowSticky(row, i)\"\n [class.row-selected]=\"isRowSelected(row, i)\"\n [class.row-highlight]=\"isRowHighlighted(row, i)\"\n [attr.aria-selected]=\"isRowSelected(row, i) ? 'true' : null\"\n [style.top.px]=\"stickyTop(row, i)\"\n [style.z-index]=\"isRowSticky(row, i) ? 3 : null\"\n >\n <td *ngIf=\"isSelectionEnabled()\" class=\"text-center align-middle\">\n <input\n type=\"checkbox\"\n [checked]=\"isRowSelected(row, i)\"\n [disabled]=\"\n selectionMode === 'none' ||\n selectionBehavior === 'row' ||\n isSelectionDisabled(row, i)\n \"\n (click)=\"$event.stopPropagation()\"\n (change)=\"toggleSelection(i, $event)\"\n [attr.aria-label]=\"rowSelectionLabel(i)\"\n />\n </td>\n\n <!-- caret/expander cell (left-most) -->\n <td *ngIf=\"rowDetailTpl\" class=\"text-center align-middle\">\n <button\n type=\"button\"\n class=\"expand btn btn-link p-0\"\n (click)=\"$event.stopPropagation(); toggleExpand(i)\"\n [attr.aria-expanded]=\"isExpanded(i)\"\n [attr.aria-controls]=\"'dg-row-detail-' + i\"\n [attr.aria-label]=\"\n isExpanded(i) ? collapseRowAriaLabel : expandRowAriaLabel\n \"\n >\n {{ isExpanded(i) ? \"-\" : \"+\" }}\n </button>\n </td>\n\n <td\n *ngIf=\"stickyRowsEnabled\"\n class=\"text-center align-middle\"\n data-title=\"Sticky\"\n >\n <button\n type=\"button\"\n class=\"btn btn-link p-0 no-edit-trigger sticky-toggle\"\n (click)=\"$event.stopPropagation(); toggleStickyRow(i)\"\n [attr.aria-pressed]=\"isRowSticky(row, i)\"\n aria-label=\"Toggle sticky row\"\n >\n <ng-container *ngIf=\"stickyIcon(row, i) as icon\">\n <span\n class=\"bi\"\n [ngClass]=\"'bi-' + icon\"\n aria-hidden=\"true\"\n ></span>\n </ng-container>\n </button>\n </td>\n\n <!-- DATA CELLS -->\n <td\n *ngFor=\"let col of columns; let ci = index\"\n [attr.data-title]=\"col.header\"\n [attr.title]=\"cellTitle(row, col)\"\n [class.cell-highlight]=\"isCellHighlighted(row, i, col, ci)\"\n >\n <!-- EDIT MODE -->\n <ng-container\n *ngIf=\"\n editingIndex === i && (col.editable ?? true);\n else readCell\n \"\n >\n <!-- editor template override -->\n <ng-container\n *ngIf=\"editTpls[col.field] as et; else defaultEditor\"\n [ngTemplateOutlet]=\"et.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: editForm.get(col.field),\n control: editForm.get(col.field),\n row: row,\n col: col,\n form: editForm,\n index: i,\n isNew: false,\n }\"\n >\n </ng-container>\n\n <!-- default editors -->\n <ng-template #defaultEditor>\n <ng-container [ngSwitch]=\"col.type\">\n <div *ngSwitchCase=\"'boolean'\" class=\"form-check m-0\">\n <input\n type=\"checkbox\"\n class=\"form-check-input\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"\n (editForm?.touched || saveAttemptedEdit) &&\n editForm?.get(col.field)?.invalid\n ? 'true'\n : null\n \"\n [attr.aria-describedby]=\"\n (editForm?.touched || saveAttemptedEdit) &&\n editForm?.get(col.field)?.invalid\n ? 'edit-error-' + col.field\n : null\n \"\n />\n </div>\n <select\n *ngSwitchCase=\"'select'\"\n class=\"form-select form-select-sm\"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n >\n <option\n *ngFor=\"let o of col.options ?? []\"\n [ngValue]=\"o.value\"\n >\n {{ o.label }}\n </option>\n </select>\n <input\n *ngSwitchDefault\n class=\"form-control form-control-sm\"\n [attr.type]=\"\n col.type === 'number'\n ? 'number'\n : col.type === 'email'\n ? 'email'\n : col.type === 'date'\n ? 'date'\n : 'text'\n \"\n [formControlName]=\"col.field\"\n [attr.aria-label]=\"inputAriaLabel(col)\"\n [attr.aria-invalid]=\"\n (editForm?.touched || saveAttemptedEdit) &&\n editForm?.get(col.field)?.invalid\n ? 'true'\n : null\n \"\n [attr.aria-describedby]=\"\n (editForm?.touched || saveAttemptedEdit) &&\n editForm?.get(col.field)?.invalid\n ? 'edit-error-' + col.field\n : null\n \"\n />\n </ng-container>\n\n <div\n class=\"invalid-feedback d-block\"\n *ngIf=\"\n (editForm?.touched || saveAttemptedEdit) &&\n editForm?.get(col.field)?.errors as e\n \"\n [attr.id]=\"'edit-error-' + col.field\"\n role=\"alert\"\n >\n <span *ngIf=\"e['required']\">Required</span>\n <span *ngIf=\"e['email']\">Invalid email</span>\n <span *ngIf=\"e['number']\">Invalid number</span>\n <span *ngIf=\"e['date']\">Invalid date</span>\n </div>\n </ng-template>\n </ng-container>\n\n <!-- READ MODE (with optional cell template) -->\n <ng-template #readCell>\n <ng-container\n *ngIf=\"cellTpls[col.field] as ct; else defaultCell\"\n [ngTemplateOutlet]=\"ct.template\"\n [ngTemplateOutletContext]=\"{\n $implicit: row[col.field],\n row: row,\n col: col,\n index: i,\n }\"\n >\n </ng-container>\n <ng-template #defaultCell>\n <ng-container [ngSwitch]=\"col.type\">\n <span *ngSwitchCase=\"'boolean'\">{{\n row[col.field] ? \"Yes\" : \"No\"\n }}</span>\n <span *ngSwitchDefault>{{ row[col.field] }}</span>\n </ng-container>\n </ng-template>\n </ng-template>\n </td>\n\n <!-- ACTIONS -->\n <td\n *ngIf=\"enableEdit || enableDelete\"\n class=\"text-nowrap text-center\"\n >\n <ng-container *ngIf=\"editingIndex !== i; else editBtns\">\n <button\n type=\"button\"\n *ngIf=\"enableEdit\"\n class=\"btn btn-sm btn-outline-primary me-1 no-edit-trigger\"\n (click)=\"startEdit(i)\"\n >\n Edit\n </button>\n <button\n type=\"button\"\n *ngIf=\"enableDelete\"\n class=\"btn btn-sm btn-outline-danger no-edit-trigger\"\n (click)=\"deleteRow(i)\"\n >\n Delete\n </button>\n </ng-container>\n <ng-template #editBtns>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-success me-1\"\n (click)=\"saveEdit(i)\"\n [disabled]=\"editForm.invalid\"\n >\n Save\n </button>\n <button\n type=\"button\"\n class=\"btn btn-sm btn-secondary\"\n (click)=\"cancelEdit(i)\"\n >\n Cancel\n </button>\n </ng-template>\n </td>\n </tr>\n\n <!-- DETAIL ROW (spans all columns) -->\n <tr\n *ngIf=\"rowDetailTpl && isExpanded(i)\"\n [attr.id]=\"'dg-row-detail-' + i\"\n >\n <td [attr.colspan]=\"detailColspan\" role=\"region\">\n <ng-container\n [ngTemplateOutlet]=\"rowDetailTpl.template\"\n [ngTemplateOutletContext]=\"{ $implicit: row, index: i }\"\n >\n </ng-container>\n </td>\n </tr>\n </ng-container>\n </tbody>\n </table>\n </div>\n <table class=\"table grid-table grid-foot mb-0\">\n <colgroup>\n <ng-container *ngFor=\"let col of columns\">\n <col [style.width.px]=\"col?.width\" />\n </ng-container>\n </colgroup>\n <tbody>\n <tr>\n <td [attr.colspan]=\"columns.length\" width=\"100%\">\n <div\n *ngIf=\"enablePagination\"\n class=\"d-flex align-items-center justify-content-between mt-2 grid-footer\"\n [class.sticky-footer]=\"isFooterSticky\"\n >\n <!-- left: count -->\n <div class=\"small text-muted\" aria-live=\"polite\">\n {{ startIndex }} - {{ endIndex }} of {{ sorted.length }}\n </div>\n\n <!-- center: pager -->\n <div class=\"flex-grow-1 d-flex justify-content-center\">\n <ngb-pagination\n [page]=\"page\"\n [pageSize]=\"pageSize\"\n [collectionSize]=\"sorted.length\"\n [maxSize]=\"5\"\n (pageChange)=\"onPage($event)\"\n >\n </ngb-pagination>\n </div>\n\n <!-- right: rows per page -->\n <div class=\"d-flex align-items-center gap-2\">\n <label class=\"form-label form-label-sm mb-0\" for=\"pageSize\"\n >Rows:</label\n >\n <select\n id=\"pageSize\"\n class=\"form-select form-select-sm\"\n [(ngModel)]=\"pageSize\"\n (ngModelChange)=\"onPageSize($event)\"\n aria-label=\"Rows per page\"\n >\n <option *ngFor=\"let s of pageSizeOptions\" [value]=\"s\">\n {{ s }}\n </option>\n </select>\n </div>\n </div>\n </td>\n </tr>\n </tbody>\n </table>\n </div>\n</div>\n\n<!-- Footer (left = count, center = pagination, right = page size) -->\n", styles: ["@charset \"UTF-8\";:host{display:block}.table{margin-bottom:.5rem}th.sortable{cursor:pointer;-webkit-user-select:none;user-select:none}.sort-indicator{margin-left:.35rem;font-size:.75rem}.expand{cursor:pointer;font-size:24px;line-height:18px;display:block}.sticky-toggle{display:inline-flex;align-items:center;justify-content:center;gap:.15rem}.table-wrapper{position:relative}.table-wrapper{overflow-x:auto;border:1px solid #dee2e6;border-radius:12px;background:#fff}.table-body-scroll{max-height:320px;overflow-y:auto;overflow-x:hidden;border-top:1px solid #e9ecef;border-bottom:1px solid #e9ecef;scrollbar-gutter:stable both-edges;display:inline-block;width:fit-content;vertical-align:top}.grid-head{border-bottom:0}.grid-table{width:100%;min-width:1200px;table-layout:fixed;margin:0;border-collapse:separate;border-spacing:0}.grid-head{position:relative;z-index:5}.grid-head thead tr:first-child th:first-child{border-top-left-radius:12px}.grid-head thead th{background:var(--bs-table-bg, #f8f9fa);border-bottom:1px solid #dee2e6;font-weight:600}.grid-table th,.grid-table td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;border-bottom:1px solid #e9ecef;padding:.55rem .65rem}.filter-row td{background:#fff;border-bottom:1px solid #dee2e6;padding-top:.45rem;padding-bottom:.45rem}.sticky-row{position:sticky;background:#ccc;z-index:4}.sticky-row td{background:#ccc}.row-selected{background-color:#e7f1ff}.row-highlight{background-color:var(--bs-warning-bg-subtle, #fff3cd)}.row-highlight td{background-color:inherit}.cell-highlight{background-color:var(--bs-info-bg-subtle, #cff4fc)}.ngb-grid[data-theme=material]{--ngb-primary: #3f51b5}.ngb-grid[data-theme=material] .btn{border-color:var(--ngb-primary)}.ngb-grid[data-theme=material] table.table th,.ngb-grid[data-theme=material] table.table td{border-bottom:1px solid #e5e7eb}.ngb-grid[data-theme=tailwind]{--ngb-primary: rgb(29 78 216)}.ngb-grid[data-theme=tailwind] .btn{border-color:var(--ngb-primary);border-radius:.75rem}.ngb-grid[data-theme=tailwind] table.table th,.ngb-grid[data-theme=tailwind] table.table td{border-bottom:1px solid #e5e7eb;padding:.5rem}.ngb-responsive .table{width:100%}@media(max-width:768px){.ngb-responsive thead{display:none}.ngb-responsive tbody tr{display:grid;grid-template-columns:1fr;gap:.5rem;border:1px solid #e5e7eb;border-radius:.75rem;padding:.75rem;margin-bottom:.75rem}.ngb-responsive tbody td{display:grid;grid-template-columns:8rem 1fr}.ngb-responsive tbody td:before{content:attr(data-title);font-weight:600;opacity:.75;padding-right:.5rem}}\n"] }]
1489
1547
  }], propDecorators: { columns: [{
1490
1548
  type: Input
1491
1549
  }], data: [{