@angular-bootstrap/ngbootstrap 0.0.1
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.
|
@@ -0,0 +1,2624 @@
|
|
|
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, ViewChildren } from '@angular/core';
|
|
3
|
+
import * as i1 from '@angular/common';
|
|
4
|
+
import { CommonModule } from '@angular/common';
|
|
5
|
+
import * as i2 from '@angular/forms';
|
|
6
|
+
import { FormBuilder, FormControl, Validators, FormsModule, ReactiveFormsModule } from '@angular/forms';
|
|
7
|
+
import { firstValueFrom, isObservable } from 'rxjs';
|
|
8
|
+
|
|
9
|
+
class NgbCellTemplate {
|
|
10
|
+
template;
|
|
11
|
+
field;
|
|
12
|
+
constructor(template) {
|
|
13
|
+
this.template = template;
|
|
14
|
+
}
|
|
15
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbCellTemplate, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
16
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: NgbCellTemplate, isStandalone: true, selector: "ng-template[ngbCell]", inputs: { field: ["ngbCell", "field"] }, ngImport: i0 });
|
|
17
|
+
}
|
|
18
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbCellTemplate, decorators: [{
|
|
19
|
+
type: Directive,
|
|
20
|
+
args: [{ selector: 'ng-template[ngbCell]' }]
|
|
21
|
+
}], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { field: [{
|
|
22
|
+
type: Input,
|
|
23
|
+
args: ['ngbCell']
|
|
24
|
+
}] } });
|
|
25
|
+
class NgbEditorTemplate {
|
|
26
|
+
template;
|
|
27
|
+
field;
|
|
28
|
+
constructor(template) {
|
|
29
|
+
this.template = template;
|
|
30
|
+
}
|
|
31
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbEditorTemplate, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
32
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: NgbEditorTemplate, isStandalone: true, selector: "ng-template[ngbEditor]", inputs: { field: ["ngbEditor", "field"] }, ngImport: i0 });
|
|
33
|
+
}
|
|
34
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbEditorTemplate, decorators: [{
|
|
35
|
+
type: Directive,
|
|
36
|
+
args: [{ selector: 'ng-template[ngbEditor]' }]
|
|
37
|
+
}], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { field: [{
|
|
38
|
+
type: Input,
|
|
39
|
+
args: ['ngbEditor']
|
|
40
|
+
}] } });
|
|
41
|
+
class NgbFilterTemplate {
|
|
42
|
+
template;
|
|
43
|
+
field;
|
|
44
|
+
constructor(template) {
|
|
45
|
+
this.template = template;
|
|
46
|
+
}
|
|
47
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbFilterTemplate, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
48
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: NgbFilterTemplate, isStandalone: true, selector: "ng-template[ngbFilter]", inputs: { field: ["ngbFilter", "field"] }, ngImport: i0 });
|
|
49
|
+
}
|
|
50
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbFilterTemplate, decorators: [{
|
|
51
|
+
type: Directive,
|
|
52
|
+
args: [{ selector: 'ng-template[ngbFilter]' }]
|
|
53
|
+
}], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { field: [{
|
|
54
|
+
type: Input,
|
|
55
|
+
args: ['ngbFilter']
|
|
56
|
+
}] } });
|
|
57
|
+
class NgbGlobalFilterTemplate {
|
|
58
|
+
template;
|
|
59
|
+
constructor(template) {
|
|
60
|
+
this.template = template;
|
|
61
|
+
}
|
|
62
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbGlobalFilterTemplate, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
63
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: NgbGlobalFilterTemplate, isStandalone: true, selector: "ng-template[ngbGlobalFilter]", ngImport: i0 });
|
|
64
|
+
}
|
|
65
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbGlobalFilterTemplate, decorators: [{
|
|
66
|
+
type: Directive,
|
|
67
|
+
args: [{ selector: 'ng-template[ngbGlobalFilter]' }]
|
|
68
|
+
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
|
|
69
|
+
class NgbRowDetailTemplate {
|
|
70
|
+
template;
|
|
71
|
+
constructor(template) {
|
|
72
|
+
this.template = template;
|
|
73
|
+
}
|
|
74
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbRowDetailTemplate, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
75
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: NgbRowDetailTemplate, isStandalone: true, selector: "ng-template[ngbRowDetail]", ngImport: i0 });
|
|
76
|
+
}
|
|
77
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbRowDetailTemplate, decorators: [{
|
|
78
|
+
type: Directive,
|
|
79
|
+
args: [{ selector: 'ng-template[ngbRowDetail]' }]
|
|
80
|
+
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
|
|
81
|
+
/** Handy constant to import in consumer apps */
|
|
82
|
+
const DATAGRID_TEMPLATE_DIRECTIVES = [
|
|
83
|
+
NgbCellTemplate, NgbEditorTemplate, NgbFilterTemplate, NgbGlobalFilterTemplate, NgbRowDetailTemplate
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
class NgbPaginationComponent {
|
|
87
|
+
page = 1;
|
|
88
|
+
pageSize = 10;
|
|
89
|
+
collectionSize = 0;
|
|
90
|
+
maxSize = 5;
|
|
91
|
+
pageChange = new EventEmitter();
|
|
92
|
+
get totalPages() {
|
|
93
|
+
return Math.max(1, Math.ceil(this.collectionSize / Math.max(1, this.pageSize)));
|
|
94
|
+
}
|
|
95
|
+
get pages() {
|
|
96
|
+
const total = this.totalPages;
|
|
97
|
+
const max = Math.max(3, this.maxSize | 0);
|
|
98
|
+
if (total <= max)
|
|
99
|
+
return Array.from({ length: total }, (_, i) => i + 1);
|
|
100
|
+
const half = Math.floor(max / 2);
|
|
101
|
+
let start = Math.max(1, this.page - half);
|
|
102
|
+
let end = start + max - 1;
|
|
103
|
+
if (end > total) {
|
|
104
|
+
end = total;
|
|
105
|
+
start = end - max + 1;
|
|
106
|
+
}
|
|
107
|
+
const nums = Array.from({ length: end - start + 1 }, (_, i) => start + i);
|
|
108
|
+
const head = start > 1 ? [1, start > 2 ? '…' : 2] : [];
|
|
109
|
+
const tail = end < total ? [end < total - 1 ? '…' : total - 1, total] : [];
|
|
110
|
+
return [...head, ...nums, ...tail];
|
|
111
|
+
}
|
|
112
|
+
go(p) {
|
|
113
|
+
if (p < 1 || p > this.totalPages || p === this.page)
|
|
114
|
+
return;
|
|
115
|
+
this.pageChange.emit(p);
|
|
116
|
+
}
|
|
117
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbPaginationComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
118
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: NgbPaginationComponent, isStandalone: true, selector: "ngb-pagination", inputs: { page: "page", pageSize: "pageSize", collectionSize: "collectionSize", maxSize: "maxSize" }, outputs: { pageChange: "pageChange" }, ngImport: i0, template: `
|
|
119
|
+
<nav *ngIf="totalPages >= 1" aria-label="Pagination">
|
|
120
|
+
<ul class="pagination pagination-sm justify-content-center mb-0">
|
|
121
|
+
<li class="page-item" [class.disabled]="page === 1">
|
|
122
|
+
<button class="page-link" type="button" (click)="go(page - 1)" aria-label="Previous"> <span aria-hidden="true">«</span> </button>
|
|
123
|
+
</li>
|
|
124
|
+
|
|
125
|
+
<li class="page-item" *ngFor="let p of pages" [class.active]="p === page" [class.disabled]="p === '…'">
|
|
126
|
+
<span *ngIf="p === '…'" class="page-link">…</span>
|
|
127
|
+
<button *ngIf="p !== '…'" class="page-link" type="button" (click)="go(p)">{{ p }}</button>
|
|
128
|
+
</li>
|
|
129
|
+
|
|
130
|
+
<li class="page-item" [class.disabled]="page === totalPages">
|
|
131
|
+
<button class="page-link" type="button" (click)="go(page + 1)" aria-label="Next"> <span aria-hidden="true">»</span> </button>
|
|
132
|
+
</li>
|
|
133
|
+
</ul>
|
|
134
|
+
</nav>
|
|
135
|
+
`, isInline: true, 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
136
|
+
}
|
|
137
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbPaginationComponent, decorators: [{
|
|
138
|
+
type: Component,
|
|
139
|
+
args: [{
|
|
140
|
+
selector: 'ngb-pagination',
|
|
141
|
+
standalone: true,
|
|
142
|
+
imports: [CommonModule],
|
|
143
|
+
template: `
|
|
144
|
+
<nav *ngIf="totalPages >= 1" aria-label="Pagination">
|
|
145
|
+
<ul class="pagination pagination-sm justify-content-center mb-0">
|
|
146
|
+
<li class="page-item" [class.disabled]="page === 1">
|
|
147
|
+
<button class="page-link" type="button" (click)="go(page - 1)" aria-label="Previous"> <span aria-hidden="true">«</span> </button>
|
|
148
|
+
</li>
|
|
149
|
+
|
|
150
|
+
<li class="page-item" *ngFor="let p of pages" [class.active]="p === page" [class.disabled]="p === '…'">
|
|
151
|
+
<span *ngIf="p === '…'" class="page-link">…</span>
|
|
152
|
+
<button *ngIf="p !== '…'" class="page-link" type="button" (click)="go(p)">{{ p }}</button>
|
|
153
|
+
</li>
|
|
154
|
+
|
|
155
|
+
<li class="page-item" [class.disabled]="page === totalPages">
|
|
156
|
+
<button class="page-link" type="button" (click)="go(page + 1)" aria-label="Next"> <span aria-hidden="true">»</span> </button>
|
|
157
|
+
</li>
|
|
158
|
+
</ul>
|
|
159
|
+
</nav>
|
|
160
|
+
`,
|
|
161
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
162
|
+
}]
|
|
163
|
+
}], propDecorators: { page: [{
|
|
164
|
+
type: Input
|
|
165
|
+
}], pageSize: [{
|
|
166
|
+
type: Input
|
|
167
|
+
}], collectionSize: [{
|
|
168
|
+
type: Input
|
|
169
|
+
}], maxSize: [{
|
|
170
|
+
type: Input
|
|
171
|
+
}], pageChange: [{
|
|
172
|
+
type: Output
|
|
173
|
+
}] } });
|
|
174
|
+
|
|
175
|
+
class PdfExportAdapter {
|
|
176
|
+
}
|
|
177
|
+
class ExcelExportAdapter {
|
|
178
|
+
}
|
|
179
|
+
class NgbExportService {
|
|
180
|
+
// Optional injections via inject()
|
|
181
|
+
pdf = inject(PdfExportAdapter, { optional: true });
|
|
182
|
+
excel = inject(ExcelExportAdapter, { optional: true });
|
|
183
|
+
exportPdf(p) {
|
|
184
|
+
if (!this.pdf)
|
|
185
|
+
throw new Error('No PdfExportAdapter provided');
|
|
186
|
+
return this.pdf.export(p);
|
|
187
|
+
}
|
|
188
|
+
exportExcel(p) {
|
|
189
|
+
if (!this.excel)
|
|
190
|
+
throw new Error('No ExcelExportAdapter provided');
|
|
191
|
+
return this.excel.export(p);
|
|
192
|
+
}
|
|
193
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbExportService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
194
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbExportService, providedIn: 'root' });
|
|
195
|
+
}
|
|
196
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbExportService, decorators: [{
|
|
197
|
+
type: Injectable,
|
|
198
|
+
args: [{ providedIn: 'root' }]
|
|
199
|
+
}] });
|
|
200
|
+
|
|
201
|
+
class JsPdfAdapter {
|
|
202
|
+
async export({ fileName, columns, rows, options }) {
|
|
203
|
+
const { jsPDF } = await import('jspdf');
|
|
204
|
+
const autoTable = (await import('jspdf-autotable')).default;
|
|
205
|
+
const doc = new jsPDF({ orientation: options?.landscape ? 'landscape' : 'portrait' });
|
|
206
|
+
autoTable(doc, { head: [columns], body: rows.map(r => columns.map(k => r[k])), margin: options?.margins });
|
|
207
|
+
doc.save(`${fileName}.pdf`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
class XlsxAdapter {
|
|
212
|
+
async export({ fileName, sheetName, columns, rows }) {
|
|
213
|
+
const XLSX = await import('xlsx');
|
|
214
|
+
const header = [columns.map(c => c.title)];
|
|
215
|
+
const data = rows.map(r => columns.map(c => r[c.key]));
|
|
216
|
+
const ws = XLSX.utils.aoa_to_sheet([...header, ...data]);
|
|
217
|
+
const wb = XLSX.utils.book_new();
|
|
218
|
+
XLSX.utils.book_append_sheet(wb, ws, sheetName);
|
|
219
|
+
XLSX.writeFile(wb, `${fileName}.xlsx`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// eslint-disable-next-line @angular-eslint/directive-selector
|
|
224
|
+
class ExportButtonDirective {
|
|
225
|
+
// Expose the TemplateRef so the grid can render it with a context
|
|
226
|
+
templateRef = inject(TemplateRef);
|
|
227
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ExportButtonDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
228
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: ExportButtonDirective, isStandalone: true, selector: "[ngbExportButton]", ngImport: i0 });
|
|
229
|
+
}
|
|
230
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: ExportButtonDirective, decorators: [{
|
|
231
|
+
type: Directive,
|
|
232
|
+
args: [{ selector: '[ngbExportButton]' }]
|
|
233
|
+
}] });
|
|
234
|
+
|
|
235
|
+
const EMAIL_RX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
236
|
+
class Datagrid {
|
|
237
|
+
/** Column definitions to render */
|
|
238
|
+
columns = [];
|
|
239
|
+
/** Row data to display */
|
|
240
|
+
data = [];
|
|
241
|
+
enableSorting = false;
|
|
242
|
+
enableFiltering = false; // per-column + global
|
|
243
|
+
enableGlobalFilter = false; // global search bar
|
|
244
|
+
enablePagination = false;
|
|
245
|
+
enableEdit = false;
|
|
246
|
+
enableDelete = false;
|
|
247
|
+
pageSizeOptions = [5, 10, 25, 50];
|
|
248
|
+
enableAdd = false;
|
|
249
|
+
/** Accessible label (aria-label) applied to the add-row button. */
|
|
250
|
+
addButtonAriaLabel = 'Add row';
|
|
251
|
+
/** Visible text rendered inside the add-row button. */
|
|
252
|
+
addButtonText = '+ Add';
|
|
253
|
+
/** Accessible label for the global filter input. */
|
|
254
|
+
globalFilterAriaLabel = 'Search all columns';
|
|
255
|
+
/** Accessible label announced when expanding a row. */
|
|
256
|
+
expandRowAriaLabel = 'Expand row';
|
|
257
|
+
/** Accessible label announced when collapsing a row. */
|
|
258
|
+
collapseRowAriaLabel = 'Collapse row';
|
|
259
|
+
/** Accessible label for the PDF export button. */
|
|
260
|
+
exportPdfAriaLabel = 'Export to PDF';
|
|
261
|
+
/** Accessible label for the Excel export button. */
|
|
262
|
+
exportExcelAriaLabel = 'Export to Excel';
|
|
263
|
+
/** Optional defaults for new rows */
|
|
264
|
+
newRowDefaults = null;
|
|
265
|
+
strictEmail = false; // turn off if needed (e.g., intranet emails)
|
|
266
|
+
editOnRowClick = false;
|
|
267
|
+
singleExpand = false; // accordion mode: one row expanded at a time
|
|
268
|
+
exportOptions = {
|
|
269
|
+
enabled: false,
|
|
270
|
+
type: 'both',
|
|
271
|
+
pages: 'current',
|
|
272
|
+
fileName: 'export'
|
|
273
|
+
};
|
|
274
|
+
theme = 'bootstrap';
|
|
275
|
+
responsive = false;
|
|
276
|
+
// Data hooks for export
|
|
277
|
+
dataProviderAll; // used when pages='all'
|
|
278
|
+
dataProviderSelection; // used when pages='selection'
|
|
279
|
+
// Grab the directive and its TemplateRef
|
|
280
|
+
exportButtonDir;
|
|
281
|
+
exporting = false;
|
|
282
|
+
rowAdd = new EventEmitter();
|
|
283
|
+
// events
|
|
284
|
+
rowEdit = new EventEmitter();
|
|
285
|
+
rowSave = new EventEmitter();
|
|
286
|
+
rowCancel = new EventEmitter();
|
|
287
|
+
rowDelete = new EventEmitter();
|
|
288
|
+
sortChange = new EventEmitter();
|
|
289
|
+
filtersChange = new EventEmitter();
|
|
290
|
+
pageChange = new EventEmitter();
|
|
291
|
+
rowDetailTpl;
|
|
292
|
+
exporter = inject(NgbExportService); // instead of constructor(private exporter: NgbExportService) {}
|
|
293
|
+
expanded = new Set();
|
|
294
|
+
fb = inject(FormBuilder);
|
|
295
|
+
filterForm = this.fb.group({});
|
|
296
|
+
globalFilterCtrl = new FormControl('', { nonNullable: true });
|
|
297
|
+
addingNew = false;
|
|
298
|
+
draftNew = null;
|
|
299
|
+
errorsNew = {};
|
|
300
|
+
// --- sorting (from previous step)
|
|
301
|
+
sort = { active: null, direction: '' };
|
|
302
|
+
// --- filtering
|
|
303
|
+
globalFilter = '';
|
|
304
|
+
filters = {}; // per-column text
|
|
305
|
+
page = 1;
|
|
306
|
+
pageSize = 10;
|
|
307
|
+
/** Inline editing state */
|
|
308
|
+
editingIndex = null; // index in `paged` view
|
|
309
|
+
editForm = this.fb.group({});
|
|
310
|
+
saveAttemptedEdit = false;
|
|
311
|
+
addForm = this.fb.group({});
|
|
312
|
+
saveAttemptedNew = false;
|
|
313
|
+
norm(v) {
|
|
314
|
+
return (v ?? '').toString().toLowerCase().trim();
|
|
315
|
+
}
|
|
316
|
+
keyOf(col) {
|
|
317
|
+
return col.field;
|
|
318
|
+
}
|
|
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
|
+
getDefaults() {
|
|
339
|
+
return typeof this.newRowDefaults === 'function'
|
|
340
|
+
? this.newRowDefaults() ?? {}
|
|
341
|
+
: this.newRowDefaults ?? {};
|
|
342
|
+
}
|
|
343
|
+
defaultFor(col) {
|
|
344
|
+
const d = this.getDefaults();
|
|
345
|
+
const key = col.field;
|
|
346
|
+
if (key in d)
|
|
347
|
+
return d[key];
|
|
348
|
+
switch (col.type) {
|
|
349
|
+
case 'boolean': return false;
|
|
350
|
+
case 'number': return '';
|
|
351
|
+
case 'date': return '';
|
|
352
|
+
default: return '';
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
numberValidator() {
|
|
356
|
+
const rx = /^-?\d+(\.\d+)?$/;
|
|
357
|
+
return (c) => c.value === '' || c.value === null || c.value === undefined || rx.test(String(c.value))
|
|
358
|
+
? null
|
|
359
|
+
: { number: true };
|
|
360
|
+
}
|
|
361
|
+
dateValidator() {
|
|
362
|
+
return (c) => {
|
|
363
|
+
const v = c.value;
|
|
364
|
+
if (v === '' || v === null || v === undefined)
|
|
365
|
+
return null;
|
|
366
|
+
return isNaN(Date.parse(String(v))) ? { date: true } : null;
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
buildFormFromRow(row) {
|
|
370
|
+
const group = {};
|
|
371
|
+
for (const col of this.columns) {
|
|
372
|
+
if (col.editable === false)
|
|
373
|
+
continue; // read-only columns not in form
|
|
374
|
+
const key = col.field;
|
|
375
|
+
const initial = row && key in row ? row[key] : this.defaultFor(col);
|
|
376
|
+
const v = [];
|
|
377
|
+
if (col.required && col.type !== 'boolean')
|
|
378
|
+
v.push(Validators.required);
|
|
379
|
+
if (col.type === 'email') {
|
|
380
|
+
v.push(this.strictEmail ? this.strictEmailValidator() : Validators.email);
|
|
381
|
+
}
|
|
382
|
+
if (col.type === 'number')
|
|
383
|
+
v.push(this.numberValidator());
|
|
384
|
+
if (col.type === 'date')
|
|
385
|
+
v.push(this.dateValidator());
|
|
386
|
+
// NOTE: If you need "boolean must be true", add Validators.requiredTrue here.
|
|
387
|
+
group[key] = [initial, v];
|
|
388
|
+
}
|
|
389
|
+
return this.fb.group(group);
|
|
390
|
+
}
|
|
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
|
+
strictEmailValidator() {
|
|
402
|
+
// simple, pragmatic: local@domain.tld (tld >= 2)
|
|
403
|
+
const rx = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/;
|
|
404
|
+
return (c) => {
|
|
405
|
+
const v = c.value;
|
|
406
|
+
if (v === null || v === undefined || v === '')
|
|
407
|
+
return null; // let "required" handle empties
|
|
408
|
+
return rx.test(String(v)) ? null : { email: true };
|
|
409
|
+
};
|
|
410
|
+
}
|
|
411
|
+
//Getter for the ng-template
|
|
412
|
+
get exportButtonTpl() {
|
|
413
|
+
return this.exportButtonDir?.templateRef ?? null;
|
|
414
|
+
}
|
|
415
|
+
// pipeline respects toggles
|
|
416
|
+
get filtered() {
|
|
417
|
+
if (!this.enableFiltering)
|
|
418
|
+
return this.data ?? [];
|
|
419
|
+
const src = this.data ?? [];
|
|
420
|
+
return src.filter(r => {
|
|
421
|
+
// per-column
|
|
422
|
+
for (const col of this.columns) {
|
|
423
|
+
if (!col.filterable)
|
|
424
|
+
continue;
|
|
425
|
+
const q = (this.filters[col.field] ?? '').toString().toLowerCase().trim();
|
|
426
|
+
if (!q)
|
|
427
|
+
continue;
|
|
428
|
+
const cell = (r[col.field] ?? '').toString().toLowerCase().trim();
|
|
429
|
+
if (!cell.includes(q))
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
// global
|
|
433
|
+
if (this.enableGlobalFilter) {
|
|
434
|
+
const g = (this.globalFilter ?? '').toString().toLowerCase().trim();
|
|
435
|
+
if (g)
|
|
436
|
+
return this.columns.some(c => (r[c.field] ?? '').toString().toLowerCase().includes(g));
|
|
437
|
+
}
|
|
438
|
+
return true;
|
|
439
|
+
});
|
|
440
|
+
}
|
|
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
|
+
get sorted() {
|
|
454
|
+
if (!this.enableSorting || !this.sort.active || !this.sort.direction)
|
|
455
|
+
return this.filtered;
|
|
456
|
+
const copy = [...this.filtered];
|
|
457
|
+
const { active, direction } = this.sort;
|
|
458
|
+
copy.sort((a, b) => {
|
|
459
|
+
const av = a?.[active];
|
|
460
|
+
const bv = b?.[active];
|
|
461
|
+
const cmp = typeof av === 'string' && typeof bv === 'string'
|
|
462
|
+
? av.localeCompare(bv, undefined, { numeric: true, sensitivity: 'base' })
|
|
463
|
+
: av < bv ? -1 : av > bv ? 1 : 0;
|
|
464
|
+
return direction === 'asc' ? cmp : -cmp;
|
|
465
|
+
});
|
|
466
|
+
return copy;
|
|
467
|
+
}
|
|
468
|
+
// pagination (filled in next step)
|
|
469
|
+
get paged() {
|
|
470
|
+
if (!this.enablePagination)
|
|
471
|
+
return this.sorted;
|
|
472
|
+
const start = (this.page - 1) * this.pageSize;
|
|
473
|
+
return this.sorted.slice(start, start + this.pageSize);
|
|
474
|
+
}
|
|
475
|
+
// getters for template conditions / display
|
|
476
|
+
get anyFilterable() {
|
|
477
|
+
return this.enableFiltering && !!this.columns?.some(c => c.filterable);
|
|
478
|
+
}
|
|
479
|
+
get startIndex() { const total = this.sorted.length; return total ? (this.page - 1) * this.pageSize + 1 : 0; }
|
|
480
|
+
get endIndex() { return Math.min(this.page * this.pageSize, this.sorted.length); }
|
|
481
|
+
get detailColspan() {
|
|
482
|
+
const actionCols = (this.enableEdit || this.enableDelete) ? 1 : 0;
|
|
483
|
+
const caretCol = this.rowDetailTpl ? 1 : 0;
|
|
484
|
+
return this.columns.length + actionCols + caretCol;
|
|
485
|
+
}
|
|
486
|
+
headerText(col) {
|
|
487
|
+
return (col.header ?? col.field ?? '').toString();
|
|
488
|
+
}
|
|
489
|
+
columnFilterAriaLabel(col) {
|
|
490
|
+
return `${this.headerText(col)} filter`;
|
|
491
|
+
}
|
|
492
|
+
inputAriaLabel(col) {
|
|
493
|
+
return this.headerText(col);
|
|
494
|
+
}
|
|
495
|
+
ariaSortFor(field) {
|
|
496
|
+
if (!this.enableSorting || this.sort.active !== field || !this.sort.direction) {
|
|
497
|
+
return 'none';
|
|
498
|
+
}
|
|
499
|
+
return this.sort.direction === 'asc' ? 'ascending' : 'descending';
|
|
500
|
+
}
|
|
501
|
+
sortButtonAriaLabel(col) {
|
|
502
|
+
const header = this.headerText(col);
|
|
503
|
+
const state = this.sort.active === col.field
|
|
504
|
+
? (this.sort.direction === 'asc'
|
|
505
|
+
? 'ascending'
|
|
506
|
+
: this.sort.direction === 'desc'
|
|
507
|
+
? 'descending'
|
|
508
|
+
: 'unsorted')
|
|
509
|
+
: 'unsorted';
|
|
510
|
+
return `Sort by ${header}. Current sort ${state}.`;
|
|
511
|
+
}
|
|
512
|
+
exportAriaLabel(kind) {
|
|
513
|
+
return kind === 'pdf' ? this.exportPdfAriaLabel : this.exportExcelAriaLabel;
|
|
514
|
+
}
|
|
515
|
+
// Helpers to map from paged index → original data index
|
|
516
|
+
// helper used above
|
|
517
|
+
dataIndexFromPaged(i) {
|
|
518
|
+
const row = this.paged[i];
|
|
519
|
+
return this.data.indexOf(row);
|
|
520
|
+
}
|
|
521
|
+
rebuildFilterForm() {
|
|
522
|
+
const group = {};
|
|
523
|
+
for (const c of this.columns)
|
|
524
|
+
if (c.filterable)
|
|
525
|
+
group[c.field] = [''];
|
|
526
|
+
this.filterForm = this.fb.group(group);
|
|
527
|
+
this.filterForm.valueChanges.subscribe(v => {
|
|
528
|
+
this.page = 1;
|
|
529
|
+
this.filtersChange.emit({ global: this.globalFilterCtrl.value, columns: v });
|
|
530
|
+
});
|
|
531
|
+
this.globalFilterCtrl.valueChanges.subscribe(() => {
|
|
532
|
+
this.page = 1;
|
|
533
|
+
this.filtersChange.emit({ global: this.globalFilterCtrl.value, columns: this.filterForm.value });
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
cellTplQ;
|
|
537
|
+
editTplQ;
|
|
538
|
+
filterTplQ;
|
|
539
|
+
globalTplQ;
|
|
540
|
+
/** Internal lookup maps */
|
|
541
|
+
cellTpls = {};
|
|
542
|
+
editTpls = {};
|
|
543
|
+
filterTpls = {};
|
|
544
|
+
globalTpl = null;
|
|
545
|
+
toRecord(items) {
|
|
546
|
+
const arr = Array.isArray(items)
|
|
547
|
+
? items
|
|
548
|
+
: items instanceof QueryList
|
|
549
|
+
? items.toArray()
|
|
550
|
+
: [];
|
|
551
|
+
return arr.reduce((acc, t) => (acc[t.field] = t, acc), {});
|
|
552
|
+
}
|
|
553
|
+
async export(kind) {
|
|
554
|
+
if (!this.exportOptions?.enabled)
|
|
555
|
+
return;
|
|
556
|
+
this.exporting = true;
|
|
557
|
+
try {
|
|
558
|
+
const data = await this.resolveDataset();
|
|
559
|
+
const cols = this.columns.map(c => ({ key: c.field, title: c.header }));
|
|
560
|
+
if (kind === 'pdf') {
|
|
561
|
+
await this.exporter.exportPdf({
|
|
562
|
+
fileName: this.exportOptions.fileName || 'export',
|
|
563
|
+
columns: cols.map(c => c.key),
|
|
564
|
+
rows: data,
|
|
565
|
+
options: this.exportOptions.pdf
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
else {
|
|
569
|
+
await this.exporter.exportExcel({
|
|
570
|
+
fileName: this.exportOptions.fileName || 'export',
|
|
571
|
+
sheetName: this.exportOptions.excel?.sheetName || 'Sheet1',
|
|
572
|
+
columns: cols,
|
|
573
|
+
rows: data
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
finally {
|
|
578
|
+
this.exporting = false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async resolveDataset() {
|
|
582
|
+
const mode = this.exportOptions?.pages || 'current';
|
|
583
|
+
if (mode === 'all') {
|
|
584
|
+
if (!this.dataProviderAll)
|
|
585
|
+
throw new Error('dataProviderAll required when pages="all"');
|
|
586
|
+
const d = this.dataProviderAll();
|
|
587
|
+
return isObservable(d) ? firstValueFrom(d) : (d instanceof Promise ? d : d);
|
|
588
|
+
}
|
|
589
|
+
if (mode === 'selection') {
|
|
590
|
+
if (!this.dataProviderSelection)
|
|
591
|
+
throw new Error('dataProviderSelection required when pages="selection"');
|
|
592
|
+
return this.dataProviderSelection();
|
|
593
|
+
}
|
|
594
|
+
// current page == your existing `paged` array
|
|
595
|
+
return this.paged;
|
|
596
|
+
}
|
|
597
|
+
ngAfterContentInit() {
|
|
598
|
+
const rebuild = () => {
|
|
599
|
+
this.cellTpls = this.toRecord(this.cellTplQ);
|
|
600
|
+
this.editTpls = this.toRecord(this.editTplQ);
|
|
601
|
+
this.filterTpls = this.toRecord(this.filterTplQ);
|
|
602
|
+
this.globalTpl = this.globalTplQ?.first ?? null;
|
|
603
|
+
};
|
|
604
|
+
rebuild();
|
|
605
|
+
this.cellTplQ?.changes.subscribe(rebuild);
|
|
606
|
+
this.editTplQ?.changes.subscribe(rebuild);
|
|
607
|
+
this.filterTplQ?.changes.subscribe(rebuild);
|
|
608
|
+
this.globalTplQ?.changes.subscribe(rebuild);
|
|
609
|
+
}
|
|
610
|
+
ngOnChanges(ch) {
|
|
611
|
+
if (ch['columns'])
|
|
612
|
+
this.rebuildFilterForm();
|
|
613
|
+
}
|
|
614
|
+
startAdd() {
|
|
615
|
+
if (!this.enableAdd || this.addingNew)
|
|
616
|
+
return;
|
|
617
|
+
this.page = 1;
|
|
618
|
+
this.editingIndex = null;
|
|
619
|
+
this.addingNew = true;
|
|
620
|
+
this.addForm = this.buildFormFromRow(); // defaults
|
|
621
|
+
this.saveAttemptedNew = false;
|
|
622
|
+
}
|
|
623
|
+
saveAdd() {
|
|
624
|
+
if (!this.addingNew || !this.addForm)
|
|
625
|
+
return;
|
|
626
|
+
this.saveAttemptedNew = true;
|
|
627
|
+
this.addForm.markAllAsTouched();
|
|
628
|
+
this.addForm.updateValueAndValidity();
|
|
629
|
+
if (this.addForm.invalid)
|
|
630
|
+
return;
|
|
631
|
+
const newRow = { ...this.addForm.value };
|
|
632
|
+
this.rowAdd.emit({ newRow });
|
|
633
|
+
this.addingNew = false;
|
|
634
|
+
this.addForm = this.fb.group({});
|
|
635
|
+
;
|
|
636
|
+
this.saveAttemptedNew = false;
|
|
637
|
+
}
|
|
638
|
+
cancelAdd() {
|
|
639
|
+
this.addingNew = false;
|
|
640
|
+
this.addForm = this.fb.group({});
|
|
641
|
+
;
|
|
642
|
+
this.saveAttemptedNew = false;
|
|
643
|
+
}
|
|
644
|
+
startEdit(i) {
|
|
645
|
+
if (!this.enableEdit)
|
|
646
|
+
return;
|
|
647
|
+
this.addingNew = false;
|
|
648
|
+
this.editingIndex = i;
|
|
649
|
+
const row = this.paged[i];
|
|
650
|
+
this.editForm = this.buildFormFromRow(this.paged[i]);
|
|
651
|
+
this.saveAttemptedEdit = false;
|
|
652
|
+
const di = this.data.indexOf(row);
|
|
653
|
+
this.rowEdit.emit({ row: this.data[di], index: di });
|
|
654
|
+
}
|
|
655
|
+
saveEdit(i) {
|
|
656
|
+
if (this.editingIndex !== i || !this.editForm)
|
|
657
|
+
return;
|
|
658
|
+
this.saveAttemptedEdit = true;
|
|
659
|
+
this.editForm.markAllAsTouched();
|
|
660
|
+
this.editForm.updateValueAndValidity();
|
|
661
|
+
if (this.editForm.invalid)
|
|
662
|
+
return;
|
|
663
|
+
const di = this.data.indexOf(this.paged[i]);
|
|
664
|
+
const original = this.data[di];
|
|
665
|
+
const updated = { ...original, ...this.editForm.value };
|
|
666
|
+
this.rowSave.emit({ original, updated, index: di });
|
|
667
|
+
this.editingIndex = null;
|
|
668
|
+
this.editForm = this.fb.group({});
|
|
669
|
+
this.saveAttemptedEdit = false;
|
|
670
|
+
}
|
|
671
|
+
cancelEdit(i) {
|
|
672
|
+
const di = this.data.indexOf(this.paged[i]);
|
|
673
|
+
this.rowCancel.emit({ row: this.data[di], index: di });
|
|
674
|
+
this.editingIndex = null;
|
|
675
|
+
this.editForm = this.fb.group({}); // empty group
|
|
676
|
+
this.saveAttemptedEdit = false;
|
|
677
|
+
}
|
|
678
|
+
// For the "Add row" draft
|
|
679
|
+
onNewDraftChange(col) {
|
|
680
|
+
this.validateInto(col, this.draftNew, this.errorsNew);
|
|
681
|
+
}
|
|
682
|
+
validateInto(col, targetDraft, targetErrors) {
|
|
683
|
+
if (!targetDraft)
|
|
684
|
+
return;
|
|
685
|
+
const key = this.keyOf(col);
|
|
686
|
+
const val = targetDraft[key];
|
|
687
|
+
let err = '';
|
|
688
|
+
// required
|
|
689
|
+
if (col.required) {
|
|
690
|
+
const empty = val == null || (typeof val === 'string' && val.trim() === '');
|
|
691
|
+
if (empty)
|
|
692
|
+
err = 'Required';
|
|
693
|
+
}
|
|
694
|
+
// type checks
|
|
695
|
+
const EMAIL_RX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
696
|
+
if (!err && col.type === 'email' && val && !EMAIL_RX.test(String(val)))
|
|
697
|
+
err = 'Invalid email';
|
|
698
|
+
if (!err && col.type === 'number' && val !== '' && val != null && Number.isNaN(Number(val)))
|
|
699
|
+
err = 'Invalid number';
|
|
700
|
+
if (!err && col.type === 'date' && val && Number.isNaN(Date.parse(String(val))))
|
|
701
|
+
err = 'Invalid date';
|
|
702
|
+
if (!err && col.type === 'boolean' && col.required && val !== true && val !== false)
|
|
703
|
+
err = 'Required';
|
|
704
|
+
if (err)
|
|
705
|
+
targetErrors[key] = err;
|
|
706
|
+
else
|
|
707
|
+
delete targetErrors[key];
|
|
708
|
+
}
|
|
709
|
+
deleteRow(i) {
|
|
710
|
+
if (!this.enableDelete)
|
|
711
|
+
return;
|
|
712
|
+
const di = this.dataIndexFromPaged(i);
|
|
713
|
+
this.rowDelete.emit({ row: this.data[di], index: di });
|
|
714
|
+
}
|
|
715
|
+
// (optional) better *ngFor performance
|
|
716
|
+
trackRow = (_, row) => row;
|
|
717
|
+
toggleSort(field) {
|
|
718
|
+
if (!this.enableSorting)
|
|
719
|
+
return;
|
|
720
|
+
if (this.sort.active !== field)
|
|
721
|
+
this.sort = { active: field, direction: 'asc' };
|
|
722
|
+
else
|
|
723
|
+
this.sort = {
|
|
724
|
+
active: field,
|
|
725
|
+
direction: this.sort.direction === 'asc' ? 'desc' : this.sort.direction === 'desc' ? '' : 'asc'
|
|
726
|
+
};
|
|
727
|
+
this.page = 1;
|
|
728
|
+
this.sortChange.emit({ active: this.sort.active, direction: this.sort.direction });
|
|
729
|
+
}
|
|
730
|
+
onGlobalFilterChange() {
|
|
731
|
+
if (!this.enableFiltering || !this.enableGlobalFilter)
|
|
732
|
+
return;
|
|
733
|
+
this.page = 1;
|
|
734
|
+
this.filtersChange.emit({ global: this.globalFilter, columns: { ...this.filters } });
|
|
735
|
+
}
|
|
736
|
+
onColumnFilterChange() {
|
|
737
|
+
if (!this.enableFiltering)
|
|
738
|
+
return;
|
|
739
|
+
this.page = 1;
|
|
740
|
+
this.filtersChange.emit({ global: this.globalFilter, columns: { ...this.filters } });
|
|
741
|
+
}
|
|
742
|
+
onPageChange(p) {
|
|
743
|
+
this.page = p;
|
|
744
|
+
this.pageChange.emit({ page: this.page, pageSize: this.pageSize });
|
|
745
|
+
}
|
|
746
|
+
onPageSizeChange() {
|
|
747
|
+
this.page = 1;
|
|
748
|
+
this.pageChange.emit({ page: this.page, pageSize: this.pageSize });
|
|
749
|
+
}
|
|
750
|
+
toggleExpand(i) {
|
|
751
|
+
const isOpen = this.expanded.has(i);
|
|
752
|
+
if (this.singleExpand) {
|
|
753
|
+
this.expanded.clear();
|
|
754
|
+
if (!isOpen)
|
|
755
|
+
this.expanded.add(i); // open the clicked row, or close all if it was open
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
|
759
|
+
isOpen ? this.expanded.delete(i) : this.expanded.add(i);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
isExpanded(i) {
|
|
763
|
+
return this.expanded.has(i);
|
|
764
|
+
}
|
|
765
|
+
onRowClick(ev, i) {
|
|
766
|
+
if (!this.editOnRowClick)
|
|
767
|
+
return;
|
|
768
|
+
if (this.addingNew || this.editingIndex === i)
|
|
769
|
+
return;
|
|
770
|
+
const el = ev.target;
|
|
771
|
+
if (el.closest('button, a, input, select, textarea, label, .no-edit-trigger'))
|
|
772
|
+
return;
|
|
773
|
+
this.startEdit(i);
|
|
774
|
+
}
|
|
775
|
+
onPage(p) {
|
|
776
|
+
if (p < 1)
|
|
777
|
+
return;
|
|
778
|
+
this.page = p;
|
|
779
|
+
this.pageChange.emit({ page: this.page, pageSize: this.pageSize });
|
|
780
|
+
}
|
|
781
|
+
onPageSize(sz) {
|
|
782
|
+
this.pageSize = Number(sz) || this.pageSize;
|
|
783
|
+
this.page = 1; // reset to first page when size changes
|
|
784
|
+
this.pageChange.emit({ page: this.page, pageSize: this.pageSize });
|
|
785
|
+
}
|
|
786
|
+
asBool(v) {
|
|
787
|
+
return typeof v === 'boolean' ? v : !!v?.enabled;
|
|
788
|
+
}
|
|
789
|
+
triggerExport(kind) {
|
|
790
|
+
this.export(kind);
|
|
791
|
+
}
|
|
792
|
+
isResponsiveEnabled() {
|
|
793
|
+
const r = this.responsive;
|
|
794
|
+
return r === true || (!!r && r.enabled === true);
|
|
795
|
+
}
|
|
796
|
+
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: [
|
|
798
|
+
{ provide: PdfExportAdapter, useClass: JsPdfAdapter },
|
|
799
|
+
{ provide: ExcelExportAdapter, useClass: XlsxAdapter }
|
|
800
|
+
], 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 });
|
|
801
|
+
}
|
|
802
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: Datagrid, decorators: [{
|
|
803
|
+
type: Component,
|
|
804
|
+
args: [{ selector: 'ngb-datagrid', imports: [CommonModule, FormsModule, NgbPaginationComponent, ReactiveFormsModule], providers: [
|
|
805
|
+
{ provide: PdfExportAdapter, useClass: JsPdfAdapter },
|
|
806
|
+
{ provide: ExcelExportAdapter, useClass: XlsxAdapter }
|
|
807
|
+
], standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: "<!-- eslint-disable @angular-eslint/template/interactive-supports-focus -->\n<!-- eslint-disable @angular-eslint/template/click-events-have-key-events -->\n<div class=\"ngb-grid\" [attr.data-theme]=\"theme\" [class.ngb-responsive]=\"isResponsiveEnabled()\">\n <!-- Export toolbar -->\n <div class=\"d-flex justify-content-end mb-2\" *ngIf=\"exportOptions?.enabled\">\n <ng-container *ngIf=\"exportButtonTpl; else defaultExportBtns\"\n [ngTemplateOutlet]=\"exportButtonTpl\"\n [ngTemplateOutletContext]=\"{ $implicit: triggerExport }\">\n </ng-container>\n\n <ng-template #defaultExportBtns>\n <button type=\"button\"\n class=\"btn btn-sm btn-outline-secondary me-2\"\n *ngIf=\"exportOptions.type === 'pdf' || exportOptions.type === 'both'\"\n [disabled]=\"exporting\"\n (click)=\"export('pdf')\"\n [attr.aria-label]=\"exportAriaLabel('pdf')\">\n Export to PDF\n </button>\n <button type=\"button\"\n class=\"btn btn-sm btn-outline-secondary\"\n *ngIf=\"exportOptions.type === 'excel' || exportOptions.type === 'both'\"\n [disabled]=\"exporting\"\n (click)=\"export('excel')\"\n [attr.aria-label]=\"exportAriaLabel('excel')\">\n Export to Excel\n </button>\n </ng-template>\n </div>\n <div class=\"d-flex justify-content-end mb-2\" *ngIf=\"enableAdd\">\n <button\n type=\"button\"\n class=\"btn btn-sm btn-primary\"\n (click)=\"startAdd()\"\n [disabled]=\"addingNew\"\n [attr.aria-label]=\"addButtonAriaLabel || addButtonText\"\n >\n {{ addButtonText }}\n </button>\n </div>\n <!-- Global filter -->\n <div class=\"mb-2\" *ngIf=\"enableFiltering && enableGlobalFilter\">\n <ng-container\n *ngIf=\"globalTpl; else defaultGlobalFilter\"\n [ngTemplateOutlet]=\"globalTpl?.template\"\n [ngTemplateOutletContext]=\"{ $implicit: globalFilterCtrl }\"\n >\n </ng-container>\n\n <ng-template #defaultGlobalFilter>\n <input\n type=\"search\"\n class=\"form-control form-control-sm\"\n placeholder=\"Search all columns...\"\n [formControl]=\"globalFilterCtrl\"\n [attr.aria-label]=\"globalFilterAriaLabel\"\n />\n </ng-template>\n </div>\n\n <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"] }]
|
|
808
|
+
}], propDecorators: { columns: [{
|
|
809
|
+
type: Input
|
|
810
|
+
}], data: [{
|
|
811
|
+
type: Input
|
|
812
|
+
}], enableSorting: [{
|
|
813
|
+
type: Input
|
|
814
|
+
}], enableFiltering: [{
|
|
815
|
+
type: Input
|
|
816
|
+
}], enableGlobalFilter: [{
|
|
817
|
+
type: Input
|
|
818
|
+
}], enablePagination: [{
|
|
819
|
+
type: Input
|
|
820
|
+
}], enableEdit: [{
|
|
821
|
+
type: Input
|
|
822
|
+
}], enableDelete: [{
|
|
823
|
+
type: Input
|
|
824
|
+
}], pageSizeOptions: [{
|
|
825
|
+
type: Input
|
|
826
|
+
}], enableAdd: [{
|
|
827
|
+
type: Input
|
|
828
|
+
}], addButtonAriaLabel: [{
|
|
829
|
+
type: Input
|
|
830
|
+
}], addButtonText: [{
|
|
831
|
+
type: Input
|
|
832
|
+
}], globalFilterAriaLabel: [{
|
|
833
|
+
type: Input
|
|
834
|
+
}], expandRowAriaLabel: [{
|
|
835
|
+
type: Input
|
|
836
|
+
}], collapseRowAriaLabel: [{
|
|
837
|
+
type: Input
|
|
838
|
+
}], exportPdfAriaLabel: [{
|
|
839
|
+
type: Input
|
|
840
|
+
}], exportExcelAriaLabel: [{
|
|
841
|
+
type: Input
|
|
842
|
+
}], newRowDefaults: [{
|
|
843
|
+
type: Input
|
|
844
|
+
}], strictEmail: [{
|
|
845
|
+
type: Input
|
|
846
|
+
}], editOnRowClick: [{
|
|
847
|
+
type: Input
|
|
848
|
+
}], singleExpand: [{
|
|
849
|
+
type: Input
|
|
850
|
+
}], exportOptions: [{
|
|
851
|
+
type: Input
|
|
852
|
+
}], theme: [{
|
|
853
|
+
type: Input
|
|
854
|
+
}], responsive: [{
|
|
855
|
+
type: Input
|
|
856
|
+
}], dataProviderAll: [{
|
|
857
|
+
type: Input
|
|
858
|
+
}], dataProviderSelection: [{
|
|
859
|
+
type: Input
|
|
860
|
+
}], exportButtonDir: [{
|
|
861
|
+
type: ContentChild,
|
|
862
|
+
args: [ExportButtonDirective]
|
|
863
|
+
}], rowAdd: [{
|
|
864
|
+
type: Output
|
|
865
|
+
}], rowEdit: [{
|
|
866
|
+
type: Output
|
|
867
|
+
}], rowSave: [{
|
|
868
|
+
type: Output
|
|
869
|
+
}], rowCancel: [{
|
|
870
|
+
type: Output
|
|
871
|
+
}], rowDelete: [{
|
|
872
|
+
type: Output
|
|
873
|
+
}], sortChange: [{
|
|
874
|
+
type: Output
|
|
875
|
+
}], filtersChange: [{
|
|
876
|
+
type: Output
|
|
877
|
+
}], pageChange: [{
|
|
878
|
+
type: Output
|
|
879
|
+
}], rowDetailTpl: [{
|
|
880
|
+
type: ContentChild,
|
|
881
|
+
args: [NgbRowDetailTemplate]
|
|
882
|
+
}], pageSize: [{
|
|
883
|
+
type: Input
|
|
884
|
+
}], cellTplQ: [{
|
|
885
|
+
type: ContentChildren,
|
|
886
|
+
args: [NgbCellTemplate]
|
|
887
|
+
}], editTplQ: [{
|
|
888
|
+
type: ContentChildren,
|
|
889
|
+
args: [NgbEditorTemplate]
|
|
890
|
+
}], filterTplQ: [{
|
|
891
|
+
type: ContentChildren,
|
|
892
|
+
args: [NgbFilterTemplate]
|
|
893
|
+
}], globalTplQ: [{
|
|
894
|
+
type: ContentChildren,
|
|
895
|
+
args: [NgbGlobalFilterTemplate]
|
|
896
|
+
}] } });
|
|
897
|
+
|
|
898
|
+
class NgbLiveAnnouncer {
|
|
899
|
+
node = null;
|
|
900
|
+
announce(message) {
|
|
901
|
+
if (!this.node) {
|
|
902
|
+
this.node = document.createElement('div');
|
|
903
|
+
this.node.setAttribute('aria-live', 'polite');
|
|
904
|
+
this.node.setAttribute('aria-atomic', 'true');
|
|
905
|
+
this.node.style.position = 'fixed';
|
|
906
|
+
this.node.style.width = '1px';
|
|
907
|
+
this.node.style.height = '1px';
|
|
908
|
+
this.node.style.overflow = 'hidden';
|
|
909
|
+
this.node.style.clip = 'rect(1px,1px,1px,1px)';
|
|
910
|
+
this.node.style.whiteSpace = 'nowrap';
|
|
911
|
+
document.body.appendChild(this.node);
|
|
912
|
+
}
|
|
913
|
+
this.node.textContent = message;
|
|
914
|
+
}
|
|
915
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbLiveAnnouncer, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
916
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbLiveAnnouncer, providedIn: 'root' });
|
|
917
|
+
}
|
|
918
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbLiveAnnouncer, decorators: [{
|
|
919
|
+
type: Injectable,
|
|
920
|
+
args: [{ providedIn: 'root' }]
|
|
921
|
+
}] });
|
|
922
|
+
const DND_LIVE_ANNOUNCE = new InjectionToken('DND_LIVE_ANNOUNCE', { providedIn: 'root', factory: () => null });
|
|
923
|
+
|
|
924
|
+
const DND_I18N = new InjectionToken('DND_I18N');
|
|
925
|
+
const defaultDndI18n = {
|
|
926
|
+
listLabel: (n) => n ?? 'List',
|
|
927
|
+
pickedUp: () => 'Picked up. Use Arrow keys to move, Enter to drop, Escape to cancel.',
|
|
928
|
+
dropped: () => 'Dropped.',
|
|
929
|
+
moveToIndex: (i, t) => `Moved to position ${i} of ${t}.`,
|
|
930
|
+
dropHere: () => 'Drop here',
|
|
931
|
+
cannotDropHere: () => 'Cannot drop here'
|
|
932
|
+
};
|
|
933
|
+
|
|
934
|
+
// drag-state.service.ts
|
|
935
|
+
class NgbDndState {
|
|
936
|
+
active = signal(false, ...(ngDevMode ? [{ debugName: "active" }] : []));
|
|
937
|
+
store = new Map();
|
|
938
|
+
currentId = null; // ✅ track current session id
|
|
939
|
+
// Public so directives can read strings and announce
|
|
940
|
+
i18n = defaultDndI18n;
|
|
941
|
+
announce;
|
|
942
|
+
constructor(announceFn, i18n) {
|
|
943
|
+
if (i18n)
|
|
944
|
+
this.i18n = i18n;
|
|
945
|
+
if (announceFn)
|
|
946
|
+
this.announce = announceFn;
|
|
947
|
+
}
|
|
948
|
+
createSession(session) {
|
|
949
|
+
const id = 'ngb-dnd-' + (crypto?.randomUUID?.() ?? Math.random().toString(36).slice(2));
|
|
950
|
+
this.store.set(id, session);
|
|
951
|
+
this.currentId = id; // ✅ remember current
|
|
952
|
+
this.active.set(true);
|
|
953
|
+
return id;
|
|
954
|
+
}
|
|
955
|
+
get(id) {
|
|
956
|
+
if (!id)
|
|
957
|
+
return null;
|
|
958
|
+
return this.store.get(id) ?? null;
|
|
959
|
+
}
|
|
960
|
+
getCurrent() {
|
|
961
|
+
return this.currentId ? (this.store.get(this.currentId) ?? null) : null;
|
|
962
|
+
}
|
|
963
|
+
clear(id) {
|
|
964
|
+
if (id)
|
|
965
|
+
this.store.delete(id);
|
|
966
|
+
if (this.currentId === id)
|
|
967
|
+
this.currentId = null;
|
|
968
|
+
if (this.store.size === 0)
|
|
969
|
+
this.active.set(false);
|
|
970
|
+
}
|
|
971
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbDndState, deps: [{ token: DND_LIVE_ANNOUNCE, optional: true }, { token: DND_I18N, optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
972
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbDndState, providedIn: 'root' });
|
|
973
|
+
}
|
|
974
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbDndState, decorators: [{
|
|
975
|
+
type: Injectable,
|
|
976
|
+
args: [{ providedIn: 'root' }]
|
|
977
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
978
|
+
type: Optional
|
|
979
|
+
}, {
|
|
980
|
+
type: Inject,
|
|
981
|
+
args: [DND_LIVE_ANNOUNCE]
|
|
982
|
+
}] }, { type: undefined, decorators: [{
|
|
983
|
+
type: Optional
|
|
984
|
+
}, {
|
|
985
|
+
type: Inject,
|
|
986
|
+
args: [DND_I18N]
|
|
987
|
+
}] }] });
|
|
988
|
+
|
|
989
|
+
class NgbDndListDirective {
|
|
990
|
+
el = inject((ElementRef));
|
|
991
|
+
state = inject(NgbDndState);
|
|
992
|
+
dndChildrenKey = 'children';
|
|
993
|
+
/** Bind your array here: <div [ngbDndList]="items"> */
|
|
994
|
+
list;
|
|
995
|
+
/** Optional: group barrier across lists */
|
|
996
|
+
dndGroup;
|
|
997
|
+
/** Disable this list */
|
|
998
|
+
dndDisabled = false;
|
|
999
|
+
/** NEW: clone if source wasn't a list (e.g. palette) */
|
|
1000
|
+
dndCloneOnDrop = false;
|
|
1001
|
+
/** NEW: custom clone function */
|
|
1002
|
+
dndCloneFn;
|
|
1003
|
+
dndIsPalette = false;
|
|
1004
|
+
dndCanDrop = true;
|
|
1005
|
+
// (optional) visual feedback while hovering a denied list
|
|
1006
|
+
_denied = false;
|
|
1007
|
+
get denied() { return this._denied; }
|
|
1008
|
+
dndAriaLabel;
|
|
1009
|
+
get ariaLabel() { return this.dndAriaLabel || null; }
|
|
1010
|
+
/** Fires after item is inserted */
|
|
1011
|
+
dndDropped = new EventEmitter();
|
|
1012
|
+
role = 'list';
|
|
1013
|
+
hostClass = true;
|
|
1014
|
+
// NEW: when the pointer is over this list while dragging
|
|
1015
|
+
hover = false;
|
|
1016
|
+
get isOver() { return this.hover; }
|
|
1017
|
+
// NEW: reflect whether the current drag can drop here (for CSS)
|
|
1018
|
+
canDrop = null;
|
|
1019
|
+
get dataDropValid() {
|
|
1020
|
+
// values: "true" | "false" | null (attribute omitted)
|
|
1021
|
+
return this.canDrop === true ? 'true' : this.canDrop === false ? 'false' : null;
|
|
1022
|
+
}
|
|
1023
|
+
placeholder;
|
|
1024
|
+
lastIndex = -1;
|
|
1025
|
+
onDragEnter(ev) {
|
|
1026
|
+
if (this.dndDisabled)
|
|
1027
|
+
return;
|
|
1028
|
+
const s = this.getSession(ev);
|
|
1029
|
+
// If there is an active drag and groups match, indicate hover
|
|
1030
|
+
if (s) {
|
|
1031
|
+
ev.preventDefault();
|
|
1032
|
+
this.hover = true;
|
|
1033
|
+
this.canDrop = true; // group check already done in getSession
|
|
1034
|
+
}
|
|
1035
|
+
else {
|
|
1036
|
+
// Drag exists but not compatible with this list (group mismatch)
|
|
1037
|
+
this.hover = true;
|
|
1038
|
+
this.canDrop = false;
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
onDragOver(ev) {
|
|
1042
|
+
if (this.dndDisabled)
|
|
1043
|
+
return;
|
|
1044
|
+
const s = this.getSession(ev);
|
|
1045
|
+
if (!s) {
|
|
1046
|
+
// Not a compatible session → show invalid
|
|
1047
|
+
this.hover = true;
|
|
1048
|
+
this.canDrop = false;
|
|
1049
|
+
return;
|
|
1050
|
+
}
|
|
1051
|
+
ev.preventDefault();
|
|
1052
|
+
this.hover = true;
|
|
1053
|
+
this.canDrop = true;
|
|
1054
|
+
try {
|
|
1055
|
+
if (ev.dataTransfer)
|
|
1056
|
+
ev.dataTransfer.dropEffect = this.dndCloneOnDrop ? 'copy' : 'move';
|
|
1057
|
+
}
|
|
1058
|
+
catch { }
|
|
1059
|
+
this.ensurePlaceholder();
|
|
1060
|
+
const idx = this.indexFromPointer(ev);
|
|
1061
|
+
if (idx !== this.lastIndex) {
|
|
1062
|
+
this.lastIndex = idx;
|
|
1063
|
+
this.positionPlaceholderAt(idx);
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
onDragLeave(ev) {
|
|
1067
|
+
// Only reset when actually leaving this list (not moving between children)
|
|
1068
|
+
if (!this.el.nativeElement.contains(ev.relatedTarget)) {
|
|
1069
|
+
this.removePlaceholder();
|
|
1070
|
+
this.lastIndex = -1;
|
|
1071
|
+
this.hover = false;
|
|
1072
|
+
this.canDrop = null;
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
onDocumentDrop(ev) {
|
|
1076
|
+
const host = this.el.nativeElement;
|
|
1077
|
+
if (!host.contains(ev.target))
|
|
1078
|
+
return;
|
|
1079
|
+
const anyEv = ev;
|
|
1080
|
+
if (anyEv.__ngbHandled)
|
|
1081
|
+
return;
|
|
1082
|
+
anyEv.__ngbHandled = true;
|
|
1083
|
+
ev.preventDefault();
|
|
1084
|
+
this.performDrop(ev);
|
|
1085
|
+
// reset visual state
|
|
1086
|
+
this.hover = false;
|
|
1087
|
+
this.canDrop = null;
|
|
1088
|
+
}
|
|
1089
|
+
onDrop(ev) {
|
|
1090
|
+
if (this.dndDisabled)
|
|
1091
|
+
return;
|
|
1092
|
+
const anyEv = ev;
|
|
1093
|
+
if (anyEv.__ngbHandled)
|
|
1094
|
+
return;
|
|
1095
|
+
anyEv.__ngbHandled = true;
|
|
1096
|
+
ev.preventDefault();
|
|
1097
|
+
ev.stopPropagation();
|
|
1098
|
+
this.performDrop(ev);
|
|
1099
|
+
// reset visual state
|
|
1100
|
+
this.hover = false;
|
|
1101
|
+
this.canDrop = null;
|
|
1102
|
+
}
|
|
1103
|
+
// helpers
|
|
1104
|
+
getSession(ev) {
|
|
1105
|
+
// Chrome often returns "" during dragover/enter; try service fallback
|
|
1106
|
+
const sid = ev.dataTransfer?.getData('text/ngb-dnd') || null;
|
|
1107
|
+
let s = sid ? this.state.get(sid) : null;
|
|
1108
|
+
if (!s)
|
|
1109
|
+
s = this.state.getCurrent(); // ✅ fallback
|
|
1110
|
+
if (!s)
|
|
1111
|
+
return null;
|
|
1112
|
+
if (this.dndGroup && s.group && s.group !== this.dndGroup)
|
|
1113
|
+
return null;
|
|
1114
|
+
return s;
|
|
1115
|
+
}
|
|
1116
|
+
ensurePlaceholder() {
|
|
1117
|
+
if (!this.placeholder) {
|
|
1118
|
+
this.placeholder = document.createElement('div');
|
|
1119
|
+
this.placeholder.className = 'ngb-dnd-placeholder';
|
|
1120
|
+
this.placeholder.setAttribute('aria-hidden', 'true');
|
|
1121
|
+
this.el.nativeElement.appendChild(this.placeholder);
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
removePlaceholder() {
|
|
1125
|
+
if (this.placeholder?.parentElement)
|
|
1126
|
+
this.placeholder.parentElement.removeChild(this.placeholder);
|
|
1127
|
+
this.placeholder = undefined;
|
|
1128
|
+
}
|
|
1129
|
+
positionPlaceholderAt(index) {
|
|
1130
|
+
const host = this.el.nativeElement;
|
|
1131
|
+
const children = Array.from(host.children).filter(c => c !== this.placeholder);
|
|
1132
|
+
const before = children[index];
|
|
1133
|
+
if (!before)
|
|
1134
|
+
host.appendChild(this.placeholder);
|
|
1135
|
+
else
|
|
1136
|
+
host.insertBefore(this.placeholder, before);
|
|
1137
|
+
}
|
|
1138
|
+
indexFromPointer(ev) {
|
|
1139
|
+
const host = this.el.nativeElement;
|
|
1140
|
+
const y = ev.clientY;
|
|
1141
|
+
const children = Array.from(host.children).filter(c => c !== this.placeholder);
|
|
1142
|
+
for (let i = 0; i < children.length; i++) {
|
|
1143
|
+
const r = children[i].getBoundingClientRect();
|
|
1144
|
+
const mid = r.top + r.height / 2;
|
|
1145
|
+
if (y < mid)
|
|
1146
|
+
return i;
|
|
1147
|
+
}
|
|
1148
|
+
return children.length;
|
|
1149
|
+
}
|
|
1150
|
+
cloneItem(i) {
|
|
1151
|
+
if (this.dndCloneFn)
|
|
1152
|
+
return this.dndCloneFn(i);
|
|
1153
|
+
// fallback shallow clone
|
|
1154
|
+
if (Array.isArray(i))
|
|
1155
|
+
return i.slice();
|
|
1156
|
+
if (i && typeof i === 'object')
|
|
1157
|
+
return { ...i };
|
|
1158
|
+
return i; // primitive
|
|
1159
|
+
}
|
|
1160
|
+
isDroppingIntoOwnDescendant(item, targetList) {
|
|
1161
|
+
const key = this.dndChildrenKey;
|
|
1162
|
+
if (!key || !item)
|
|
1163
|
+
return false;
|
|
1164
|
+
const stack = [];
|
|
1165
|
+
if (Array.isArray(item[key]))
|
|
1166
|
+
stack.push(item[key]);
|
|
1167
|
+
while (stack.length) {
|
|
1168
|
+
const arr = stack.pop();
|
|
1169
|
+
if (arr === targetList)
|
|
1170
|
+
return true;
|
|
1171
|
+
for (const child of arr) {
|
|
1172
|
+
const kids = child?.[key];
|
|
1173
|
+
if (Array.isArray(kids))
|
|
1174
|
+
stack.push(kids);
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
return false;
|
|
1178
|
+
}
|
|
1179
|
+
/**
|
|
1180
|
+
* Build a guard payload using whatever drag context you already store.
|
|
1181
|
+
* If some fields don’t exist in your code, the fallbacks keep it working.
|
|
1182
|
+
*/
|
|
1183
|
+
_buildGuardPayload(dstIndex) {
|
|
1184
|
+
const anyThis = this;
|
|
1185
|
+
// Try your known drag context fields; fall back harmlessly if absent
|
|
1186
|
+
const dragItem = anyThis._dragCtx?.item ?? anyThis.dragItem ?? anyThis._drag?.item ?? null;
|
|
1187
|
+
const srcList = anyThis._dragCtx?.src?.list ?? anyThis.srcList ?? anyThis._drag?.srcList ?? [];
|
|
1188
|
+
const srcIndex = anyThis._dragCtx?.src?.index ?? anyThis.srcIndex ?? anyThis._drag?.srcIndex ?? -1;
|
|
1189
|
+
const dstList = this.list;
|
|
1190
|
+
return {
|
|
1191
|
+
dragItem,
|
|
1192
|
+
srcList,
|
|
1193
|
+
srcIndex,
|
|
1194
|
+
dstList,
|
|
1195
|
+
dstIndex,
|
|
1196
|
+
isExternal: srcList !== dstList,
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
/**
|
|
1200
|
+
* Compute destination index from the event position (no placeholder var needed).
|
|
1201
|
+
* Uses vertical list heuristics; adjust for horizontal if your list is horizontal.
|
|
1202
|
+
*/
|
|
1203
|
+
_computeDropIndex(ev) {
|
|
1204
|
+
const host = this._host?.nativeElement
|
|
1205
|
+
?? this._el?.nativeElement
|
|
1206
|
+
?? this.el?.nativeElement
|
|
1207
|
+
?? this.elementRef?.nativeElement
|
|
1208
|
+
?? null;
|
|
1209
|
+
if (!host)
|
|
1210
|
+
return this.list?.length ?? 0;
|
|
1211
|
+
const children = Array.from(host.children);
|
|
1212
|
+
const y = ev.clientY;
|
|
1213
|
+
for (let i = 0; i < children.length; i++) {
|
|
1214
|
+
const rect = children[i].getBoundingClientRect();
|
|
1215
|
+
const midY = rect.top + rect.height / 2;
|
|
1216
|
+
if (y < midY)
|
|
1217
|
+
return i;
|
|
1218
|
+
}
|
|
1219
|
+
return children.length;
|
|
1220
|
+
}
|
|
1221
|
+
// Evaluate the guard safely
|
|
1222
|
+
_allowDrop(payload) {
|
|
1223
|
+
const guard = this.dndCanDrop;
|
|
1224
|
+
if (typeof guard === 'boolean')
|
|
1225
|
+
return guard;
|
|
1226
|
+
try {
|
|
1227
|
+
return guard ? !!guard(payload) : true;
|
|
1228
|
+
}
|
|
1229
|
+
catch {
|
|
1230
|
+
return false;
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
// Extract your existing onDrop logic into a single method:
|
|
1234
|
+
performDrop(ev) {
|
|
1235
|
+
const session = this.getSession(ev);
|
|
1236
|
+
if (!session)
|
|
1237
|
+
return;
|
|
1238
|
+
const dstIndex = this._computeDropIndex(ev);
|
|
1239
|
+
// Guard check (block drop if not allowed)
|
|
1240
|
+
const payload = this._buildGuardPayload(dstIndex);
|
|
1241
|
+
if (!this._allowDrop(payload)) {
|
|
1242
|
+
ev.preventDefault();
|
|
1243
|
+
ev.stopPropagation();
|
|
1244
|
+
this._denied = true; // cosmetic: adds .ngb-dnd-denied for styling
|
|
1245
|
+
return; // ⛔ do not mutate arrays
|
|
1246
|
+
}
|
|
1247
|
+
this._denied = false;
|
|
1248
|
+
// ❗ block cycles: don't allow dropping an item into its own descendants
|
|
1249
|
+
if (this.isDroppingIntoOwnDescendant(session.item, this.list)) {
|
|
1250
|
+
// optional: visual/aural feedback
|
|
1251
|
+
this.state.announce?.(this.state.i18n.cannotDropHere());
|
|
1252
|
+
this.removePlaceholder();
|
|
1253
|
+
this.lastIndex = -1;
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
const targetIndex = this.lastIndex < 0 ? this.list.length : this.lastIndex;
|
|
1257
|
+
const fromList = session.fromList ?? undefined;
|
|
1258
|
+
const fromIndex = session.fromIndex ?? -1;
|
|
1259
|
+
const sameList = fromList === this.list;
|
|
1260
|
+
const isExternal = session.fromIsPalette === true || !fromList || fromIndex === -1;
|
|
1261
|
+
// ✅ External (palette): always transform/clone into canvas, and DO NOT remove from source.
|
|
1262
|
+
// ✅ Canvas↔Canvas: move by default; if you want to clone across canvas, set dndCloneOnDrop=true.
|
|
1263
|
+
const shouldCloneAcrossCanvas = this.dndCloneOnDrop && !sameList && !isExternal;
|
|
1264
|
+
const itemToInsert = isExternal
|
|
1265
|
+
? this.cloneItem(session.item) // transform palette item to Panel
|
|
1266
|
+
: (shouldCloneAcrossCanvas ? this.cloneItem(session.item) : session.item);
|
|
1267
|
+
// Remove from source ONLY for real moves within canvas
|
|
1268
|
+
if (!isExternal && !shouldCloneAcrossCanvas && fromIndex > -1) {
|
|
1269
|
+
fromList.splice(fromIndex, 1);
|
|
1270
|
+
}
|
|
1271
|
+
// adjust index when reordering within same list and moving forward
|
|
1272
|
+
const insertAt = sameList && fromIndex > -1 && targetIndex > fromIndex
|
|
1273
|
+
? targetIndex - 1
|
|
1274
|
+
: targetIndex;
|
|
1275
|
+
this.list.splice(insertAt, 0, itemToInsert);
|
|
1276
|
+
this.removePlaceholder();
|
|
1277
|
+
this.lastIndex = -1;
|
|
1278
|
+
this.dndDropped.emit({
|
|
1279
|
+
item: itemToInsert,
|
|
1280
|
+
fromIndex,
|
|
1281
|
+
toIndex: insertAt,
|
|
1282
|
+
fromList: fromList ?? [],
|
|
1283
|
+
toList: this.list,
|
|
1284
|
+
sameList
|
|
1285
|
+
});
|
|
1286
|
+
this.state.announce?.(this.state.i18n.dropped());
|
|
1287
|
+
}
|
|
1288
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbDndListDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
|
|
1289
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: NgbDndListDirective, isStandalone: true, selector: "[ngbDndList]", inputs: { dndChildrenKey: "dndChildrenKey", list: ["ngbDndList", "list"], dndGroup: "dndGroup", dndDisabled: "dndDisabled", dndCloneOnDrop: "dndCloneOnDrop", dndCloneFn: "dndCloneFn", dndIsPalette: "dndIsPalette", dndCanDrop: "dndCanDrop", dndAriaLabel: "dndAriaLabel" }, outputs: { dndDropped: "dndDropped" }, host: { listeners: { "dragenter": "onDragEnter($event)", "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)", "document:drop": "onDocumentDrop($event)", "drop": "onDrop($event)" }, properties: { "class.ngb-dnd-denied": "this.denied", "attr.aria-label": "this.ariaLabel", "attr.role": "this.role", "class.ngb-dnd-list": "this.hostClass", "class.ngb-dnd-over": "this.isOver", "attr.data-drop-valid": "this.dataDropValid" } }, ngImport: i0 });
|
|
1290
|
+
}
|
|
1291
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbDndListDirective, decorators: [{
|
|
1292
|
+
type: Directive,
|
|
1293
|
+
args: [{
|
|
1294
|
+
selector: '[ngbDndList]',
|
|
1295
|
+
standalone: true
|
|
1296
|
+
}]
|
|
1297
|
+
}], propDecorators: { dndChildrenKey: [{
|
|
1298
|
+
type: Input
|
|
1299
|
+
}], list: [{
|
|
1300
|
+
type: Input,
|
|
1301
|
+
args: ['ngbDndList']
|
|
1302
|
+
}], dndGroup: [{
|
|
1303
|
+
type: Input
|
|
1304
|
+
}], dndDisabled: [{
|
|
1305
|
+
type: Input
|
|
1306
|
+
}], dndCloneOnDrop: [{
|
|
1307
|
+
type: Input
|
|
1308
|
+
}], dndCloneFn: [{
|
|
1309
|
+
type: Input
|
|
1310
|
+
}], dndIsPalette: [{
|
|
1311
|
+
type: Input
|
|
1312
|
+
}], dndCanDrop: [{
|
|
1313
|
+
type: Input
|
|
1314
|
+
}], denied: [{
|
|
1315
|
+
type: HostBinding,
|
|
1316
|
+
args: ['class.ngb-dnd-denied']
|
|
1317
|
+
}], dndAriaLabel: [{
|
|
1318
|
+
type: Input
|
|
1319
|
+
}], ariaLabel: [{
|
|
1320
|
+
type: HostBinding,
|
|
1321
|
+
args: ['attr.aria-label']
|
|
1322
|
+
}], dndDropped: [{
|
|
1323
|
+
type: Output
|
|
1324
|
+
}], role: [{
|
|
1325
|
+
type: HostBinding,
|
|
1326
|
+
args: ['attr.role']
|
|
1327
|
+
}], hostClass: [{
|
|
1328
|
+
type: HostBinding,
|
|
1329
|
+
args: ['class.ngb-dnd-list']
|
|
1330
|
+
}], isOver: [{
|
|
1331
|
+
type: HostBinding,
|
|
1332
|
+
args: ['class.ngb-dnd-over']
|
|
1333
|
+
}], dataDropValid: [{
|
|
1334
|
+
type: HostBinding,
|
|
1335
|
+
args: ['attr.data-drop-valid']
|
|
1336
|
+
}], onDragEnter: [{
|
|
1337
|
+
type: HostListener,
|
|
1338
|
+
args: ['dragenter', ['$event']]
|
|
1339
|
+
}], onDragOver: [{
|
|
1340
|
+
type: HostListener,
|
|
1341
|
+
args: ['dragover', ['$event']]
|
|
1342
|
+
}], onDragLeave: [{
|
|
1343
|
+
type: HostListener,
|
|
1344
|
+
args: ['dragleave', ['$event']]
|
|
1345
|
+
}], onDocumentDrop: [{
|
|
1346
|
+
type: HostListener,
|
|
1347
|
+
args: ['document:drop', ['$event']]
|
|
1348
|
+
}], onDrop: [{
|
|
1349
|
+
type: HostListener,
|
|
1350
|
+
args: ['drop', ['$event']]
|
|
1351
|
+
}] } });
|
|
1352
|
+
|
|
1353
|
+
class NgbDndItemDirective {
|
|
1354
|
+
parentList;
|
|
1355
|
+
el = inject((ElementRef));
|
|
1356
|
+
state = inject(NgbDndState);
|
|
1357
|
+
constructor(parentList) {
|
|
1358
|
+
this.parentList = parentList;
|
|
1359
|
+
}
|
|
1360
|
+
/** required: the value carried by this row/panel */
|
|
1361
|
+
item;
|
|
1362
|
+
/** optional: constrain cross-list drops */
|
|
1363
|
+
dndGroup;
|
|
1364
|
+
/** index inside its parent list (bind to *ngFor index) */
|
|
1365
|
+
dndIndex;
|
|
1366
|
+
dndDisabled = false;
|
|
1367
|
+
dndDragStart = new EventEmitter();
|
|
1368
|
+
dndDragEnd = new EventEmitter();
|
|
1369
|
+
get draggable() { return !this.dndDisabled; }
|
|
1370
|
+
dragging = false;
|
|
1371
|
+
get ariaGrabbed() { return this.dragging ? 'true' : 'false'; }
|
|
1372
|
+
role = 'listitem';
|
|
1373
|
+
tabIndex = 0; // keyboard focusable
|
|
1374
|
+
sessionId = null;
|
|
1375
|
+
onDragStart(ev) {
|
|
1376
|
+
if (this.dndDisabled || this.item == null) {
|
|
1377
|
+
ev.preventDefault();
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
this.sessionId = this.state.createSession({
|
|
1381
|
+
item: this.item,
|
|
1382
|
+
group: this.dndGroup ?? this.parentList?.dndGroup,
|
|
1383
|
+
fromList: this.parentList?.list,
|
|
1384
|
+
fromIndex: this.dndIndex,
|
|
1385
|
+
fromIsPalette: this.parentList?.dndIsPalette === true
|
|
1386
|
+
});
|
|
1387
|
+
// ✅ set both a custom and a plain text payload; keep plain text non-empty
|
|
1388
|
+
ev.dataTransfer?.setData('text/ngb-dnd', this.sessionId);
|
|
1389
|
+
ev.dataTransfer?.setData('text/plain', 'ngb'); // some browsers require non-empty
|
|
1390
|
+
if (ev.dataTransfer)
|
|
1391
|
+
ev.dataTransfer.effectAllowed = 'copyMove';
|
|
1392
|
+
ev.dataTransfer?.setDragImage(this.el.nativeElement, 16, 16);
|
|
1393
|
+
this.dragging = true;
|
|
1394
|
+
this.dndDragStart.emit(this.item);
|
|
1395
|
+
}
|
|
1396
|
+
onDragEnd() {
|
|
1397
|
+
this.dragging = false;
|
|
1398
|
+
this.state.clear(this.sessionId);
|
|
1399
|
+
this.sessionId = null;
|
|
1400
|
+
this.dndDragEnd.emit();
|
|
1401
|
+
}
|
|
1402
|
+
onKeydown(ev) {
|
|
1403
|
+
if (this.dndDisabled)
|
|
1404
|
+
return;
|
|
1405
|
+
// start dragging (keyboard mode)
|
|
1406
|
+
if (ev.code === 'Space' && !this.dragging) {
|
|
1407
|
+
ev.preventDefault();
|
|
1408
|
+
this.onDragStart(new DragEvent('dragstart')); // reuse logic
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
if (!this.dragging)
|
|
1412
|
+
return;
|
|
1413
|
+
// move within same list
|
|
1414
|
+
const parent = this.parentList;
|
|
1415
|
+
if (!parent || typeof this.dndIndex !== 'number')
|
|
1416
|
+
return;
|
|
1417
|
+
if (ev.code === 'ArrowUp' || ev.code === 'ArrowDown') {
|
|
1418
|
+
ev.preventDefault();
|
|
1419
|
+
const list = parent.list;
|
|
1420
|
+
const from = this.dndIndex;
|
|
1421
|
+
let to = ev.code === 'ArrowUp' ? from - 1 : from + 1;
|
|
1422
|
+
to = Math.max(0, Math.min(list.length - 1, to));
|
|
1423
|
+
if (to !== from) {
|
|
1424
|
+
const [it] = list.splice(from, 1);
|
|
1425
|
+
list.splice(to, 0, it);
|
|
1426
|
+
this.dndIndex = to;
|
|
1427
|
+
// announce (see live region below)
|
|
1428
|
+
this.state.announce?.(this.state.i18n.moveToIndex(to + 1, list.length));
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (ev.code === 'Enter') {
|
|
1432
|
+
ev.preventDefault();
|
|
1433
|
+
this.onDragEnd();
|
|
1434
|
+
} // drop
|
|
1435
|
+
if (ev.code === 'Escape') {
|
|
1436
|
+
ev.preventDefault();
|
|
1437
|
+
this.onDragEnd();
|
|
1438
|
+
} // cancel
|
|
1439
|
+
}
|
|
1440
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbDndItemDirective, deps: [{ token: NgbDndListDirective, optional: true }], target: i0.ɵɵFactoryTarget.Directive });
|
|
1441
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: NgbDndItemDirective, isStandalone: true, selector: "[ngbDndItem]", inputs: { item: ["ngbDndItem", "item"], dndGroup: "dndGroup", dndIndex: "dndIndex", dndDisabled: "dndDisabled" }, outputs: { dndDragStart: "dndDragStart", dndDragEnd: "dndDragEnd" }, host: { listeners: { "dragstart": "onDragStart($event)", "dragend": "onDragEnd()", "keydown": "onKeydown($event)" }, properties: { "attr.draggable": "this.draggable", "class.ngb-dnd-dragging": "this.dragging", "attr.aria-grabbed": "this.ariaGrabbed", "attr.role": "this.role", "attr.tabindex": "this.tabIndex" } }, ngImport: i0 });
|
|
1442
|
+
}
|
|
1443
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbDndItemDirective, decorators: [{
|
|
1444
|
+
type: Directive,
|
|
1445
|
+
args: [{
|
|
1446
|
+
selector: '[ngbDndItem]',
|
|
1447
|
+
standalone: true
|
|
1448
|
+
}]
|
|
1449
|
+
}], ctorParameters: () => [{ type: NgbDndListDirective, decorators: [{
|
|
1450
|
+
type: Optional
|
|
1451
|
+
}] }], propDecorators: { item: [{
|
|
1452
|
+
type: Input,
|
|
1453
|
+
args: ['ngbDndItem']
|
|
1454
|
+
}], dndGroup: [{
|
|
1455
|
+
type: Input
|
|
1456
|
+
}], dndIndex: [{
|
|
1457
|
+
type: Input
|
|
1458
|
+
}], dndDisabled: [{
|
|
1459
|
+
type: Input
|
|
1460
|
+
}], dndDragStart: [{
|
|
1461
|
+
type: Output
|
|
1462
|
+
}], dndDragEnd: [{
|
|
1463
|
+
type: Output
|
|
1464
|
+
}], draggable: [{
|
|
1465
|
+
type: HostBinding,
|
|
1466
|
+
args: ['attr.draggable']
|
|
1467
|
+
}], dragging: [{
|
|
1468
|
+
type: HostBinding,
|
|
1469
|
+
args: ['class.ngb-dnd-dragging']
|
|
1470
|
+
}], ariaGrabbed: [{
|
|
1471
|
+
type: HostBinding,
|
|
1472
|
+
args: ['attr.aria-grabbed']
|
|
1473
|
+
}], role: [{
|
|
1474
|
+
type: HostBinding,
|
|
1475
|
+
args: ['attr.role']
|
|
1476
|
+
}], tabIndex: [{
|
|
1477
|
+
type: HostBinding,
|
|
1478
|
+
args: ['attr.tabindex']
|
|
1479
|
+
}], onDragStart: [{
|
|
1480
|
+
type: HostListener,
|
|
1481
|
+
args: ['dragstart', ['$event']]
|
|
1482
|
+
}], onDragEnd: [{
|
|
1483
|
+
type: HostListener,
|
|
1484
|
+
args: ['dragend']
|
|
1485
|
+
}], onKeydown: [{
|
|
1486
|
+
type: HostListener,
|
|
1487
|
+
args: ['keydown', ['$event']]
|
|
1488
|
+
}] } });
|
|
1489
|
+
|
|
1490
|
+
class NgbStepLabelDirective {
|
|
1491
|
+
template;
|
|
1492
|
+
for;
|
|
1493
|
+
constructor(template) {
|
|
1494
|
+
this.template = template;
|
|
1495
|
+
}
|
|
1496
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbStepLabelDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive });
|
|
1497
|
+
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "21.0.6", type: NgbStepLabelDirective, isStandalone: true, selector: "[ngbStepLabel]", inputs: { for: ["ngbStepLabel", "for"] }, ngImport: i0 });
|
|
1498
|
+
}
|
|
1499
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbStepLabelDirective, decorators: [{
|
|
1500
|
+
type: Directive,
|
|
1501
|
+
args: [{
|
|
1502
|
+
selector: '[ngbStepLabel]',
|
|
1503
|
+
standalone: true,
|
|
1504
|
+
}]
|
|
1505
|
+
}], ctorParameters: () => [{ type: i0.TemplateRef }], propDecorators: { for: [{
|
|
1506
|
+
type: Input,
|
|
1507
|
+
args: ['ngbStepLabel']
|
|
1508
|
+
}] } });
|
|
1509
|
+
|
|
1510
|
+
class NgbStepperComponent {
|
|
1511
|
+
/** Steps definition */
|
|
1512
|
+
steps = [];
|
|
1513
|
+
/** Orientation of the stepper */
|
|
1514
|
+
orientation = 'horizontal';
|
|
1515
|
+
/** Position of labels around the indicator */
|
|
1516
|
+
labelPosition = 'right';
|
|
1517
|
+
/** Position of content relative to header (horizontal only) */
|
|
1518
|
+
contentPosition = 'bottom';
|
|
1519
|
+
/** Allow navigating back to previous steps by clicking header */
|
|
1520
|
+
allowRevisit = true;
|
|
1521
|
+
/** Enable lazy loading of step content (default true) */
|
|
1522
|
+
lazy = true;
|
|
1523
|
+
/** Theme keyword (only CSS classes, no external deps) */
|
|
1524
|
+
theme = 'bootstrap';
|
|
1525
|
+
/** Whether to apply responsive helper class */
|
|
1526
|
+
responsive = true;
|
|
1527
|
+
/** Disable transition / animation helpers */
|
|
1528
|
+
disableAnimation = false;
|
|
1529
|
+
/** Animation duration (ms) for step transitions when enabled */
|
|
1530
|
+
animationDuration = 150;
|
|
1531
|
+
/** Label strings (for i18n) */
|
|
1532
|
+
nextLabel = 'Next';
|
|
1533
|
+
previousLabel = 'Previous';
|
|
1534
|
+
resetLabel = 'Reset';
|
|
1535
|
+
cancelLabel = 'Cancel';
|
|
1536
|
+
optionalLabel = 'Optional';
|
|
1537
|
+
/** Custom mapping from state -> icon text */
|
|
1538
|
+
stateIcons = {
|
|
1539
|
+
number: '#',
|
|
1540
|
+
done: '✓',
|
|
1541
|
+
active: '•',
|
|
1542
|
+
error: '!',
|
|
1543
|
+
disabled: '×',
|
|
1544
|
+
};
|
|
1545
|
+
/** Current selected index */
|
|
1546
|
+
selectedIndex = 0;
|
|
1547
|
+
/** Emits when selectedIndex changes (programmatically or by user) */
|
|
1548
|
+
selectedIndexChange = new EventEmitter();
|
|
1549
|
+
/** Emits when selection changes with rich event */
|
|
1550
|
+
selectionChange = new EventEmitter();
|
|
1551
|
+
/** Navigation events */
|
|
1552
|
+
nextClicked = new EventEmitter();
|
|
1553
|
+
prevClicked = new EventEmitter();
|
|
1554
|
+
resetClicked = new EventEmitter();
|
|
1555
|
+
cancelClicked = new EventEmitter();
|
|
1556
|
+
/** Custom label template (applied for all steps) */
|
|
1557
|
+
stepLabelTplDir;
|
|
1558
|
+
/** Custom icon template for step indicator */
|
|
1559
|
+
customIconTpl;
|
|
1560
|
+
/** Content template; user will typically use <ng-template #stepContent ...> */
|
|
1561
|
+
stepContentTpl;
|
|
1562
|
+
/** Track which steps have been visited */
|
|
1563
|
+
visited = new Set();
|
|
1564
|
+
/** Used when allowRevisit=false to block earlier steps */
|
|
1565
|
+
visitedFrom = 0;
|
|
1566
|
+
ngAfterContentInit() {
|
|
1567
|
+
if (this.selectedIndex < 0)
|
|
1568
|
+
this.selectedIndex = 0;
|
|
1569
|
+
if (this.selectedIndex > this.steps.length - 1 && this.steps.length > 0) {
|
|
1570
|
+
this.selectedIndex = this.steps.length - 1;
|
|
1571
|
+
}
|
|
1572
|
+
this.markVisited(this.selectedIndex);
|
|
1573
|
+
}
|
|
1574
|
+
get stepLabelTpl() {
|
|
1575
|
+
return this.stepLabelTplDir?.template ?? null;
|
|
1576
|
+
}
|
|
1577
|
+
get labelPositionClass() {
|
|
1578
|
+
switch (this.labelPosition) {
|
|
1579
|
+
case 'top':
|
|
1580
|
+
return 'ngb-stepper-label-top';
|
|
1581
|
+
case 'bottom':
|
|
1582
|
+
return 'ngb-stepper-label-bottom';
|
|
1583
|
+
case 'left':
|
|
1584
|
+
return 'ngb-stepper-label-left';
|
|
1585
|
+
default:
|
|
1586
|
+
return 'ngb-stepper-label-right';
|
|
1587
|
+
}
|
|
1588
|
+
}
|
|
1589
|
+
get currentErrorMessage() {
|
|
1590
|
+
const step = this.steps[this.selectedIndex];
|
|
1591
|
+
return step?.errorMessage ?? null;
|
|
1592
|
+
}
|
|
1593
|
+
stepId(i) {
|
|
1594
|
+
const s = this.steps[i];
|
|
1595
|
+
const base = s?.id || `ngb-step-${i}`;
|
|
1596
|
+
return `${base}-label`;
|
|
1597
|
+
}
|
|
1598
|
+
contentId(i) {
|
|
1599
|
+
const s = this.steps[i];
|
|
1600
|
+
const base = s?.id || `ngb-step-${i}`;
|
|
1601
|
+
return `${base}-content`;
|
|
1602
|
+
}
|
|
1603
|
+
markVisited(i) {
|
|
1604
|
+
this.visited.add(i);
|
|
1605
|
+
this.visitedFrom = Math.max(this.visitedFrom, i);
|
|
1606
|
+
}
|
|
1607
|
+
stepState(i) {
|
|
1608
|
+
const s = this.steps[i];
|
|
1609
|
+
if (!s)
|
|
1610
|
+
return 'number';
|
|
1611
|
+
if (s.state)
|
|
1612
|
+
return s.state;
|
|
1613
|
+
if (this.isStepInError(i))
|
|
1614
|
+
return 'error';
|
|
1615
|
+
if (i === this.selectedIndex)
|
|
1616
|
+
return 'active';
|
|
1617
|
+
if (i < this.selectedIndex)
|
|
1618
|
+
return 'done';
|
|
1619
|
+
if (s.disabled)
|
|
1620
|
+
return 'disabled';
|
|
1621
|
+
return 'number';
|
|
1622
|
+
}
|
|
1623
|
+
isStepInError(i) {
|
|
1624
|
+
const s = this.steps[i];
|
|
1625
|
+
return !!s?.errorMessage && this.visited.has(i);
|
|
1626
|
+
}
|
|
1627
|
+
indicatorClass(i) {
|
|
1628
|
+
const state = this.stepState(i);
|
|
1629
|
+
return state;
|
|
1630
|
+
}
|
|
1631
|
+
defaultIconForState(state, index) {
|
|
1632
|
+
if (state === 'number') {
|
|
1633
|
+
return String(index + 1);
|
|
1634
|
+
}
|
|
1635
|
+
return this.stateIcons[state] ?? this.stateIcons['number'] ?? String(index + 1);
|
|
1636
|
+
}
|
|
1637
|
+
onHeaderClick(i) {
|
|
1638
|
+
if (i === this.selectedIndex)
|
|
1639
|
+
return;
|
|
1640
|
+
const step = this.steps[i];
|
|
1641
|
+
if (!step || step.disabled)
|
|
1642
|
+
return;
|
|
1643
|
+
if (!this.allowRevisit && i < this.visitedFrom)
|
|
1644
|
+
return;
|
|
1645
|
+
this.changeIndex(i);
|
|
1646
|
+
}
|
|
1647
|
+
onHeaderKeydown(event, i) {
|
|
1648
|
+
event.preventDefault();
|
|
1649
|
+
this.onHeaderClick(i);
|
|
1650
|
+
}
|
|
1651
|
+
next() {
|
|
1652
|
+
const target = Math.min(this.steps.length - 1, this.selectedIndex + 1);
|
|
1653
|
+
if (target === this.selectedIndex)
|
|
1654
|
+
return;
|
|
1655
|
+
this.nextClicked.emit(target);
|
|
1656
|
+
this.changeIndex(target);
|
|
1657
|
+
}
|
|
1658
|
+
prev() {
|
|
1659
|
+
if (!this.allowRevisit)
|
|
1660
|
+
return;
|
|
1661
|
+
const target = Math.max(0, this.selectedIndex - 1);
|
|
1662
|
+
if (target === this.selectedIndex)
|
|
1663
|
+
return;
|
|
1664
|
+
this.prevClicked.emit(target);
|
|
1665
|
+
this.changeIndex(target);
|
|
1666
|
+
}
|
|
1667
|
+
onReset() {
|
|
1668
|
+
this.resetClicked.emit();
|
|
1669
|
+
this.selectedIndex = 0;
|
|
1670
|
+
this.selectedIndexChange.emit(this.selectedIndex);
|
|
1671
|
+
this.selectionChange.emit({
|
|
1672
|
+
previousIndex: 0,
|
|
1673
|
+
currentIndex: 0,
|
|
1674
|
+
step: this.steps[0],
|
|
1675
|
+
});
|
|
1676
|
+
this.visited.clear();
|
|
1677
|
+
this.visitedFrom = 0;
|
|
1678
|
+
this.markVisited(0);
|
|
1679
|
+
}
|
|
1680
|
+
onCancel() {
|
|
1681
|
+
this.cancelClicked.emit();
|
|
1682
|
+
}
|
|
1683
|
+
changeIndex(nextIndex) {
|
|
1684
|
+
const prev = this.selectedIndex;
|
|
1685
|
+
this.selectedIndex = nextIndex;
|
|
1686
|
+
this.markVisited(nextIndex);
|
|
1687
|
+
this.selectedIndexChange.emit(this.selectedIndex);
|
|
1688
|
+
this.selectionChange.emit({
|
|
1689
|
+
previousIndex: prev,
|
|
1690
|
+
currentIndex: nextIndex,
|
|
1691
|
+
step: this.steps[nextIndex],
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1694
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbStepperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1695
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: NgbStepperComponent, isStandalone: true, selector: "ngb-stepper", inputs: { steps: "steps", orientation: "orientation", labelPosition: "labelPosition", contentPosition: "contentPosition", allowRevisit: "allowRevisit", lazy: "lazy", theme: "theme", responsive: "responsive", disableAnimation: "disableAnimation", animationDuration: "animationDuration", nextLabel: "nextLabel", previousLabel: "previousLabel", resetLabel: "resetLabel", cancelLabel: "cancelLabel", optionalLabel: "optionalLabel", stateIcons: "stateIcons", selectedIndex: "selectedIndex" }, outputs: { selectedIndexChange: "selectedIndexChange", selectionChange: "selectionChange", nextClicked: "nextClicked", prevClicked: "prevClicked", resetClicked: "resetClicked", cancelClicked: "cancelClicked" }, queries: [{ propertyName: "stepLabelTplDir", first: true, predicate: NgbStepLabelDirective, descendants: true }, { propertyName: "customIconTpl", first: true, predicate: ["ngbStepperIcon"], descendants: true, read: TemplateRef }, { propertyName: "stepContentTpl", first: true, predicate: ["stepContent"], descendants: true, read: TemplateRef }], ngImport: i0, template: "<div\n class=\"ngb-stepper\"\n [class.horizontal]=\"orientation === 'horizontal'\"\n [class.vertical]=\"orientation === 'vertical'\"\n [class.ngb-responsive]=\"responsive\"\n [class.bootstrap]=\"theme === 'bootstrap'\"\n [class.material]=\"theme === 'material'\"\n [class.tailwind]=\"theme === 'tailwind'\"\n role=\"tablist\"\n [attr.aria-orientation]=\"orientation\"\n>\n <!-- HEADER / LABELS -->\n <div class=\"ngb-stepper-header\">\n <ol class=\"ngb-stepper-steps\" role=\"presentation\">\n <li\n *ngFor=\"let step of steps; let i = index\"\n class=\"ngb-stepper-step\"\n [class.active]=\"i === selectedIndex\"\n [class.done]=\"i < selectedIndex && !isStepInError(i)\"\n [class.error]=\"isStepInError(i)\"\n [class.disabled]=\"step.disabled\"\n role=\"tab\"\n [attr.id]=\"stepId(i)\"\n [attr.aria-controls]=\"contentId(i)\"\n [attr.aria-selected]=\"i === selectedIndex\"\n [attr.aria-disabled]=\"step.disabled || (!allowRevisit && i < visitedFrom && i !== selectedIndex)\"\n tabindex=\"{{ i === selectedIndex ? 0 : -1 }}\"\n (click)=\"onHeaderClick(i)\"\n (keydown.enter)=\"onHeaderKeydown($event, i)\"\n (keydown.space)=\"onHeaderKeydown($event, i)\"\n >\n <div\n class=\"ngb-stepper-indicator\"\n [ngClass]=\"indicatorClass(i)\"\n aria-hidden=\"true\"\n >\n <ng-container *ngIf=\"customIconTpl; else defaultIcon\">\n <ng-container\n [ngTemplateOutlet]=\"customIconTpl\"\n [ngTemplateOutletContext]=\"{ index: i, state: stepState(i) }\"\n ></ng-container>\n </ng-container>\n <ng-template #defaultIcon>\n {{ defaultIconForState(stepState(i), i) }}\n </ng-template>\n </div>\n\n <div\n class=\"ngb-stepper-label-container\"\n [ngClass]=\"labelPositionClass\"\n >\n <div class=\"ngb-stepper-label\" *ngIf=\"stepLabelTpl; else textLabel\">\n <ng-container\n [ngTemplateOutlet]=\"stepLabelTpl\"\n [ngTemplateOutletContext]=\"{ index: i, step: step }\"\n ></ng-container>\n </div>\n <ng-template #textLabel>\n <div class=\"ngb-stepper-label-text\">\n {{ step.label || ('Step ' + (i + 1)) }}\n </div>\n </ng-template>\n <div class=\"ngb-stepper-optional\" *ngIf=\"step.optional\">\n {{ optionalLabel }}\n </div>\n </div>\n </li>\n </ol>\n </div>\n\n <!-- CONTENT -->\n <div class=\"ngb-stepper-content\" [class.ngb-stepper-content-top]=\"contentPosition === 'top'\">\n <ng-container *ngIf=\"lazy; else allContent\">\n <ng-container *ngFor=\"let index of [selectedIndex]\">\n <div\n class=\"fade-step\"\n [style.--ngb-stepper-animation-duration]=\"(disableAnimation ? 0 : animationDuration) + 'ms'\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"stepId(index)\"\n [attr.id]=\"contentId(index)\"\n >\n <ng-container *ngTemplateOutlet=\"stepContentTpl; context: { index: index }\"></ng-container>\n <div\n *ngIf=\"currentErrorMessage\"\n class=\"ngb-stepper-error\"\n role=\"alert\"\n >\n {{ currentErrorMessage }}\n </div>\n </div>\n </ng-container>\n </ng-container>\n\n <ng-template #allContent>\n <div\n class=\"fade-step\"\n [style.--ngb-stepper-animation-duration]=\"(disableAnimation ? 0 : animationDuration) + 'ms'\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"stepId(selectedIndex)\"\n [attr.id]=\"contentId(selectedIndex)\"\n >\n <ng-container *ngTemplateOutlet=\"stepContentTpl; context: { index: selectedIndex }\"></ng-container>\n <div\n *ngIf=\"currentErrorMessage\"\n class=\"ngb-stepper-error\"\n role=\"alert\"\n >\n {{ currentErrorMessage }}\n </div>\n </div>\n </ng-template>\n </div>\n\n <!-- ACTIONS -->\n <div class=\"ngb-stepper-buttons\">\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"onCancel()\"\n >\n {{ cancelLabel }}\n </button>\n\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"onReset()\"\n >\n {{ resetLabel }}\n </button>\n\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"prev()\"\n [disabled]=\"selectedIndex <= 0 || !allowRevisit\"\n >\n {{ previousLabel }}\n </button>\n\n <button\n type=\"button\"\n class=\"btn btn-sm btn-primary\"\n (click)=\"next()\"\n [disabled]=\"selectedIndex >= steps.length - 1 || steps[selectedIndex]?.disabled\"\n >\n {{ nextLabel }}\n </button>\n </div>\n</div>\n", styles: [".ngb-stepper{display:flex;flex-direction:column}.ngb-stepper.horizontal .ngb-stepper-header{display:flex;overflow-x:auto}.ngb-stepper.vertical .ngb-stepper-header{display:block}.ngb-stepper-header{margin-bottom:1rem}.ngb-stepper-steps{list-style:none;padding:0;margin:0;display:flex;flex-wrap:nowrap}.ngb-stepper.vertical .ngb-stepper-steps{flex-direction:column}.ngb-stepper-step{position:relative;display:flex;align-items:center;cursor:pointer}.ngb-stepper.horizontal .ngb-stepper-step{padding:.5rem .75rem}.ngb-stepper.vertical .ngb-stepper-step{padding:.5rem 0}.ngb-stepper-step.disabled{cursor:default;opacity:.6}.ngb-stepper-indicator{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:50%;margin-right:.5rem;flex-shrink:0}.ngb-stepper.bootstrap .ngb-stepper-indicator{border:1px solid var(--bs-border-color, #dee2e6);background-color:var(--bs-body-bg, #fff)}.ngb-stepper.bootstrap .ngb-stepper-step.active .ngb-stepper-indicator{background-color:var(--bs-primary, #0d6efd);color:#fff}.ngb-stepper.bootstrap .ngb-stepper-step.done .ngb-stepper-indicator{background-color:var(--bs-success, #198754);color:#fff}.ngb-stepper.bootstrap .ngb-stepper-step.error .ngb-stepper-indicator{background-color:var(--bs-danger, #dc3545);color:#fff}.ngb-stepper-label-container{display:flex;flex-direction:column}.ngb-stepper-label-top{flex-direction:column-reverse}.ngb-stepper-label-bottom{flex-direction:column}.ngb-stepper-label-left{flex-direction:row-reverse}.ngb-stepper-label-right{flex-direction:row}.ngb-stepper-content{position:relative}.ngb-stepper-content-top{order:-1}.ngb-stepper-buttons{display:flex;justify-content:flex-end;gap:.5rem;margin-top:1rem}.ngb-stepper-error{color:var(--bs-danger, #dc3545);font-size:.875rem;margin-top:.25rem}.ngb-stepper-optional{font-size:.75rem;opacity:.7}.ngb-stepper .fade-step{animation:ngb-stepper-fade var(--ngb-stepper-animation-duration, .15s) ease-in-out;animation-fill-mode:both}@keyframes ngb-stepper-fade{0%{opacity:0;transform:translateY(.35rem)}to{opacity:1;transform:translateY(0)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
1696
|
+
}
|
|
1697
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbStepperComponent, decorators: [{
|
|
1698
|
+
type: Component,
|
|
1699
|
+
args: [{ selector: 'ngb-stepper', standalone: true, imports: [CommonModule, NgbStepLabelDirective], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div\n class=\"ngb-stepper\"\n [class.horizontal]=\"orientation === 'horizontal'\"\n [class.vertical]=\"orientation === 'vertical'\"\n [class.ngb-responsive]=\"responsive\"\n [class.bootstrap]=\"theme === 'bootstrap'\"\n [class.material]=\"theme === 'material'\"\n [class.tailwind]=\"theme === 'tailwind'\"\n role=\"tablist\"\n [attr.aria-orientation]=\"orientation\"\n>\n <!-- HEADER / LABELS -->\n <div class=\"ngb-stepper-header\">\n <ol class=\"ngb-stepper-steps\" role=\"presentation\">\n <li\n *ngFor=\"let step of steps; let i = index\"\n class=\"ngb-stepper-step\"\n [class.active]=\"i === selectedIndex\"\n [class.done]=\"i < selectedIndex && !isStepInError(i)\"\n [class.error]=\"isStepInError(i)\"\n [class.disabled]=\"step.disabled\"\n role=\"tab\"\n [attr.id]=\"stepId(i)\"\n [attr.aria-controls]=\"contentId(i)\"\n [attr.aria-selected]=\"i === selectedIndex\"\n [attr.aria-disabled]=\"step.disabled || (!allowRevisit && i < visitedFrom && i !== selectedIndex)\"\n tabindex=\"{{ i === selectedIndex ? 0 : -1 }}\"\n (click)=\"onHeaderClick(i)\"\n (keydown.enter)=\"onHeaderKeydown($event, i)\"\n (keydown.space)=\"onHeaderKeydown($event, i)\"\n >\n <div\n class=\"ngb-stepper-indicator\"\n [ngClass]=\"indicatorClass(i)\"\n aria-hidden=\"true\"\n >\n <ng-container *ngIf=\"customIconTpl; else defaultIcon\">\n <ng-container\n [ngTemplateOutlet]=\"customIconTpl\"\n [ngTemplateOutletContext]=\"{ index: i, state: stepState(i) }\"\n ></ng-container>\n </ng-container>\n <ng-template #defaultIcon>\n {{ defaultIconForState(stepState(i), i) }}\n </ng-template>\n </div>\n\n <div\n class=\"ngb-stepper-label-container\"\n [ngClass]=\"labelPositionClass\"\n >\n <div class=\"ngb-stepper-label\" *ngIf=\"stepLabelTpl; else textLabel\">\n <ng-container\n [ngTemplateOutlet]=\"stepLabelTpl\"\n [ngTemplateOutletContext]=\"{ index: i, step: step }\"\n ></ng-container>\n </div>\n <ng-template #textLabel>\n <div class=\"ngb-stepper-label-text\">\n {{ step.label || ('Step ' + (i + 1)) }}\n </div>\n </ng-template>\n <div class=\"ngb-stepper-optional\" *ngIf=\"step.optional\">\n {{ optionalLabel }}\n </div>\n </div>\n </li>\n </ol>\n </div>\n\n <!-- CONTENT -->\n <div class=\"ngb-stepper-content\" [class.ngb-stepper-content-top]=\"contentPosition === 'top'\">\n <ng-container *ngIf=\"lazy; else allContent\">\n <ng-container *ngFor=\"let index of [selectedIndex]\">\n <div\n class=\"fade-step\"\n [style.--ngb-stepper-animation-duration]=\"(disableAnimation ? 0 : animationDuration) + 'ms'\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"stepId(index)\"\n [attr.id]=\"contentId(index)\"\n >\n <ng-container *ngTemplateOutlet=\"stepContentTpl; context: { index: index }\"></ng-container>\n <div\n *ngIf=\"currentErrorMessage\"\n class=\"ngb-stepper-error\"\n role=\"alert\"\n >\n {{ currentErrorMessage }}\n </div>\n </div>\n </ng-container>\n </ng-container>\n\n <ng-template #allContent>\n <div\n class=\"fade-step\"\n [style.--ngb-stepper-animation-duration]=\"(disableAnimation ? 0 : animationDuration) + 'ms'\"\n role=\"tabpanel\"\n [attr.aria-labelledby]=\"stepId(selectedIndex)\"\n [attr.id]=\"contentId(selectedIndex)\"\n >\n <ng-container *ngTemplateOutlet=\"stepContentTpl; context: { index: selectedIndex }\"></ng-container>\n <div\n *ngIf=\"currentErrorMessage\"\n class=\"ngb-stepper-error\"\n role=\"alert\"\n >\n {{ currentErrorMessage }}\n </div>\n </div>\n </ng-template>\n </div>\n\n <!-- ACTIONS -->\n <div class=\"ngb-stepper-buttons\">\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"onCancel()\"\n >\n {{ cancelLabel }}\n </button>\n\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"onReset()\"\n >\n {{ resetLabel }}\n </button>\n\n <button\n type=\"button\"\n class=\"btn btn-sm btn-outline-secondary\"\n (click)=\"prev()\"\n [disabled]=\"selectedIndex <= 0 || !allowRevisit\"\n >\n {{ previousLabel }}\n </button>\n\n <button\n type=\"button\"\n class=\"btn btn-sm btn-primary\"\n (click)=\"next()\"\n [disabled]=\"selectedIndex >= steps.length - 1 || steps[selectedIndex]?.disabled\"\n >\n {{ nextLabel }}\n </button>\n </div>\n</div>\n", styles: [".ngb-stepper{display:flex;flex-direction:column}.ngb-stepper.horizontal .ngb-stepper-header{display:flex;overflow-x:auto}.ngb-stepper.vertical .ngb-stepper-header{display:block}.ngb-stepper-header{margin-bottom:1rem}.ngb-stepper-steps{list-style:none;padding:0;margin:0;display:flex;flex-wrap:nowrap}.ngb-stepper.vertical .ngb-stepper-steps{flex-direction:column}.ngb-stepper-step{position:relative;display:flex;align-items:center;cursor:pointer}.ngb-stepper.horizontal .ngb-stepper-step{padding:.5rem .75rem}.ngb-stepper.vertical .ngb-stepper-step{padding:.5rem 0}.ngb-stepper-step.disabled{cursor:default;opacity:.6}.ngb-stepper-indicator{display:inline-flex;align-items:center;justify-content:center;width:2rem;height:2rem;border-radius:50%;margin-right:.5rem;flex-shrink:0}.ngb-stepper.bootstrap .ngb-stepper-indicator{border:1px solid var(--bs-border-color, #dee2e6);background-color:var(--bs-body-bg, #fff)}.ngb-stepper.bootstrap .ngb-stepper-step.active .ngb-stepper-indicator{background-color:var(--bs-primary, #0d6efd);color:#fff}.ngb-stepper.bootstrap .ngb-stepper-step.done .ngb-stepper-indicator{background-color:var(--bs-success, #198754);color:#fff}.ngb-stepper.bootstrap .ngb-stepper-step.error .ngb-stepper-indicator{background-color:var(--bs-danger, #dc3545);color:#fff}.ngb-stepper-label-container{display:flex;flex-direction:column}.ngb-stepper-label-top{flex-direction:column-reverse}.ngb-stepper-label-bottom{flex-direction:column}.ngb-stepper-label-left{flex-direction:row-reverse}.ngb-stepper-label-right{flex-direction:row}.ngb-stepper-content{position:relative}.ngb-stepper-content-top{order:-1}.ngb-stepper-buttons{display:flex;justify-content:flex-end;gap:.5rem;margin-top:1rem}.ngb-stepper-error{color:var(--bs-danger, #dc3545);font-size:.875rem;margin-top:.25rem}.ngb-stepper-optional{font-size:.75rem;opacity:.7}.ngb-stepper .fade-step{animation:ngb-stepper-fade var(--ngb-stepper-animation-duration, .15s) ease-in-out;animation-fill-mode:both}@keyframes ngb-stepper-fade{0%{opacity:0;transform:translateY(.35rem)}to{opacity:1;transform:translateY(0)}}\n"] }]
|
|
1700
|
+
}], propDecorators: { steps: [{
|
|
1701
|
+
type: Input
|
|
1702
|
+
}], orientation: [{
|
|
1703
|
+
type: Input
|
|
1704
|
+
}], labelPosition: [{
|
|
1705
|
+
type: Input
|
|
1706
|
+
}], contentPosition: [{
|
|
1707
|
+
type: Input
|
|
1708
|
+
}], allowRevisit: [{
|
|
1709
|
+
type: Input
|
|
1710
|
+
}], lazy: [{
|
|
1711
|
+
type: Input
|
|
1712
|
+
}], theme: [{
|
|
1713
|
+
type: Input
|
|
1714
|
+
}], responsive: [{
|
|
1715
|
+
type: Input
|
|
1716
|
+
}], disableAnimation: [{
|
|
1717
|
+
type: Input
|
|
1718
|
+
}], animationDuration: [{
|
|
1719
|
+
type: Input
|
|
1720
|
+
}], nextLabel: [{
|
|
1721
|
+
type: Input
|
|
1722
|
+
}], previousLabel: [{
|
|
1723
|
+
type: Input
|
|
1724
|
+
}], resetLabel: [{
|
|
1725
|
+
type: Input
|
|
1726
|
+
}], cancelLabel: [{
|
|
1727
|
+
type: Input
|
|
1728
|
+
}], optionalLabel: [{
|
|
1729
|
+
type: Input
|
|
1730
|
+
}], stateIcons: [{
|
|
1731
|
+
type: Input
|
|
1732
|
+
}], selectedIndex: [{
|
|
1733
|
+
type: Input
|
|
1734
|
+
}], selectedIndexChange: [{
|
|
1735
|
+
type: Output
|
|
1736
|
+
}], selectionChange: [{
|
|
1737
|
+
type: Output
|
|
1738
|
+
}], nextClicked: [{
|
|
1739
|
+
type: Output
|
|
1740
|
+
}], prevClicked: [{
|
|
1741
|
+
type: Output
|
|
1742
|
+
}], resetClicked: [{
|
|
1743
|
+
type: Output
|
|
1744
|
+
}], cancelClicked: [{
|
|
1745
|
+
type: Output
|
|
1746
|
+
}], stepLabelTplDir: [{
|
|
1747
|
+
type: ContentChild,
|
|
1748
|
+
args: [NgbStepLabelDirective]
|
|
1749
|
+
}], customIconTpl: [{
|
|
1750
|
+
type: ContentChild,
|
|
1751
|
+
args: ['ngbStepperIcon', { read: TemplateRef }]
|
|
1752
|
+
}], stepContentTpl: [{
|
|
1753
|
+
type: ContentChild,
|
|
1754
|
+
args: ['stepContent', { read: TemplateRef }]
|
|
1755
|
+
}] } });
|
|
1756
|
+
|
|
1757
|
+
class NgbSplitterPaneComponent {
|
|
1758
|
+
templateRef;
|
|
1759
|
+
size;
|
|
1760
|
+
min;
|
|
1761
|
+
max;
|
|
1762
|
+
collapsible = false;
|
|
1763
|
+
collapsed = false;
|
|
1764
|
+
collapsedChange = new EventEmitter();
|
|
1765
|
+
toggleCollapsed(next) {
|
|
1766
|
+
const value = next ?? !this.collapsed;
|
|
1767
|
+
if (value !== this.collapsed) {
|
|
1768
|
+
this.collapsed = value;
|
|
1769
|
+
this.collapsedChange.emit(this.collapsed);
|
|
1770
|
+
}
|
|
1771
|
+
}
|
|
1772
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbSplitterPaneComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1773
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: NgbSplitterPaneComponent, isStandalone: true, selector: "ngb-splitter-pane", inputs: { size: "size", min: "min", max: "max", collapsible: "collapsible", collapsed: "collapsed" }, outputs: { collapsedChange: "collapsedChange" }, viewQueries: [{ propertyName: "templateRef", first: true, predicate: ["paneTpl"], descendants: true, static: true }], ngImport: i0, template: `<ng-template #paneTpl><ng-content></ng-content></ng-template>`, isInline: true });
|
|
1774
|
+
}
|
|
1775
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbSplitterPaneComponent, decorators: [{
|
|
1776
|
+
type: Component,
|
|
1777
|
+
args: [{
|
|
1778
|
+
selector: 'ngb-splitter-pane',
|
|
1779
|
+
standalone: true,
|
|
1780
|
+
template: `<ng-template #paneTpl><ng-content></ng-content></ng-template>`,
|
|
1781
|
+
}]
|
|
1782
|
+
}], propDecorators: { templateRef: [{
|
|
1783
|
+
type: ViewChild,
|
|
1784
|
+
args: ['paneTpl', { static: true }]
|
|
1785
|
+
}], size: [{
|
|
1786
|
+
type: Input
|
|
1787
|
+
}], min: [{
|
|
1788
|
+
type: Input
|
|
1789
|
+
}], max: [{
|
|
1790
|
+
type: Input
|
|
1791
|
+
}], collapsible: [{
|
|
1792
|
+
type: Input
|
|
1793
|
+
}], collapsed: [{
|
|
1794
|
+
type: Input
|
|
1795
|
+
}], collapsedChange: [{
|
|
1796
|
+
type: Output
|
|
1797
|
+
}] } });
|
|
1798
|
+
|
|
1799
|
+
class NgbSplitterComponent {
|
|
1800
|
+
panes;
|
|
1801
|
+
container;
|
|
1802
|
+
orientation = 'horizontal';
|
|
1803
|
+
handleThickness = 12;
|
|
1804
|
+
paneStates = [];
|
|
1805
|
+
resizeSub;
|
|
1806
|
+
currentDrag;
|
|
1807
|
+
ngAfterContentInit() {
|
|
1808
|
+
this.initStates();
|
|
1809
|
+
this.panes.changes.subscribe(() => this.initStates());
|
|
1810
|
+
}
|
|
1811
|
+
ngOnChanges(changes) {
|
|
1812
|
+
if (changes['orientation'] && !changes['orientation'].firstChange) {
|
|
1813
|
+
this.initStates();
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
ngOnDestroy() {
|
|
1817
|
+
this.detachDragListeners();
|
|
1818
|
+
}
|
|
1819
|
+
onWindowResize() {
|
|
1820
|
+
this.initStates();
|
|
1821
|
+
}
|
|
1822
|
+
startResize(index, event) {
|
|
1823
|
+
event.preventDefault();
|
|
1824
|
+
const total = this.getContainerSize();
|
|
1825
|
+
const sizes = this.computePaneSizes(total);
|
|
1826
|
+
this.currentDrag = {
|
|
1827
|
+
index,
|
|
1828
|
+
startPos: this.pointerPosition(event),
|
|
1829
|
+
startSizes: [sizes[index], sizes[index + 1]],
|
|
1830
|
+
};
|
|
1831
|
+
const move = (e) => this.onDrag(e);
|
|
1832
|
+
const up = () => this.stopDrag();
|
|
1833
|
+
document.addEventListener('mousemove', move);
|
|
1834
|
+
document.addEventListener('mouseup', up, { once: true });
|
|
1835
|
+
this.resizeSub = { move, up };
|
|
1836
|
+
}
|
|
1837
|
+
onHandleKeydown(index, event) {
|
|
1838
|
+
const isHorizontal = this.orientation === 'horizontal';
|
|
1839
|
+
const delta = this.handleThickness;
|
|
1840
|
+
if ((isHorizontal && (event.key === 'ArrowLeft' || event.key === 'ArrowRight')) ||
|
|
1841
|
+
(!isHorizontal && (event.key === 'ArrowUp' || event.key === 'ArrowDown'))) {
|
|
1842
|
+
event.preventDefault();
|
|
1843
|
+
const direction = event.key === 'ArrowLeft' || event.key === 'ArrowUp' ? -1 : 1;
|
|
1844
|
+
this.adjustPaneSizes(index, delta * direction);
|
|
1845
|
+
}
|
|
1846
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
1847
|
+
event.preventDefault();
|
|
1848
|
+
this.toggleFromHandle(index);
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
toggleFromHandle(index) {
|
|
1852
|
+
const panesArray = this.panes.toArray();
|
|
1853
|
+
const target = panesArray[index].collapsible ? panesArray[index] : panesArray[index + 1];
|
|
1854
|
+
if (target?.collapsible) {
|
|
1855
|
+
target.toggleCollapsed();
|
|
1856
|
+
this.initStates();
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
onDrag(event) {
|
|
1860
|
+
if (!this.currentDrag) {
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
event.preventDefault();
|
|
1864
|
+
const delta = this.pointerPosition(event) - this.currentDrag.startPos;
|
|
1865
|
+
this.adjustPaneSizes(this.currentDrag.index, delta, this.currentDrag.startSizes);
|
|
1866
|
+
}
|
|
1867
|
+
stopDrag() {
|
|
1868
|
+
this.detachDragListeners();
|
|
1869
|
+
this.currentDrag = undefined;
|
|
1870
|
+
}
|
|
1871
|
+
adjustPaneSizes(index, delta, startSizes) {
|
|
1872
|
+
const total = this.getContainerSize();
|
|
1873
|
+
const sizes = startSizes ?? this.computePaneSizes(total);
|
|
1874
|
+
const panesArray = this.panes.toArray();
|
|
1875
|
+
const first = panesArray[index];
|
|
1876
|
+
const second = panesArray[index + 1];
|
|
1877
|
+
if (first.collapsed || second.collapsed) {
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
let firstSize = sizes[0] + delta;
|
|
1881
|
+
let secondSize = sizes[1] - delta;
|
|
1882
|
+
const firstLimits = this.getLimits(first, total);
|
|
1883
|
+
const secondLimits = this.getLimits(second, total);
|
|
1884
|
+
firstSize = this.clamp(firstSize, firstLimits.minPx, firstLimits.maxPx);
|
|
1885
|
+
secondSize = this.clamp(secondSize, secondLimits.minPx, secondLimits.maxPx);
|
|
1886
|
+
if (firstSize + secondSize > 0) {
|
|
1887
|
+
this.paneStates[index].sizePx = firstSize;
|
|
1888
|
+
this.paneStates[index + 1].sizePx = secondSize;
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
initStates() {
|
|
1892
|
+
const total = this.getContainerSize();
|
|
1893
|
+
const sizes = this.computePaneSizes(total);
|
|
1894
|
+
const panesArray = this.panes.toArray();
|
|
1895
|
+
this.paneStates = panesArray.map((pane, idx) => {
|
|
1896
|
+
const limits = this.getLimits(pane, total);
|
|
1897
|
+
return {
|
|
1898
|
+
id: `ngb-splitter-pane-${idx}`,
|
|
1899
|
+
sizePx: pane.collapsed ? 0 : sizes[idx],
|
|
1900
|
+
minPx: limits.minPx,
|
|
1901
|
+
maxPx: limits.maxPx,
|
|
1902
|
+
};
|
|
1903
|
+
});
|
|
1904
|
+
}
|
|
1905
|
+
computePaneSizes(total) {
|
|
1906
|
+
const panesArray = this.panes?.toArray() ?? [];
|
|
1907
|
+
if (!panesArray.length)
|
|
1908
|
+
return [];
|
|
1909
|
+
const resolved = panesArray.map((pane) => pane.collapsed ? 0 : this.resolveSize(pane.size, total));
|
|
1910
|
+
const specifiedTotal = resolved.reduce((sum, val) => sum + (val ?? 0), 0);
|
|
1911
|
+
const unspecified = resolved.filter((v) => v == null).length;
|
|
1912
|
+
const remaining = Math.max(total - specifiedTotal, 0);
|
|
1913
|
+
const autoSize = unspecified > 0 ? remaining / unspecified : 0;
|
|
1914
|
+
return resolved.map((val) => {
|
|
1915
|
+
if (val != null) {
|
|
1916
|
+
return val;
|
|
1917
|
+
}
|
|
1918
|
+
if (autoSize) {
|
|
1919
|
+
return autoSize;
|
|
1920
|
+
}
|
|
1921
|
+
return total / panesArray.length;
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
resolveSize(value, total) {
|
|
1925
|
+
if (!value)
|
|
1926
|
+
return undefined;
|
|
1927
|
+
const trimmed = value.trim();
|
|
1928
|
+
if (trimmed.endsWith('%')) {
|
|
1929
|
+
const pct = Number.parseFloat(trimmed.slice(0, -1));
|
|
1930
|
+
return (pct / 100) * total;
|
|
1931
|
+
}
|
|
1932
|
+
if (trimmed.endsWith('px')) {
|
|
1933
|
+
return Number.parseFloat(trimmed.replace('px', ''));
|
|
1934
|
+
}
|
|
1935
|
+
const asNumber = Number.parseFloat(trimmed);
|
|
1936
|
+
return Number.isFinite(asNumber) ? asNumber : undefined;
|
|
1937
|
+
}
|
|
1938
|
+
getLimits(pane, total) {
|
|
1939
|
+
return {
|
|
1940
|
+
minPx: pane.collapsed ? 0 : this.resolveSize(pane.min, total),
|
|
1941
|
+
maxPx: pane.collapsed ? 0 : this.resolveSize(pane.max, total),
|
|
1942
|
+
};
|
|
1943
|
+
}
|
|
1944
|
+
clamp(value, min, max) {
|
|
1945
|
+
let next = value;
|
|
1946
|
+
if (min != null)
|
|
1947
|
+
next = Math.max(min, next);
|
|
1948
|
+
if (max != null)
|
|
1949
|
+
next = Math.min(max, next);
|
|
1950
|
+
return next;
|
|
1951
|
+
}
|
|
1952
|
+
pointerPosition(event) {
|
|
1953
|
+
return this.orientation === 'horizontal' ? event.clientX : event.clientY;
|
|
1954
|
+
}
|
|
1955
|
+
getContainerSize() {
|
|
1956
|
+
const rect = this.container?.nativeElement.getBoundingClientRect();
|
|
1957
|
+
return rect && this.orientation === 'horizontal' ? rect.width : rect?.height || 0;
|
|
1958
|
+
}
|
|
1959
|
+
detachDragListeners() {
|
|
1960
|
+
if (this.resizeSub) {
|
|
1961
|
+
document.removeEventListener('mousemove', this.resizeSub.move);
|
|
1962
|
+
document.removeEventListener('mouseup', this.resizeSub.up);
|
|
1963
|
+
this.resizeSub = undefined;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbSplitterComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
1967
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: NgbSplitterComponent, isStandalone: true, selector: "ngb-splitter", inputs: { orientation: "orientation", handleThickness: "handleThickness" }, host: { listeners: { "window:resize": "onWindowResize()" } }, queries: [{ propertyName: "panes", predicate: NgbSplitterPaneComponent }], viewQueries: [{ propertyName: "container", first: true, predicate: ["container"], descendants: true, static: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
1968
|
+
<div
|
|
1969
|
+
#container
|
|
1970
|
+
class="d-flex"
|
|
1971
|
+
[class.flex-row]="orientation === 'horizontal'"
|
|
1972
|
+
[class.flex-column]="orientation === 'vertical'"
|
|
1973
|
+
role="group"
|
|
1974
|
+
>
|
|
1975
|
+
<ng-container *ngFor="let pane of panes.toArray(); let i = index">
|
|
1976
|
+
<div
|
|
1977
|
+
class="d-flex flex-column overflow-auto"
|
|
1978
|
+
[attr.data-pane-id]="paneStates[i]?.id"
|
|
1979
|
+
[class.d-none]="pane.collapsed"
|
|
1980
|
+
[style.flex-basis.px]="paneStates[i]?.sizePx"
|
|
1981
|
+
[style.min-width.px]="orientation === 'horizontal' ? paneStates[i]?.minPx : undefined"
|
|
1982
|
+
[style.max-width.px]="orientation === 'horizontal' ? paneStates[i]?.maxPx : undefined"
|
|
1983
|
+
[style.min-height.px]="orientation === 'vertical' ? paneStates[i]?.minPx : undefined"
|
|
1984
|
+
[style.max-height.px]="orientation === 'vertical' ? paneStates[i]?.maxPx : undefined"
|
|
1985
|
+
>
|
|
1986
|
+
<ng-container *ngTemplateOutlet="pane.templateRef"></ng-container>
|
|
1987
|
+
</div>
|
|
1988
|
+
|
|
1989
|
+
<button
|
|
1990
|
+
*ngIf="i < panes.length - 1"
|
|
1991
|
+
type="button"
|
|
1992
|
+
class="bg-transparent border-0 p-0 d-flex align-items-center justify-content-center"
|
|
1993
|
+
[style.width.px]="orientation === 'horizontal' ? handleThickness : undefined"
|
|
1994
|
+
[style.height.px]="orientation === 'vertical' ? handleThickness : undefined"
|
|
1995
|
+
[class.w-100]="orientation === 'vertical'"
|
|
1996
|
+
[class.h-100]="orientation === 'horizontal'"
|
|
1997
|
+
role="separator"
|
|
1998
|
+
[attr.aria-orientation]="orientation"
|
|
1999
|
+
[attr.aria-controls]="paneStates[i]?.id + ' ' + paneStates[i + 1]?.id"
|
|
2000
|
+
tabindex="0"
|
|
2001
|
+
(mousedown)="startResize(i, $event)"
|
|
2002
|
+
(keydown)="onHandleKeydown(i, $event)"
|
|
2003
|
+
(dblclick)="toggleFromHandle(i)"
|
|
2004
|
+
aria-label="Splitter resize handle"
|
|
2005
|
+
>
|
|
2006
|
+
<span class="border bg-light" [style.width.px]="orientation === 'horizontal' ? 2 : handleThickness" [style.height.px]="orientation === 'vertical' ? 2 : handleThickness"></span>
|
|
2007
|
+
</button>
|
|
2008
|
+
</ng-container>
|
|
2009
|
+
</div>
|
|
2010
|
+
`, isInline: true, 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2011
|
+
}
|
|
2012
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbSplitterComponent, decorators: [{
|
|
2013
|
+
type: Component,
|
|
2014
|
+
args: [{
|
|
2015
|
+
selector: 'ngb-splitter',
|
|
2016
|
+
standalone: true,
|
|
2017
|
+
imports: [CommonModule, NgbSplitterPaneComponent],
|
|
2018
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2019
|
+
template: `
|
|
2020
|
+
<div
|
|
2021
|
+
#container
|
|
2022
|
+
class="d-flex"
|
|
2023
|
+
[class.flex-row]="orientation === 'horizontal'"
|
|
2024
|
+
[class.flex-column]="orientation === 'vertical'"
|
|
2025
|
+
role="group"
|
|
2026
|
+
>
|
|
2027
|
+
<ng-container *ngFor="let pane of panes.toArray(); let i = index">
|
|
2028
|
+
<div
|
|
2029
|
+
class="d-flex flex-column overflow-auto"
|
|
2030
|
+
[attr.data-pane-id]="paneStates[i]?.id"
|
|
2031
|
+
[class.d-none]="pane.collapsed"
|
|
2032
|
+
[style.flex-basis.px]="paneStates[i]?.sizePx"
|
|
2033
|
+
[style.min-width.px]="orientation === 'horizontal' ? paneStates[i]?.minPx : undefined"
|
|
2034
|
+
[style.max-width.px]="orientation === 'horizontal' ? paneStates[i]?.maxPx : undefined"
|
|
2035
|
+
[style.min-height.px]="orientation === 'vertical' ? paneStates[i]?.minPx : undefined"
|
|
2036
|
+
[style.max-height.px]="orientation === 'vertical' ? paneStates[i]?.maxPx : undefined"
|
|
2037
|
+
>
|
|
2038
|
+
<ng-container *ngTemplateOutlet="pane.templateRef"></ng-container>
|
|
2039
|
+
</div>
|
|
2040
|
+
|
|
2041
|
+
<button
|
|
2042
|
+
*ngIf="i < panes.length - 1"
|
|
2043
|
+
type="button"
|
|
2044
|
+
class="bg-transparent border-0 p-0 d-flex align-items-center justify-content-center"
|
|
2045
|
+
[style.width.px]="orientation === 'horizontal' ? handleThickness : undefined"
|
|
2046
|
+
[style.height.px]="orientation === 'vertical' ? handleThickness : undefined"
|
|
2047
|
+
[class.w-100]="orientation === 'vertical'"
|
|
2048
|
+
[class.h-100]="orientation === 'horizontal'"
|
|
2049
|
+
role="separator"
|
|
2050
|
+
[attr.aria-orientation]="orientation"
|
|
2051
|
+
[attr.aria-controls]="paneStates[i]?.id + ' ' + paneStates[i + 1]?.id"
|
|
2052
|
+
tabindex="0"
|
|
2053
|
+
(mousedown)="startResize(i, $event)"
|
|
2054
|
+
(keydown)="onHandleKeydown(i, $event)"
|
|
2055
|
+
(dblclick)="toggleFromHandle(i)"
|
|
2056
|
+
aria-label="Splitter resize handle"
|
|
2057
|
+
>
|
|
2058
|
+
<span class="border bg-light" [style.width.px]="orientation === 'horizontal' ? 2 : handleThickness" [style.height.px]="orientation === 'vertical' ? 2 : handleThickness"></span>
|
|
2059
|
+
</button>
|
|
2060
|
+
</ng-container>
|
|
2061
|
+
</div>
|
|
2062
|
+
`,
|
|
2063
|
+
}]
|
|
2064
|
+
}], propDecorators: { panes: [{
|
|
2065
|
+
type: ContentChildren,
|
|
2066
|
+
args: [NgbSplitterPaneComponent]
|
|
2067
|
+
}], container: [{
|
|
2068
|
+
type: ViewChild,
|
|
2069
|
+
args: ['container', { static: true }]
|
|
2070
|
+
}], orientation: [{
|
|
2071
|
+
type: Input
|
|
2072
|
+
}], handleThickness: [{
|
|
2073
|
+
type: Input
|
|
2074
|
+
}], onWindowResize: [{
|
|
2075
|
+
type: HostListener,
|
|
2076
|
+
args: ['window:resize']
|
|
2077
|
+
}] } });
|
|
2078
|
+
|
|
2079
|
+
class NgbTypeaheadComponent {
|
|
2080
|
+
data = [];
|
|
2081
|
+
debounceTime = 200;
|
|
2082
|
+
characterTyped = 1;
|
|
2083
|
+
limit = 10;
|
|
2084
|
+
selectExact = false;
|
|
2085
|
+
multiSelect = false;
|
|
2086
|
+
i18n;
|
|
2087
|
+
itemTemplate;
|
|
2088
|
+
selectedItems = new EventEmitter();
|
|
2089
|
+
selectionChange = new EventEmitter();
|
|
2090
|
+
onChange = new EventEmitter();
|
|
2091
|
+
onScrollEvent = new EventEmitter();
|
|
2092
|
+
scroller;
|
|
2093
|
+
inputEl;
|
|
2094
|
+
itemButtons;
|
|
2095
|
+
query = '';
|
|
2096
|
+
filtered = [];
|
|
2097
|
+
visible = [];
|
|
2098
|
+
selected = [];
|
|
2099
|
+
viewportHeight = 240;
|
|
2100
|
+
itemHeight = 40;
|
|
2101
|
+
beforePadding = 0;
|
|
2102
|
+
afterPadding = 0;
|
|
2103
|
+
debounceId;
|
|
2104
|
+
ngAfterViewInit() {
|
|
2105
|
+
this.applyFilter('');
|
|
2106
|
+
}
|
|
2107
|
+
ngOnChanges(changes) {
|
|
2108
|
+
if (changes['data'] && !changes['data'].firstChange) {
|
|
2109
|
+
this.applyFilter(this.query);
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
ngOnDestroy() {
|
|
2113
|
+
if (this.debounceId) {
|
|
2114
|
+
clearTimeout(this.debounceId);
|
|
2115
|
+
}
|
|
2116
|
+
}
|
|
2117
|
+
get selectionSummary() {
|
|
2118
|
+
if (!this.selected.length)
|
|
2119
|
+
return '';
|
|
2120
|
+
if (this.selected.length <= 2) {
|
|
2121
|
+
return this.selected.map((s) => s.label).join(', ');
|
|
2122
|
+
}
|
|
2123
|
+
const firstTwo = this.selected.slice(0, 2).map((s) => s.label).join(', ');
|
|
2124
|
+
return `${firstTwo} (+${this.selected.length - 2})`;
|
|
2125
|
+
}
|
|
2126
|
+
onInput(value) {
|
|
2127
|
+
this.query = value;
|
|
2128
|
+
this.onChange.emit(value);
|
|
2129
|
+
if (this.debounceId)
|
|
2130
|
+
clearTimeout(this.debounceId);
|
|
2131
|
+
if (this.debounceTime <= 0) {
|
|
2132
|
+
this.applyFilter(value);
|
|
2133
|
+
}
|
|
2134
|
+
else {
|
|
2135
|
+
this.debounceId = setTimeout(() => this.applyFilter(value), this.debounceTime);
|
|
2136
|
+
}
|
|
2137
|
+
}
|
|
2138
|
+
onKeydown(event) {
|
|
2139
|
+
if (event.key === 'ArrowDown' || event.key === 'ArrowUp') {
|
|
2140
|
+
event.preventDefault();
|
|
2141
|
+
this.focusSibling(event.key === 'ArrowDown' ? 1 : -1);
|
|
2142
|
+
}
|
|
2143
|
+
else if (event.key === 'Enter') {
|
|
2144
|
+
event.preventDefault();
|
|
2145
|
+
const first = this.visible[0];
|
|
2146
|
+
if (first)
|
|
2147
|
+
this.selectItem(first);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
onScroll(event) {
|
|
2151
|
+
const target = event.target;
|
|
2152
|
+
const scrollTop = target.scrollTop;
|
|
2153
|
+
this.onScrollEvent.emit();
|
|
2154
|
+
this.updateViewport(scrollTop);
|
|
2155
|
+
}
|
|
2156
|
+
selectItem(item) {
|
|
2157
|
+
if (!item || item.disabled)
|
|
2158
|
+
return;
|
|
2159
|
+
this.validateItem(item);
|
|
2160
|
+
if (this.multiSelect) {
|
|
2161
|
+
const exists = this.selected.find((s) => s.id === item.id);
|
|
2162
|
+
if (exists) {
|
|
2163
|
+
this.selected = this.selected.filter((s) => s.id !== item.id);
|
|
2164
|
+
}
|
|
2165
|
+
else {
|
|
2166
|
+
this.selected = [...this.selected, item];
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
else {
|
|
2170
|
+
this.selected = [item];
|
|
2171
|
+
}
|
|
2172
|
+
this.selectedItems.emit(this.selected);
|
|
2173
|
+
this.selectionChange.emit(this.selected);
|
|
2174
|
+
}
|
|
2175
|
+
clearSelection() {
|
|
2176
|
+
this.selected = [];
|
|
2177
|
+
this.selectedItems.emit(this.selected);
|
|
2178
|
+
this.selectionChange.emit(this.selected);
|
|
2179
|
+
}
|
|
2180
|
+
isSelected(item) {
|
|
2181
|
+
return this.selected.some((s) => s.id === item.id);
|
|
2182
|
+
}
|
|
2183
|
+
trackById = (_, item) => item.id;
|
|
2184
|
+
applyFilter(value) {
|
|
2185
|
+
const term = (value || '').toLowerCase();
|
|
2186
|
+
const meetsThreshold = term.length >= this.characterTyped;
|
|
2187
|
+
const base = meetsThreshold ? this.data.filter((d) => (d.label || '').toLowerCase().includes(term)) : [];
|
|
2188
|
+
const limited = this.limit ? base.slice(0, this.limit) : base;
|
|
2189
|
+
this.filtered = limited;
|
|
2190
|
+
if (this.selectExact && limited.length === 1) {
|
|
2191
|
+
this.selectItem(limited[0]);
|
|
2192
|
+
}
|
|
2193
|
+
this.updateViewport(this.scroller?.nativeElement.scrollTop || 0);
|
|
2194
|
+
}
|
|
2195
|
+
updateViewport(scrollTop) {
|
|
2196
|
+
const total = this.filtered.length;
|
|
2197
|
+
const visibleCount = Math.ceil(this.viewportHeight / this.itemHeight) + 2;
|
|
2198
|
+
const start = Math.max(Math.floor(scrollTop / this.itemHeight), 0);
|
|
2199
|
+
const end = Math.min(start + visibleCount, total);
|
|
2200
|
+
this.beforePadding = start * this.itemHeight;
|
|
2201
|
+
this.afterPadding = Math.max(total - end, 0) * this.itemHeight;
|
|
2202
|
+
this.visible = this.filtered.slice(start, end);
|
|
2203
|
+
}
|
|
2204
|
+
focusSibling(direction) {
|
|
2205
|
+
const buttons = this.scroller?.nativeElement.querySelectorAll('button.list-group-item');
|
|
2206
|
+
if (!buttons || buttons.length === 0)
|
|
2207
|
+
return;
|
|
2208
|
+
const active = document.activeElement;
|
|
2209
|
+
const index = Array.prototype.indexOf.call(buttons, active);
|
|
2210
|
+
const next = buttons[index + direction] || buttons[(index + direction + buttons.length) % buttons.length];
|
|
2211
|
+
next?.focus();
|
|
2212
|
+
}
|
|
2213
|
+
validateItem(item) {
|
|
2214
|
+
if (this.itemTemplate) {
|
|
2215
|
+
if (item.id == null || (item.value == null && item.value !== 0)) {
|
|
2216
|
+
throw new Error('Custom templates require items to provide both id and value.');
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
}
|
|
2220
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbTypeaheadComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2221
|
+
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", i18n: "i18n" }, outputs: { selectedItems: "selectedItems", selectionChange: "selectionChange", onChange: "onChange", onScrollEvent: "onScrollEvent" }, queries: [{ propertyName: "itemTemplate", first: true, predicate: TemplateRef, descendants: true }], viewQueries: [{ propertyName: "scroller", first: true, predicate: ["scroller"], descendants: true, static: true }, { propertyName: "inputEl", first: true, predicate: ["inputEl"], descendants: true, static: true }, { propertyName: "itemButtons", predicate: ["itemButton"], descendants: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
2222
|
+
<div class="mb-2">
|
|
2223
|
+
<input
|
|
2224
|
+
#inputEl
|
|
2225
|
+
type="text"
|
|
2226
|
+
class="form-control"
|
|
2227
|
+
[attr.placeholder]="i18n?.placeholder || 'Type to search'"
|
|
2228
|
+
[ngModel]="query"
|
|
2229
|
+
(ngModelChange)="onInput($event)"
|
|
2230
|
+
(keydown)="onKeydown($event)"
|
|
2231
|
+
aria-autocomplete="list"
|
|
2232
|
+
role="combobox"
|
|
2233
|
+
[attr.aria-expanded]="filtered.length > 0"
|
|
2234
|
+
/>
|
|
2235
|
+
</div>
|
|
2236
|
+
|
|
2237
|
+
<div
|
|
2238
|
+
#scroller
|
|
2239
|
+
class="border rounded list-group"
|
|
2240
|
+
style="max-height: 240px; overflow: auto;"
|
|
2241
|
+
role="listbox"
|
|
2242
|
+
(scroll)="onScroll($event)"
|
|
2243
|
+
>
|
|
2244
|
+
<div [style.height.px]="beforePadding"></div>
|
|
2245
|
+
<button
|
|
2246
|
+
*ngFor="let item of visible; trackBy: trackById"
|
|
2247
|
+
type="button"
|
|
2248
|
+
class="list-group-item list-group-item-action d-flex align-items-center justify-content-between"
|
|
2249
|
+
[class.active]="isSelected(item)"
|
|
2250
|
+
[attr.aria-selected]="isSelected(item)"
|
|
2251
|
+
(click)="selectItem(item)"
|
|
2252
|
+
[disabled]="item.disabled"
|
|
2253
|
+
>
|
|
2254
|
+
<ng-container *ngIf="itemTemplate; else defaultTpl" [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item }"></ng-container>
|
|
2255
|
+
<ng-template #defaultTpl>
|
|
2256
|
+
<span>{{ item.label }}</span>
|
|
2257
|
+
<span *ngIf="multiSelect && isSelected(item)" class="badge bg-secondary">✓</span>
|
|
2258
|
+
</ng-template>
|
|
2259
|
+
</button>
|
|
2260
|
+
<div [style.height.px]="afterPadding"></div>
|
|
2261
|
+
|
|
2262
|
+
<div *ngIf="!filtered.length" class="list-group-item text-muted text-center">
|
|
2263
|
+
{{ i18n?.noResults || 'No results' }}
|
|
2264
|
+
</div>
|
|
2265
|
+
</div>
|
|
2266
|
+
|
|
2267
|
+
<div *ngIf="multiSelect && selected.length" class="mt-2 small text-muted">
|
|
2268
|
+
{{ selectionSummary }}
|
|
2269
|
+
<button type="button" class="btn btn-link btn-sm ps-1" (click)="clearSelection()">
|
|
2270
|
+
{{ i18n?.clearSelection || 'Clear' }}
|
|
2271
|
+
</button>
|
|
2272
|
+
</div>
|
|
2273
|
+
`, isInline: true, 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: "ngmodule", type: FormsModule }, { 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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2274
|
+
}
|
|
2275
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbTypeaheadComponent, decorators: [{
|
|
2276
|
+
type: Component,
|
|
2277
|
+
args: [{
|
|
2278
|
+
selector: 'ngb-typeahead',
|
|
2279
|
+
standalone: true,
|
|
2280
|
+
imports: [CommonModule, FormsModule],
|
|
2281
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2282
|
+
template: `
|
|
2283
|
+
<div class="mb-2">
|
|
2284
|
+
<input
|
|
2285
|
+
#inputEl
|
|
2286
|
+
type="text"
|
|
2287
|
+
class="form-control"
|
|
2288
|
+
[attr.placeholder]="i18n?.placeholder || 'Type to search'"
|
|
2289
|
+
[ngModel]="query"
|
|
2290
|
+
(ngModelChange)="onInput($event)"
|
|
2291
|
+
(keydown)="onKeydown($event)"
|
|
2292
|
+
aria-autocomplete="list"
|
|
2293
|
+
role="combobox"
|
|
2294
|
+
[attr.aria-expanded]="filtered.length > 0"
|
|
2295
|
+
/>
|
|
2296
|
+
</div>
|
|
2297
|
+
|
|
2298
|
+
<div
|
|
2299
|
+
#scroller
|
|
2300
|
+
class="border rounded list-group"
|
|
2301
|
+
style="max-height: 240px; overflow: auto;"
|
|
2302
|
+
role="listbox"
|
|
2303
|
+
(scroll)="onScroll($event)"
|
|
2304
|
+
>
|
|
2305
|
+
<div [style.height.px]="beforePadding"></div>
|
|
2306
|
+
<button
|
|
2307
|
+
*ngFor="let item of visible; trackBy: trackById"
|
|
2308
|
+
type="button"
|
|
2309
|
+
class="list-group-item list-group-item-action d-flex align-items-center justify-content-between"
|
|
2310
|
+
[class.active]="isSelected(item)"
|
|
2311
|
+
[attr.aria-selected]="isSelected(item)"
|
|
2312
|
+
(click)="selectItem(item)"
|
|
2313
|
+
[disabled]="item.disabled"
|
|
2314
|
+
>
|
|
2315
|
+
<ng-container *ngIf="itemTemplate; else defaultTpl" [ngTemplateOutlet]="itemTemplate" [ngTemplateOutletContext]="{ $implicit: item }"></ng-container>
|
|
2316
|
+
<ng-template #defaultTpl>
|
|
2317
|
+
<span>{{ item.label }}</span>
|
|
2318
|
+
<span *ngIf="multiSelect && isSelected(item)" class="badge bg-secondary">✓</span>
|
|
2319
|
+
</ng-template>
|
|
2320
|
+
</button>
|
|
2321
|
+
<div [style.height.px]="afterPadding"></div>
|
|
2322
|
+
|
|
2323
|
+
<div *ngIf="!filtered.length" class="list-group-item text-muted text-center">
|
|
2324
|
+
{{ i18n?.noResults || 'No results' }}
|
|
2325
|
+
</div>
|
|
2326
|
+
</div>
|
|
2327
|
+
|
|
2328
|
+
<div *ngIf="multiSelect && selected.length" class="mt-2 small text-muted">
|
|
2329
|
+
{{ selectionSummary }}
|
|
2330
|
+
<button type="button" class="btn btn-link btn-sm ps-1" (click)="clearSelection()">
|
|
2331
|
+
{{ i18n?.clearSelection || 'Clear' }}
|
|
2332
|
+
</button>
|
|
2333
|
+
</div>
|
|
2334
|
+
`,
|
|
2335
|
+
}]
|
|
2336
|
+
}], propDecorators: { data: [{
|
|
2337
|
+
type: Input
|
|
2338
|
+
}], debounceTime: [{
|
|
2339
|
+
type: Input
|
|
2340
|
+
}], characterTyped: [{
|
|
2341
|
+
type: Input
|
|
2342
|
+
}], limit: [{
|
|
2343
|
+
type: Input
|
|
2344
|
+
}], selectExact: [{
|
|
2345
|
+
type: Input
|
|
2346
|
+
}], multiSelect: [{
|
|
2347
|
+
type: Input
|
|
2348
|
+
}], i18n: [{
|
|
2349
|
+
type: Input
|
|
2350
|
+
}], itemTemplate: [{
|
|
2351
|
+
type: ContentChild,
|
|
2352
|
+
args: [TemplateRef]
|
|
2353
|
+
}], selectedItems: [{
|
|
2354
|
+
type: Output
|
|
2355
|
+
}], selectionChange: [{
|
|
2356
|
+
type: Output
|
|
2357
|
+
}], onChange: [{
|
|
2358
|
+
type: Output
|
|
2359
|
+
}], onScrollEvent: [{
|
|
2360
|
+
type: Output
|
|
2361
|
+
}], scroller: [{
|
|
2362
|
+
type: ViewChild,
|
|
2363
|
+
args: ['scroller', { static: true }]
|
|
2364
|
+
}], inputEl: [{
|
|
2365
|
+
type: ViewChild,
|
|
2366
|
+
args: ['inputEl', { static: true }]
|
|
2367
|
+
}], itemButtons: [{
|
|
2368
|
+
type: ViewChildren,
|
|
2369
|
+
args: ['itemButton']
|
|
2370
|
+
}] } });
|
|
2371
|
+
|
|
2372
|
+
class NgbTreeComponent {
|
|
2373
|
+
nodes = [];
|
|
2374
|
+
type = 'text';
|
|
2375
|
+
showCheckbox = false;
|
|
2376
|
+
i18n;
|
|
2377
|
+
expand = new EventEmitter();
|
|
2378
|
+
collapse = new EventEmitter();
|
|
2379
|
+
expandAll = new EventEmitter();
|
|
2380
|
+
collapseAll = new EventEmitter();
|
|
2381
|
+
selectionChange = new EventEmitter();
|
|
2382
|
+
nodeButtons;
|
|
2383
|
+
trackById = (_, node) => node.id;
|
|
2384
|
+
hasChildren(node) {
|
|
2385
|
+
return Array.isArray(node.children) && node.children.length > 0;
|
|
2386
|
+
}
|
|
2387
|
+
toggleNode(node) {
|
|
2388
|
+
if (!this.hasChildren(node))
|
|
2389
|
+
return;
|
|
2390
|
+
node.expanded = !node.expanded;
|
|
2391
|
+
node.expanded ? this.expand.emit(node) : this.collapse.emit(node);
|
|
2392
|
+
}
|
|
2393
|
+
toggleSelection(node, value) {
|
|
2394
|
+
node.selected = value;
|
|
2395
|
+
this.selectionChange.emit(this.collectSelection(this.nodes));
|
|
2396
|
+
}
|
|
2397
|
+
expandAllNodes() {
|
|
2398
|
+
this.setExpanded(this.nodes, true);
|
|
2399
|
+
this.expandAll.emit();
|
|
2400
|
+
}
|
|
2401
|
+
collapseAllNodes() {
|
|
2402
|
+
this.setExpanded(this.nodes, false);
|
|
2403
|
+
this.collapseAll.emit();
|
|
2404
|
+
}
|
|
2405
|
+
onNodeKeydown(event, node) {
|
|
2406
|
+
const key = event.key;
|
|
2407
|
+
const target = event.target;
|
|
2408
|
+
if (key === 'Enter' || key === ' ') {
|
|
2409
|
+
event.preventDefault();
|
|
2410
|
+
this.toggleNode(node);
|
|
2411
|
+
}
|
|
2412
|
+
else if (key === 'ArrowRight') {
|
|
2413
|
+
if (this.hasChildren(node) && !node.expanded) {
|
|
2414
|
+
event.preventDefault();
|
|
2415
|
+
node.expanded = true;
|
|
2416
|
+
this.expand.emit(node);
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
else if (key === 'ArrowLeft') {
|
|
2420
|
+
if (this.hasChildren(node) && node.expanded) {
|
|
2421
|
+
event.preventDefault();
|
|
2422
|
+
node.expanded = false;
|
|
2423
|
+
this.collapse.emit(node);
|
|
2424
|
+
}
|
|
2425
|
+
}
|
|
2426
|
+
else if (key === 'ArrowDown' || key === 'ArrowUp') {
|
|
2427
|
+
event.preventDefault();
|
|
2428
|
+
this.focusSibling(target, key === 'ArrowDown' ? 1 : -1);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
focusSibling(current, direction) {
|
|
2432
|
+
const buttons = this.nodeButtons?.toArray().map((b) => b.nativeElement) || [];
|
|
2433
|
+
const idx = buttons.indexOf(current);
|
|
2434
|
+
if (idx === -1)
|
|
2435
|
+
return;
|
|
2436
|
+
const next = buttons[idx + direction];
|
|
2437
|
+
next?.focus();
|
|
2438
|
+
}
|
|
2439
|
+
setExpanded(nodes, expanded) {
|
|
2440
|
+
for (const n of nodes) {
|
|
2441
|
+
if (this.hasChildren(n)) {
|
|
2442
|
+
n.expanded = expanded;
|
|
2443
|
+
this.setExpanded(n.children || [], expanded);
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
}
|
|
2447
|
+
collectSelection(nodes, acc = []) {
|
|
2448
|
+
for (const n of nodes) {
|
|
2449
|
+
if (n.selected)
|
|
2450
|
+
acc.push(n);
|
|
2451
|
+
if (this.hasChildren(n))
|
|
2452
|
+
this.collectSelection(n.children, acc);
|
|
2453
|
+
}
|
|
2454
|
+
return acc;
|
|
2455
|
+
}
|
|
2456
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbTreeComponent, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
2457
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "21.0.6", type: NgbTreeComponent, isStandalone: true, selector: "ngb-tree", inputs: { nodes: "nodes", type: "type", showCheckbox: "showCheckbox", i18n: "i18n" }, outputs: { expand: "expand", collapse: "collapse", expandAll: "expandAll", collapseAll: "collapseAll", selectionChange: "selectionChange" }, viewQueries: [{ propertyName: "nodeButtons", predicate: ["nodeBtn"], descendants: true, read: ElementRef }], ngImport: i0, template: `
|
|
2458
|
+
<div class="d-flex gap-2 mb-2">
|
|
2459
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="expandAllNodes()" [attr.aria-label]="i18n?.expandAll || 'Expand all'">
|
|
2460
|
+
{{ i18n?.expandAll || 'Expand all' }}
|
|
2461
|
+
</button>
|
|
2462
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="collapseAllNodes()" [attr.aria-label]="i18n?.collapseAll || 'Collapse all'">
|
|
2463
|
+
{{ i18n?.collapseAll || 'Collapse all' }}
|
|
2464
|
+
</button>
|
|
2465
|
+
</div>
|
|
2466
|
+
|
|
2467
|
+
<ul class="list-unstyled mb-0" role="tree" [attr.aria-label]="i18n?.treeLabel || 'Tree'">
|
|
2468
|
+
<ng-container *ngFor="let node of nodes; let i = index; trackBy: trackById">
|
|
2469
|
+
<ng-container *ngTemplateOutlet="renderNode; context: { $implicit: node, level: 1, index: i }"></ng-container>
|
|
2470
|
+
</ng-container>
|
|
2471
|
+
</ul>
|
|
2472
|
+
|
|
2473
|
+
<ng-template #renderNode let-node let-level="level" let-index="index">
|
|
2474
|
+
<li
|
|
2475
|
+
class="d-flex align-items-start"
|
|
2476
|
+
role="treeitem"
|
|
2477
|
+
[attr.aria-expanded]="hasChildren(node) ? !!node.expanded : null"
|
|
2478
|
+
[attr.aria-level]="level"
|
|
2479
|
+
>
|
|
2480
|
+
<button
|
|
2481
|
+
#nodeBtn
|
|
2482
|
+
type="button"
|
|
2483
|
+
class="btn btn-sm btn-link px-1"
|
|
2484
|
+
[disabled]="!hasChildren(node)"
|
|
2485
|
+
(click)="toggleNode(node)"
|
|
2486
|
+
(keydown)="onNodeKeydown($event, node)"
|
|
2487
|
+
[attr.aria-label]="(node.expanded ? i18n?.collapse || 'Collapse' : i18n?.expand || 'Expand') + ' ' + node.label"
|
|
2488
|
+
>
|
|
2489
|
+
<span *ngIf="hasChildren(node)">
|
|
2490
|
+
<span *ngIf="type === 'json'">{{ node.expanded ? '-' : '+' }}</span>
|
|
2491
|
+
<span *ngIf="type !== 'json'">{{ node.expanded ? '▾' : '▸' }}</span>
|
|
2492
|
+
</span>
|
|
2493
|
+
<span *ngIf="!hasChildren(node)" class="text-muted">•</span>
|
|
2494
|
+
</button>
|
|
2495
|
+
|
|
2496
|
+
<label class="form-check d-inline-flex align-items-center gap-2 flex-grow-1 mb-0">
|
|
2497
|
+
<input
|
|
2498
|
+
*ngIf="showCheckbox"
|
|
2499
|
+
type="checkbox"
|
|
2500
|
+
class="form-check-input"
|
|
2501
|
+
[checked]="!!node.selected"
|
|
2502
|
+
[disabled]="node.disabled"
|
|
2503
|
+
(change)="toggleSelection(node, $event.target.checked)"
|
|
2504
|
+
[attr.aria-label]="(i18n?.select || 'Select') + ' ' + node.label"
|
|
2505
|
+
/>
|
|
2506
|
+
<span class="form-check-label">{{ node.label }}</span>
|
|
2507
|
+
</label>
|
|
2508
|
+
</li>
|
|
2509
|
+
|
|
2510
|
+
<ul
|
|
2511
|
+
*ngIf="node.expanded && hasChildren(node)"
|
|
2512
|
+
class="list-unstyled ms-4"
|
|
2513
|
+
role="group"
|
|
2514
|
+
>
|
|
2515
|
+
<ng-container *ngFor="let child of node.children; let ci = index; trackBy: trackById">
|
|
2516
|
+
<ng-container *ngTemplateOutlet="renderNode; context: { $implicit: child, level: level + 1, index: ci }"></ng-container>
|
|
2517
|
+
</ng-container>
|
|
2518
|
+
</ul>
|
|
2519
|
+
</ng-template>
|
|
2520
|
+
`, isInline: true, 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"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush });
|
|
2521
|
+
}
|
|
2522
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.6", ngImport: i0, type: NgbTreeComponent, decorators: [{
|
|
2523
|
+
type: Component,
|
|
2524
|
+
args: [{
|
|
2525
|
+
selector: 'ngb-tree',
|
|
2526
|
+
standalone: true,
|
|
2527
|
+
imports: [CommonModule],
|
|
2528
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
2529
|
+
template: `
|
|
2530
|
+
<div class="d-flex gap-2 mb-2">
|
|
2531
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="expandAllNodes()" [attr.aria-label]="i18n?.expandAll || 'Expand all'">
|
|
2532
|
+
{{ i18n?.expandAll || 'Expand all' }}
|
|
2533
|
+
</button>
|
|
2534
|
+
<button type="button" class="btn btn-sm btn-outline-secondary" (click)="collapseAllNodes()" [attr.aria-label]="i18n?.collapseAll || 'Collapse all'">
|
|
2535
|
+
{{ i18n?.collapseAll || 'Collapse all' }}
|
|
2536
|
+
</button>
|
|
2537
|
+
</div>
|
|
2538
|
+
|
|
2539
|
+
<ul class="list-unstyled mb-0" role="tree" [attr.aria-label]="i18n?.treeLabel || 'Tree'">
|
|
2540
|
+
<ng-container *ngFor="let node of nodes; let i = index; trackBy: trackById">
|
|
2541
|
+
<ng-container *ngTemplateOutlet="renderNode; context: { $implicit: node, level: 1, index: i }"></ng-container>
|
|
2542
|
+
</ng-container>
|
|
2543
|
+
</ul>
|
|
2544
|
+
|
|
2545
|
+
<ng-template #renderNode let-node let-level="level" let-index="index">
|
|
2546
|
+
<li
|
|
2547
|
+
class="d-flex align-items-start"
|
|
2548
|
+
role="treeitem"
|
|
2549
|
+
[attr.aria-expanded]="hasChildren(node) ? !!node.expanded : null"
|
|
2550
|
+
[attr.aria-level]="level"
|
|
2551
|
+
>
|
|
2552
|
+
<button
|
|
2553
|
+
#nodeBtn
|
|
2554
|
+
type="button"
|
|
2555
|
+
class="btn btn-sm btn-link px-1"
|
|
2556
|
+
[disabled]="!hasChildren(node)"
|
|
2557
|
+
(click)="toggleNode(node)"
|
|
2558
|
+
(keydown)="onNodeKeydown($event, node)"
|
|
2559
|
+
[attr.aria-label]="(node.expanded ? i18n?.collapse || 'Collapse' : i18n?.expand || 'Expand') + ' ' + node.label"
|
|
2560
|
+
>
|
|
2561
|
+
<span *ngIf="hasChildren(node)">
|
|
2562
|
+
<span *ngIf="type === 'json'">{{ node.expanded ? '-' : '+' }}</span>
|
|
2563
|
+
<span *ngIf="type !== 'json'">{{ node.expanded ? '▾' : '▸' }}</span>
|
|
2564
|
+
</span>
|
|
2565
|
+
<span *ngIf="!hasChildren(node)" class="text-muted">•</span>
|
|
2566
|
+
</button>
|
|
2567
|
+
|
|
2568
|
+
<label class="form-check d-inline-flex align-items-center gap-2 flex-grow-1 mb-0">
|
|
2569
|
+
<input
|
|
2570
|
+
*ngIf="showCheckbox"
|
|
2571
|
+
type="checkbox"
|
|
2572
|
+
class="form-check-input"
|
|
2573
|
+
[checked]="!!node.selected"
|
|
2574
|
+
[disabled]="node.disabled"
|
|
2575
|
+
(change)="toggleSelection(node, $event.target.checked)"
|
|
2576
|
+
[attr.aria-label]="(i18n?.select || 'Select') + ' ' + node.label"
|
|
2577
|
+
/>
|
|
2578
|
+
<span class="form-check-label">{{ node.label }}</span>
|
|
2579
|
+
</label>
|
|
2580
|
+
</li>
|
|
2581
|
+
|
|
2582
|
+
<ul
|
|
2583
|
+
*ngIf="node.expanded && hasChildren(node)"
|
|
2584
|
+
class="list-unstyled ms-4"
|
|
2585
|
+
role="group"
|
|
2586
|
+
>
|
|
2587
|
+
<ng-container *ngFor="let child of node.children; let ci = index; trackBy: trackById">
|
|
2588
|
+
<ng-container *ngTemplateOutlet="renderNode; context: { $implicit: child, level: level + 1, index: ci }"></ng-container>
|
|
2589
|
+
</ng-container>
|
|
2590
|
+
</ul>
|
|
2591
|
+
</ng-template>
|
|
2592
|
+
`,
|
|
2593
|
+
}]
|
|
2594
|
+
}], propDecorators: { nodes: [{
|
|
2595
|
+
type: Input
|
|
2596
|
+
}], type: [{
|
|
2597
|
+
type: Input
|
|
2598
|
+
}], showCheckbox: [{
|
|
2599
|
+
type: Input
|
|
2600
|
+
}], i18n: [{
|
|
2601
|
+
type: Input
|
|
2602
|
+
}], expand: [{
|
|
2603
|
+
type: Output
|
|
2604
|
+
}], collapse: [{
|
|
2605
|
+
type: Output
|
|
2606
|
+
}], expandAll: [{
|
|
2607
|
+
type: Output
|
|
2608
|
+
}], collapseAll: [{
|
|
2609
|
+
type: Output
|
|
2610
|
+
}], selectionChange: [{
|
|
2611
|
+
type: Output
|
|
2612
|
+
}], nodeButtons: [{
|
|
2613
|
+
type: ViewChildren,
|
|
2614
|
+
args: ['nodeBtn', { read: ElementRef }]
|
|
2615
|
+
}] } });
|
|
2616
|
+
|
|
2617
|
+
/// <reference path="./types/optional-peer-deps.d.ts" />
|
|
2618
|
+
|
|
2619
|
+
/**
|
|
2620
|
+
* Generated bundle index. Do not edit.
|
|
2621
|
+
*/
|
|
2622
|
+
|
|
2623
|
+
export { DATAGRID_TEMPLATE_DIRECTIVES, DND_I18N, DND_LIVE_ANNOUNCE, Datagrid, ExcelExportAdapter, ExportButtonDirective, JsPdfAdapter, NgbCellTemplate, NgbDndItemDirective, NgbDndListDirective, NgbDndState, NgbEditorTemplate, NgbExportService, NgbFilterTemplate, NgbGlobalFilterTemplate, NgbLiveAnnouncer, NgbPaginationComponent, NgbRowDetailTemplate, NgbSplitterComponent, NgbStepLabelDirective, NgbStepperComponent, NgbTreeComponent, NgbTypeaheadComponent, PdfExportAdapter, XlsxAdapter, defaultDndI18n };
|
|
2624
|
+
//# sourceMappingURL=angular-bootstrap-ngbootstrap.mjs.map
|