@angular-bootstrap/ngbootstrap 0.0.8 → 0.0.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Input, Directive, EventEmitter, Output, ChangeDetectionStrategy, Component, inject, Injectable, TemplateRef, QueryList, ContentChildren, ContentChild, InjectionToken, signal, Optional, Inject, ElementRef, HostListener, HostBinding, ViewChild, forwardRef, ViewChildren } from '@angular/core';
2
+ import { Input, Directive, EventEmitter, Output, ChangeDetectionStrategy, Component, inject, Injectable, signal, TemplateRef, QueryList, ContentChildren, ContentChild, InjectionToken, Optional, Inject, ElementRef, HostListener, HostBinding, ViewChild, forwardRef, ViewChildren } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
4
  import { CommonModule } from '@angular/common';
5
5
  import * as i2 from '@angular/forms';
@@ -198,6 +198,105 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
198
198
  args: [{ providedIn: 'root' }]
199
199
  }] });
200
200
 
201
+ const clone = (value) => {
202
+ try {
203
+ // Prefer structuredClone when available.
204
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-return
205
+ return globalThis.structuredClone ? globalThis.structuredClone(value) : JSON.parse(JSON.stringify(value));
206
+ }
207
+ catch {
208
+ return value;
209
+ }
210
+ };
211
+ const shallowEqual = (a, b) => {
212
+ if (a === b)
213
+ return true;
214
+ if (!a || !b)
215
+ return false;
216
+ const aKeys = Object.keys(a);
217
+ const bKeys = Object.keys(b);
218
+ if (aKeys.length !== bKeys.length)
219
+ return false;
220
+ for (const key of aKeys) {
221
+ if (a[key] !== b[key])
222
+ return false;
223
+ }
224
+ return true;
225
+ };
226
+ class NgbDatagridDefaultEditService {
227
+ originals = signal([], ...(ngDevMode ? [{ debugName: "originals" }] : []));
228
+ newRows = signal([], ...(ngDevMode ? [{ debugName: "newRows" }] : []));
229
+ create(data, newRow, _rowIndex, rowId) {
230
+ this.newRows.update((ids) => (ids.some((id) => Object.is(id, rowId)) ? ids : [...ids, rowId]));
231
+ // Baseline for a new row is itself until saved/cancelled.
232
+ this.originals.update((items) => {
233
+ if (items.some((it) => Object.is(it.id, rowId)))
234
+ return items;
235
+ return [...items, { id: rowId, row: clone(newRow) }];
236
+ });
237
+ return [...data, newRow];
238
+ }
239
+ update(data, updatedRow, rowIndex, rowId) {
240
+ // Snapshot original the first time we see this rowId.
241
+ const hasBaseline = this.originals().some((it) => Object.is(it.id, rowId));
242
+ if (!hasBaseline) {
243
+ const current = data[rowIndex];
244
+ if (current != null) {
245
+ this.originals.update((items) => [...items, { id: rowId, row: clone(current) }]);
246
+ }
247
+ }
248
+ return this.replaceRow(data, updatedRow, rowIndex);
249
+ }
250
+ remove(data, rowIndex, rowId) {
251
+ this.newRows.update((ids) => ids.filter((id) => !Object.is(id, rowId)));
252
+ this.originals.update((items) => items.filter((it) => !Object.is(it.id, rowId)));
253
+ return this.removeRow(data, rowIndex);
254
+ }
255
+ assignValues(row, values) {
256
+ return { ...row, ...values };
257
+ }
258
+ isNew(rowId) {
259
+ return this.newRows().some((id) => Object.is(id, rowId));
260
+ }
261
+ hasChanges(rowId, currentRow) {
262
+ const baseline = this.originals().find((it) => Object.is(it.id, rowId))?.row;
263
+ if (!baseline)
264
+ return false;
265
+ return !shallowEqual(baseline, currentRow);
266
+ }
267
+ saveChanges(data, _rowIndex, rowId, currentRow) {
268
+ this.newRows.update((ids) => ids.filter((id) => !Object.is(id, rowId)));
269
+ // Once saved, clear baseline tracking (no longer "dirty").
270
+ this.originals.update((items) => items.filter((it) => !Object.is(it.id, rowId)));
271
+ // Data is already updated by `create` or `update` at this point.
272
+ return data.slice();
273
+ }
274
+ cancelChanges(data, rowIndex, rowId) {
275
+ if (this.newRows().some((id) => Object.is(id, rowId))) {
276
+ this.newRows.update((ids) => ids.filter((id) => !Object.is(id, rowId)));
277
+ this.originals.update((items) => items.filter((it) => !Object.is(it.id, rowId)));
278
+ return this.removeRow(data, rowIndex);
279
+ }
280
+ const baseline = this.originals().find((it) => Object.is(it.id, rowId))?.row;
281
+ this.originals.update((items) => items.filter((it) => !Object.is(it.id, rowId)));
282
+ if (!baseline)
283
+ return data.slice();
284
+ return this.replaceRow(data, baseline, rowIndex);
285
+ }
286
+ replaceRow(data, updatedRow, rowIndex) {
287
+ if (rowIndex < 0 || rowIndex >= data.length)
288
+ return data.slice();
289
+ const copy = data.slice();
290
+ copy[rowIndex] = updatedRow;
291
+ return copy;
292
+ }
293
+ removeRow(data, rowIndex) {
294
+ if (rowIndex < 0 || rowIndex >= data.length)
295
+ return data.slice();
296
+ return data.filter((_, i) => i !== rowIndex);
297
+ }
298
+ }
299
+
201
300
  class JsPdfAdapter {
202
301
  async export({ fileName, columns, rows, options }) {
203
302
  const { jsPDF } = await import('jspdf');
@@ -232,7 +331,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
232
331
  args: [{ selector: '[ngbExportButton]' }]
233
332
  }] });
