@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,
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 = (
|
|
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.
|
|
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
|
-
|
|
2691
|
-
|
|
2692
|
-
const
|
|
2693
|
-
|
|
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
|