@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 {
|
|
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
|
-
|
|
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
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
|
521
|
-
|
|
522
|
-
|
|
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
|
|
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
|
|
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: [{
|