234
333
 
235
- const EMAIL_RX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
334
+ const MAX_EMAIL_LENGTH = 254;
335
+ const isReasonableEmail = (value) => {
336
+ if (value == null)
337
+ return false;
338
+ const str = String(value);
339
+ if (!str || str.length > MAX_EMAIL_LENGTH)
340
+ return false;
341
+ // Basic, linear-time check: one "@", non-empty local part, and a domain with at least one ".".
342
+ const at = str.indexOf('@');
343
+ if (at <= 0)
344
+ return false;
345
+ if (str.indexOf('@', at + 1) !== -1)
346
+ return false;
347
+ if (at === str.length - 1)
348
+ return false;
349
+ if (/\s/.test(str))
350
+ return false;
351
+ const dot = str.lastIndexOf('.');
352
+ if (dot <= at + 1)
353
+ return false;
354
+ if (dot === str.length - 1)
355
+ return false;
356
+ return true;
357
+ };
236
358
  class Datagrid {
237
359
  /** Column definitions to render */
238
360
  columns = [];
@@ -273,6 +395,8 @@ class Datagrid {
273
395
  };
274
396
  theme = 'bootstrap';
275
397
  responsive = false;
398
+ trackBy;
399
+ editService;
276
400
  // Data hooks for export
277
401
  dataProviderAll; // used when pages='all'
278
402
  dataProviderSelection; // used when pages='selection'
@@ -310,31 +434,14 @@ class Datagrid {
310
434
  saveAttemptedEdit = false;
311
435
  addForm = this.fb.group({});
312
436
  saveAttemptedNew = false;
437
+ addDraftRowId = null;
438
+ defaultEditService = new NgbDatagridDefaultEditService();
313
439
  norm(v) {
314
440
  return (v ?? '').toString().toLowerCase().trim();
315
441
  }
316
442
  keyOf(col) {
317
443
  return col.field;
318
444
  }
319
- passesRowFilters(row) {
320
- // per-column
321
- for (const col of this.columns) {
322
- if (!col.filterable)
323
- continue;
324
- const key = col.field;
325
- const q = this.norm(this.filters[key]);
326
- if (!q)
327
- continue;
328
- const cell = this.norm(row[col.field]);
329
- if (!cell.includes(q))
330
- return false;
331
- }
332
- // global
333
- const g = this.norm(this.globalFilter);
334
- if (!g)
335
- return true;
336
- return this.columns.some(c => this.norm(row[c.field]).includes(g));
337
- }
338
445
  getDefaults() {
339
446
  return typeof this.newRowDefaults === 'function'
340
447
  ? this.newRowDefaults() ?? {}
@@ -388,16 +495,6 @@ class Datagrid {
388
495
  }
389
496
  return this.fb.group(group);
390
497
  }
391
- rowIsValidAgainst(targetErrors) {
392
- for (const col of this.columns) {
393
- if (col.editable === false)
394
- continue;
395
- const key = this.keyOf(col);
396
- if (targetErrors[key])
397
- return false;
398
- }
399
- return true;
400
- }
401
498
  strictEmailValidator() {
402
499
  // simple, pragmatic: local@domain.tld (tld >= 2)
403
500
  const rx = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
@@ -405,6 +502,8 @@ class Datagrid {
405
502
  const v = c.value;
406
503
  if (v === null || v === undefined || v === '')
407
504
  return null; // let "required" handle empties
505
+ if (`${v}`.length > MAX_EMAIL_LENGTH)
506
+ return { email: true };
408
507
  return rx.test(String(v)) ? null : { email: true };
409
508
  };
410
509
  }
@@ -438,18 +537,6 @@ class Datagrid {
438
537
  return true;
439
538
  });
440
539
  }
441
- compare(a, b) {
442
- if (a == null && b == null)
443
- return 0;
444
- if (a == null)
445
- return -1;
446
- if (b == null)
447
- return 1;
448
- if (typeof a === 'string' && typeof b === 'string') {
449
- return a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' });
450
- }
451
- return a < b ? -1 : a > b ? 1 : 0;
452
- }
453
540
  get sorted() {
454
541
  if (!this.enableSorting || !this.sort.active || !this.sort.direction)
455
542
  return this.filtered;
@@ -611,6 +698,12 @@ class Datagrid {
611
698
  if (ch['columns'])
612
699
  this.rebuildFilterForm();
613
700
  }
701
+ getRowId(rowIndex, row) {
702
+ return this.trackBy ? this.trackBy(rowIndex, row) : rowIndex;
703
+ }
704
+ getEditService() {
705
+ return this.editService ?? this.defaultEditService;
706
+ }
614
707
  startAdd() {
615
708
  if (!this.enableAdd || this.addingNew)
616
709
  return;
@@ -619,6 +712,11 @@ class Datagrid {
619
712
  this.addingNew = true;
620
713
  this.addForm = this.buildFormFromRow(); // defaults
621
714
  this.saveAttemptedNew = false;
715
+ // Register a draft row with the edit service so implementations can track "new" state.
716
+ const service = this.getEditService();
717
+ const draft = service.assignValues({}, this.addForm.value);
718
+ this.addDraftRowId = Symbol('ngb-datagrid-new-row');
719
+ service.create(this.data ?? [], draft, this.data.length, this.addDraftRowId);
622
720
  }
623
721
  saveAdd() {
624
722
  if (!this.addingNew || !this.addForm)
@@ -628,18 +726,28 @@ class Datagrid {
628
726
  this.addForm.updateValueAndValidity();
629
727
  if (this.addForm.invalid)
630
728
  return;
631
- const newRow = { ...this.addForm.value };
729
+ const service = this.getEditService();
730
+ const newRow = service.assignValues({}, this.addForm.value);
731
+ const rowIndex = this.data.length;
732
+ const rowId = this.addDraftRowId ?? this.getRowId(rowIndex, newRow);
733
+ service.create(this.data ?? [], newRow, rowIndex, rowId);
734
+ service.saveChanges(this.data ?? [], rowIndex, rowId, newRow);
632
735
  this.rowAdd.emit({ newRow });
633
736
  this.addingNew = false;
634
737
  this.addForm = this.fb.group({});
635
738
  ;
636
739
  this.saveAttemptedNew = false;
740
+ this.addDraftRowId = null;
637
741
  }
638
742
  cancelAdd() {
743
+ if (this.addDraftRowId != null) {
744
+ this.getEditService().cancelChanges(this.data ?? [], this.data.length, this.addDraftRowId);
745
+ }
639
746
  this.addingNew = false;
640
747
  this.addForm = this.fb.group({});
641
748
  ;
642
749
  this.saveAttemptedNew = false;
750
+ this.addDraftRowId = null;
643
751
  }
644
752
  startEdit(i) {
645
753
  if (!this.enableEdit)
@@ -650,6 +758,9 @@ class Datagrid {
650
758
  this.editForm = this.buildFormFromRow(this.paged[i]);
651
759
  this.saveAttemptedEdit = false;
652
760
  const di = this.data.indexOf(row);
761
+ const rowId = this.getRowId(di, this.data[di]);
762
+ // Start tracking baseline for the row (service can snapshot original state).
763
+ this.getEditService().update(this.data ?? [], this.data[di], di, rowId);
653
764
  this.rowEdit.emit({ row: this.data[di], index: di });
654
765
  }
655
766
  saveEdit(i) {
@@ -662,7 +773,11 @@ class Datagrid {
662
773
  return;
663
774
  const di = this.data.indexOf(this.paged[i]);
664
775
  const original = this.data[di];
665
- const updated = { ...original, ...this.editForm.value };
776
+ const rowId = this.getRowId(di, original);
777
+ const service = this.getEditService();
778
+ const updated = service.assignValues(original, this.editForm.value);
779
+ service.update(this.data ?? [], updated, di, rowId);
780
+ service.saveChanges(this.data ?? [], di, rowId, updated);
666
781
  this.rowSave.emit({ original, updated, index: di });
667
782
  this.editingIndex = null;
668
783
  this.editForm = this.fb.group({});
@@ -670,6 +785,8 @@ class Datagrid {
670
785
  }
671
786
  cancelEdit(i) {
672
787
  const di = this.data.indexOf(this.paged[i]);
788
+ const rowId = this.getRowId(di, this.data[di]);
789
+ this.getEditService().cancelChanges(this.data ?? [], di, rowId);
673
790
  this.rowCancel.emit({ row: this.data[di], index: di });
674
791
  this.editingIndex = null;
675
792
  this.editForm = this.fb.group({}); // empty group
@@ -677,6 +794,10 @@ class Datagrid {
677
794
  }
678
795
  // For the "Add row" draft
679
796
  onNewDraftChange(col) {
797
+ if (this.draftNew) {
798
+ const key = this.keyOf(col);
799
+ this.draftNew = this.getEditService().assignValues(this.draftNew, { [key]: this.draftNew[key] });
800
+ }
680
801
  this.validateInto(col, this.draftNew, this.errorsNew);
681
802
  }
682
803
  validateInto(col, targetDraft, targetErrors) {
@@ -692,8 +813,7 @@ class Datagrid {
692
813
  err = 'Required';
693
814
  }
694
815
  // type checks
695
- const EMAIL_RX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
696
- if (!err && col.type === 'email' && val && !EMAIL_RX.test(String(val)))
816
+ if (!err && col.type === 'email' && val && !isReasonableEmail(val))
697
817
  err = 'Invalid email';
698
818
  if (!err && col.type === 'number' && val !== '' && val != null && Number.isNaN(Number(val)))
699
819
  err = 'Invalid number';
@@ -710,10 +830,17 @@ class Datagrid {
710
830
  if (!this.enableDelete)
711
831
  return;
712
832
  const di = this.dataIndexFromPaged(i);
713
- this.rowDelete.emit({ row: this.data[di], index: di });
833
+ const row = this.data[di];
834
+ const rowId = this.getRowId(di, row);
835
+ this.getEditService().remove(this.data ?? [], di, rowId);
836
+ this.rowDelete.emit({ row, index: di });
714
837
  }
715
838
  // (optional) better *ngFor performance
716
- trackRow = (_, row) => row;
839
+ trackRow = (index, row) => {
840
+ const di = this.data.indexOf(row);
841
+ const rowIndex = di >= 0 ? di : index;
842
+ return this.trackBy ? this.trackBy(rowIndex, row) : rowIndex;
843
+ };
717
844
  toggleSort(field) {
718
845
  if (!this.enableSorting)
719
846
  return;
@@ -794,7 +921,7 @@ class Datagrid {
794
921
  return r === true || (!!r && r.enabled === true);
795
922
  }
796
923
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: Datagrid, deps: [], target: i0.ɵɵFactoryTarget.Component });
797
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", 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", globalFilterAriaLabel: "globalFilterAriaLabel", expandRowAriaLabel: "expandRowAriaLabel", collapseRowAriaLabel: "collapseRowAriaLabel", exportPdfAriaLabel: "exportPdfAriaLabel", exportExcelAriaLabel: "exportExcelAriaLabel", newRowDefaults: "newRowDefaults", strictEmail: "strictEmail", editOnRowClick: "editOnRowClick", singleExpand: "singleExpand", exportOptions: "exportOptions", theme: "theme", responsive: "responsive", dataProviderAll: "dataProviderAll", dataProviderSelection: "dataProviderSelection", pageSize: "pageSize" }, outputs: { rowAdd: "rowAdd", rowEdit: "rowEdit", rowSave: "rowSave", rowCancel: "rowCancel", rowDelete: "rowDelete", sortChange: "sortChange", filtersChange: "filtersChange", pageChange: "pageChange" }, providers: [
924
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", 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", 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" }, providers: [
798
925
  { provide: PdfExportAdapter, useClass: JsPdfAdapter },
799
926
  { provide: ExcelExportAdapter, useClass: XlsxAdapter }
800
927
  ], 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 }], 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 <table class=\"table table-bordered\"\n role=\"grid\"\n [attr.aria-readonly]=\"(enableEdit || enableAdd) ? 'false' : 'true'\">\n <thead class=\"thead-light\">\n <tr>\n <th *ngIf=\"rowDetailTpl\" style=\"width:1%\" scope=\"col\" aria-hidden=\"true\"></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 *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\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=\"rowDetailTpl\"></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)=\"onRowClick($event, i)\" [formGroup]=\"editForm\">\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 <!-- DATA CELLS -->\n <td *ngFor=\"let col of columns\" [attr.data-title]=\"col.header\">\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\n <!-- Footer (left = count, center = pagination, right = page size) -->\n <div *ngIf=\"enablePagination\" class=\"d-flex align-items-center justify-content-between mt-2\">\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</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}.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.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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
@@ -853,6 +980,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
853
980
  type: Input
854
981
  }], responsive: [{
855
982
  type: Input
983
+ }], trackBy: [{
984
+ type: Input
985
+ }], editService: [{
986
+ type: Input
856
987
  }], dataProviderAll: [{
857
988
  type: Input
858
989
  }], dataProviderSelection: [{
@@ -2198,6 +2329,8 @@ class NgbTypeaheadComponent {
2198
2329
  updateOnTab = true;
2199
2330
  separator = ',';
2200
2331
  chips = false;
2332
+ vScroll = false;
2333
+ vItemSize = 40;
2201
2334
  // `TemplateRef` types can become non-assignable in monorepo setups with multiple Angular installations.
2202
2335
  // Using `any` keeps the API flexible while still supporting Angular templates at runtime.
2203
2336
  itemTemplate;
@@ -2241,6 +2374,7 @@ class NgbTypeaheadComponent {
2241
2374
  itemHeight = 40;
2242
2375
  beforePadding = 0;
2243
2376
  afterPadding = 0;
2377
+ viewportStartIndex = 0;
2244
2378
  debounceId;
2245
2379
  onControlChange = () => { };
2246
2380
  onControlTouched = () => { };
@@ -2250,6 +2384,11 @@ class NgbTypeaheadComponent {
2250
2384
  this.applyFilter('');
2251
2385
  }
2252
2386
  ngOnChanges(changes) {
2387
+ if (changes['vScroll'] || changes['vItemSize']) {
2388
+ const nextSize = Number(this.vItemSize);
2389
+ this.itemHeight = Number.isFinite(nextSize) && nextSize > 0 ? nextSize : 40;
2390
+ this.updateViewport(this.scroller?.nativeElement.scrollTop || 0);
2391
+ }
2253
2392
  if (changes['data'] && !changes['data'].firstChange) {
2254
2393
  this.applyFilter(this.query);
2255
2394
  }
@@ -2450,15 +2589,37 @@ class NgbTypeaheadComponent {
2450
2589
  this.focusInput();
2451
2590
  return;
2452
2591
  }
2592
+ if (event.key === 'Tab' && this.overlayVisible) {
2593
+ const active = this.visible[this.activeIndex];
2594
+ if (active) {
2595
+ this.selectItem(active);
2596
+ this.hideOverlay(event);
2597
+ }
2598
+ return;
2599
+ }
2453
2600
  if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
2454
2601
  event.preventDefault();
2455
2602
  this.moveActive(event.key === 'ArrowDown' ? 1 : -1);
2456
2603
  }
2604
+ else if (event.key === 'Home') {
2605
+ if (this.overlayVisible) {
2606
+ event.preventDefault();
2607
+ this.setActiveGlobalIndex(0);
2608
+ }
2609
+ }
2610
+ else if (event.key === 'End') {
2611
+ if (this.overlayVisible) {
2612
+ event.preventDefault();
2613
+ this.setActiveGlobalIndex(this.filtered.length - 1);
2614
+ }
2615
+ }
2457
2616
  else if (event.key === 'Enter') {
2458
2617
  event.preventDefault();
2459
2618
  const active = this.visible[this.activeIndex] || this.visible[0];
2460
- if (active)
2619
+ if (active) {
2461
2620
  this.selectItem(active);
2621
+ this.hideOverlay(event);
2622
+ }
2462
2623
  }
2463
2624
  else if (event.key === 'Escape') {
2464
2625
  if (this.overlayVisible) {
@@ -2649,6 +2810,14 @@ class NgbTypeaheadComponent {
2649
2810
  this.cdr.markForCheck();
2650
2811
  }
2651
2812
  updateViewport(scrollTop) {
2813
+ if (!this.vScroll) {
2814
+ this.beforePadding = 0;
2815
+ this.afterPadding = 0;
2816
+ this.visible = this.filtered;
2817
+ this.viewportStartIndex = 0;
2818
+ this.activeIndex = this.visible.length ? Math.min(this.activeIndex, this.visible.length - 1) : -1;
2819
+ return;
2820
+ }
2652
2821
  const total = this.filtered.length;
2653
2822
  const visibleCount = Math.ceil(this.viewportHeight / this.itemHeight) + 2;
2654
2823
  const start = Math.max(Math.floor(scrollTop / this.itemHeight), 0);
@@ -2656,6 +2825,7 @@ class NgbTypeaheadComponent {
2656
2825
  this.beforePadding = start * this.itemHeight;
2657
2826
  this.afterPadding = Math.max(total - end, 0) * this.itemHeight;
2658
2827
  this.visible = this.filtered.slice(start, end);
2828
+ this.viewportStartIndex = start;
2659
2829
  this.activeIndex = this.visible.length ? Math.min(this.activeIndex, this.visible.length - 1) : -1;
2660
2830
  }
2661
2831
  activatePreferredOption() {
@@ -2685,12 +2855,46 @@ class NgbTypeaheadComponent {
2685
2855
  if (!this.overlayVisible) {
2686
2856
  this.showOverlay();
2687
2857
  }
2688
- if (!this.visible.length)
2858
+ if (!this.filtered.length)
2859
+ return;
2860
+ const currentGlobal = this.activeGlobalIndex();
2861
+ const total = this.filtered.length;
2862
+ const nextGlobal = currentGlobal < 0 ? 0 : (currentGlobal + direction + total) % total;
2863
+ this.setActiveGlobalIndex(nextGlobal);
2864
+ }
2865
+ activeGlobalIndex() {
2866
+ if (this.activeIndex < 0)
2867
+ return -1;
2868
+ return this.viewportStartIndex + this.activeIndex;
2869
+ }
2870
+ setActiveGlobalIndex(globalIndex) {
2871
+ if (!this.filtered.length) {
2872
+ this.activeIndex = -1;
2873
+ return;
2874
+ }
2875
+ const clamped = Math.max(0, Math.min(globalIndex, this.filtered.length - 1));
2876
+ const scroller = this.scroller?.nativeElement;
2877
+ if (!scroller) {
2878
+ this.activeIndex = 0;
2689
2879
  return;
2690
- const nextIndex = this.activeIndex < 0 ? 0 : (this.activeIndex + direction + this.visible.length) % this.visible.length;
2691
- this.activeIndex = nextIndex;
2692
- const buttons = this.scroller?.nativeElement.querySelectorAll('button.dropdown-item');
2693
- buttons?.[this.activeIndex]?.focus();
2880
+ }
2881
+ const itemTop = clamped * this.itemHeight;
2882
+ const itemBottom = itemTop + this.itemHeight;
2883
+ const viewTop = scroller.scrollTop;
2884
+ const viewBottom = viewTop + (scroller.clientHeight || this.viewportHeight);
2885
+ let nextScrollTop = viewTop;
2886
+ if (itemTop < viewTop) {
2887
+ nextScrollTop = itemTop;
2888
+ }
2889
+ else if (itemBottom > viewBottom) {
2890
+ nextScrollTop = Math.max(0, itemBottom - (scroller.clientHeight || this.viewportHeight));
2891
+ }
2892
+ if (nextScrollTop !== viewTop) {
2893
+ scroller.scrollTop = nextScrollTop;
2894
+ }
2895
+ this.updateViewport(scroller.scrollTop);
2896
+ this.activeIndex = Math.max(0, Math.min(clamped - this.viewportStartIndex, this.visible.length - 1));
2897
+ this.cdr.markForCheck();
2694
2898
  }
2695
2899
  validateItem(item) {
2696
2900
  if (item.id == null) {
@@ -2810,7 +3014,7 @@ class NgbTypeaheadComponent {
2810
3014
  return { id: `${value}`, label: `${value}`, value };
2811
3015
  }
2812
3016
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbTypeaheadComponent, deps: [{ token: i0.ElementRef }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
2813
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: NgbTypeaheadComponent, isStandalone: true, selector: "ngb-typeahead", inputs: { data: "data", debounceTime: "debounceTime", characterTyped: "characterTyped", limit: "limit", selectExact: "selectExact", multiSelect: "multiSelect", matchSelection: "matchSelection", showDropdownButton: "showDropdownButton", showClearButton: "showClearButton", updateOnBlur: "updateOnBlur", updateOnTab: "updateOnTab", separator: "separator", chips: "chips", itemTemplate: "itemTemplate", i18n: "i18n" }, outputs: { completeMethod: "completeMethod", onSelect: "onSelect", onUnselect: "onUnselect", onAdd: "onAdd", onFocus: "onFocus", onBlur: "onBlur", onDropdownClick: "onDropdownClick", onClear: "onClear", onInputKeydown: "onInputKeydown", onKeyUp: "onKeyUp", onShow: "onShow", onHide: "onHide", onLazyLoad: "onLazyLoad", selectedItems: "selectedItems", selectionChange: "selectionChange", onChange: "onChange", onScrollEvent: "onScrollEvent" }, host: { listeners: { "document:mousedown": "onDocumentMouseDown($event)", "document:focusin": "onDocumentFocusIn($event)" } }, providers: [
3017
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: NgbTypeaheadComponent, isStandalone: true, selector: "ngb-typeahead", inputs: { data: "data", debounceTime: "debounceTime", characterTyped: "characterTyped", limit: "limit", selectExact: "selectExact", multiSelect: "multiSelect", matchSelection: "matchSelection", showDropdownButton: "showDropdownButton", showClearButton: "showClearButton", updateOnBlur: "updateOnBlur", updateOnTab: "updateOnTab", separator: "separator", chips: "chips", vScroll: "vScroll", vItemSize: "vItemSize", itemTemplate: "itemTemplate", i18n: "i18n" }, outputs: { completeMethod: "completeMethod", onSelect: "onSelect", onUnselect: "onUnselect", onAdd: "onAdd", onFocus: "onFocus", onBlur: "onBlur", onDropdownClick: "onDropdownClick", onClear: "onClear", onInputKeydown: "onInputKeydown", onKeyUp: "onKeyUp", onShow: "onShow", onHide: "onHide", onLazyLoad: "onLazyLoad", selectedItems: "selectedItems", selectionChange: "selectionChange", onChange: "onChange", onScrollEvent: "onScrollEvent" }, host: { listeners: { "document:mousedown": "onDocumentMouseDown($event)", "document:focusin": "onDocumentFocusIn($event)" } }, providers: [
2814
3018
  {
2815
3019
  provide: NG_VALUE_ACCESSOR,
2816
3020
  useExisting: forwardRef(() => NgbTypeaheadComponent),
@@ -2898,7 +3102,7 @@ class NgbTypeaheadComponent {
2898
3102
  role="listbox"
2899
3103
  (scroll)="onScroll($event)"
2900
3104
  >
2901
- <div [style.height.px]="beforePadding"></div>
3105
+ <div *ngIf="vScroll" [style.height.px]="beforePadding"></div>
2902
3106
 
2903
3107
  <button
2904
3108
  *ngFor="let item of visible; trackBy: trackById; let idx = index"
@@ -2940,7 +3144,7 @@ class NgbTypeaheadComponent {
2940
3144
  </div>
2941
3145
  </button>
2942
3146
 
2943
- <div [style.height.px]="afterPadding"></div>
3147
+ <div *ngIf="vScroll" [style.height.px]="afterPadding"></div>
2944
3148
 
2945
3149
  <div *ngIf="showNoResults" class="dropdown-item text-muted text-center">
2946
3150
  {{ i18n?.noResults || 'No results' }}
@@ -3046,7 +3250,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
3046
3250
  role="listbox"
3047
3251
  (scroll)="onScroll($event)"
3048
3252
  >
3049
- <div [style.height.px]="beforePadding"></div>
3253
+ <div *ngIf="vScroll" [style.height.px]="beforePadding"></div>
3050
3254
 
3051
3255
  <button
3052
3256
  *ngFor="let item of visible; trackBy: trackById; let idx = index"
@@ -3088,7 +3292,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
3088
3292
  </div>
3089
3293
  </button>
3090
3294
 
3091
- <div [style.height.px]="afterPadding"></div>
3295
+ <div *ngIf="vScroll" [style.height.px]="afterPadding"></div>
3092
3296
 
3093
3297
  <div *ngIf="showNoResults" class="dropdown-item text-muted text-center">
3094
3298
  {{ i18n?.noResults || 'No results' }}
@@ -3129,6 +3333,10 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
3129
3333
  type: Input
3130
3334
  }], chips: [{
3131
3335
  type: Input
3336
+ }], vScroll: [{
3337
+ type: Input
3338
+ }], vItemSize: [{
3339
+ type: Input
3132
3340
  }], itemTemplate: [{
3133
3341
  type: Input
3134
3342
  }], i18n: [{
@@ -3452,5 +3660,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImpor
3452
3660
  * Generated bundle index. Do not edit.
3453
3661
  */
3454
3662
 
3455
- export { DATAGRID_TEMPLATE_DIRECTIVES, DND_I18N, DND_LIVE_ANNOUNCE, Datagrid, ExcelExportAdapter, ExportButtonDirective, JsPdfAdapter, NgbCellTemplate, NgbChipsComponent, NgbDndItemDirective, NgbDndListDirective, NgbDndState, NgbEditorTemplate, NgbExportService, NgbFilterTemplate, NgbGlobalFilterTemplate, NgbLiveAnnouncer, NgbPaginationComponent, NgbRowDetailTemplate, NgbSplitterComponent, NgbSplitterPaneComponent, NgbStepLabelDirective, NgbStepperComponent, NgbTreeComponent, NgbTypeaheadComponent, PdfExportAdapter, XlsxAdapter, defaultDndI18n };
3663
+ export { DATAGRID_TEMPLATE_DIRECTIVES, DND_I18N, DND_LIVE_ANNOUNCE, Datagrid, ExcelExportAdapter, ExportButtonDirective, JsPdfAdapter, NgbCellTemplate, NgbChipsComponent, NgbDatagridDefaultEditService, NgbDndItemDirective, NgbDndListDirective, NgbDndState, NgbEditorTemplate, NgbExportService, NgbFilterTemplate, NgbGlobalFilterTemplate, NgbLiveAnnouncer, NgbPaginationComponent, NgbRowDetailTemplate, NgbSplitterComponent, NgbSplitterPaneComponent, NgbStepLabelDirective, NgbStepperComponent, NgbTreeComponent, NgbTypeaheadComponent, PdfExportAdapter, XlsxAdapter, defaultDndI18n };
3456
3664
  //# sourceMappingURL=angular-bootstrap-ngbootstrap.mjs.map