ng-dynamic-datatable 0.0.0 → 0.0.2
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.
- package/README.md +109 -34
- package/fesm2022/ng-dynamic-datatable.mjs +483 -0
- package/fesm2022/ng-dynamic-datatable.mjs.map +1 -0
- package/index.d.ts +5 -0
- package/lib/dynamic-datatable.component.d.ts +83 -0
- package/lib/dynamic-datatable.types.d.ts +4 -0
- package/lib/models.d.ts +45 -0
- package/package.json +35 -38
- package/{projects/dynamic-datatable/src/public-api.ts → public-api.d.ts} +2 -6
- package/.editorconfig +0 -17
- package/.vscode/extensions.json +0 -4
- package/.vscode/launch.json +0 -20
- package/.vscode/tasks.json +0 -42
- package/angular.json +0 -44
- package/projects/dynamic-datatable/README.md +0 -134
- package/projects/dynamic-datatable/ng-package.json +0 -7
- package/projects/dynamic-datatable/package.json +0 -24
- package/projects/dynamic-datatable/src/lib/dynamic-datatable.component.html +0 -137
- package/projects/dynamic-datatable/src/lib/dynamic-datatable.component.scss +0 -237
- package/projects/dynamic-datatable/src/lib/dynamic-datatable.component.spec.ts +0 -23
- package/projects/dynamic-datatable/src/lib/dynamic-datatable.component.ts +0 -556
- package/projects/dynamic-datatable/src/lib/dynamic-datatable.service.spec.ts +0 -16
- package/projects/dynamic-datatable/src/lib/dynamic-datatable.service.ts +0 -9
- package/projects/dynamic-datatable/src/lib/dynamic-datatable.types.ts +0 -4
- package/projects/dynamic-datatable/src/lib/models.ts +0 -52
- package/projects/dynamic-datatable/tsconfig.lib.json +0 -15
- package/projects/dynamic-datatable/tsconfig.lib.prod.json +0 -11
- package/projects/dynamic-datatable/tsconfig.spec.json +0 -15
- package/tsconfig.json +0 -32
|
@@ -1,556 +0,0 @@
|
|
|
1
|
-
import { CommonModule } from '@angular/common';
|
|
2
|
-
import { Component, EventEmitter, HostListener, Input, OnChanges, Output, SimpleChanges } from '@angular/core';
|
|
3
|
-
import {
|
|
4
|
-
DataTableAction,
|
|
5
|
-
DataTableActionEvent,
|
|
6
|
-
DataTableColumn,
|
|
7
|
-
DataTableExportEvent,
|
|
8
|
-
DynamicDataTableConfig
|
|
9
|
-
} from './models';
|
|
10
|
-
import { InternalSortState } from './dynamic-datatable.types';
|
|
11
|
-
|
|
12
|
-
type RowData = Record<string, unknown>;
|
|
13
|
-
|
|
14
|
-
interface InternalRow<T extends RowData = RowData> {
|
|
15
|
-
id: string;
|
|
16
|
-
data: T;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const DEFAULT_CONFIG: Required<Pick<DynamicDataTableConfig, 'title' | 'rowsPerPageOptions' | 'defaultRowsPerPage'>> & {
|
|
20
|
-
features: Required<NonNullable<DynamicDataTableConfig['features']>>;
|
|
21
|
-
exportOptions: Required<NonNullable<DynamicDataTableConfig['exportOptions']>>;
|
|
22
|
-
} = {
|
|
23
|
-
title: 'Dynamic DataTable',
|
|
24
|
-
features: {
|
|
25
|
-
export: true,
|
|
26
|
-
sorting: true,
|
|
27
|
-
searching: true,
|
|
28
|
-
pagination: true,
|
|
29
|
-
rowsPerPage: true,
|
|
30
|
-
actions: true,
|
|
31
|
-
checkboxSelection: true
|
|
32
|
-
},
|
|
33
|
-
rowsPerPageOptions: [5, 10, 25, 50],
|
|
34
|
-
defaultRowsPerPage: 10,
|
|
35
|
-
exportOptions: {
|
|
36
|
-
fileName: 'datatable-export',
|
|
37
|
-
formats: ['csv', 'excel']
|
|
38
|
-
}
|
|
39
|
-
};
|
|
40
|
-
|
|
41
|
-
@Component({
|
|
42
|
-
selector: 'lib-dynamic-datatable',
|
|
43
|
-
standalone: true,
|
|
44
|
-
imports: [CommonModule],
|
|
45
|
-
templateUrl: './dynamic-datatable.component.html',
|
|
46
|
-
styleUrl: './dynamic-datatable.component.scss'
|
|
47
|
-
})
|
|
48
|
-
export class DynamicDatatableComponent implements OnChanges {
|
|
49
|
-
@Input() config: DynamicDataTableConfig = {};
|
|
50
|
-
@Input() columns: DataTableColumn<RowData>[] = [];
|
|
51
|
-
@Input() data: RowData[] = [];
|
|
52
|
-
|
|
53
|
-
@Output() readonly actionSelected = new EventEmitter<DataTableActionEvent<RowData>>();
|
|
54
|
-
@Output() readonly selectionChange = new EventEmitter<RowData[]>();
|
|
55
|
-
@Output() readonly sortChange = new EventEmitter<InternalSortState>();
|
|
56
|
-
@Output() readonly pageChange = new EventEmitter<number>();
|
|
57
|
-
@Output() readonly exportTriggered = new EventEmitter<DataTableExportEvent<RowData>>();
|
|
58
|
-
|
|
59
|
-
mergedConfig: DynamicDataTableConfig<RowData> = {
|
|
60
|
-
title: DEFAULT_CONFIG.title,
|
|
61
|
-
features: { ...DEFAULT_CONFIG.features },
|
|
62
|
-
rowsPerPageOptions: [...DEFAULT_CONFIG.rowsPerPageOptions],
|
|
63
|
-
defaultRowsPerPage: DEFAULT_CONFIG.defaultRowsPerPage,
|
|
64
|
-
exportOptions: { ...DEFAULT_CONFIG.exportOptions },
|
|
65
|
-
columns: [],
|
|
66
|
-
actions: []
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
mergedColumns: DataTableColumn<RowData>[] = [];
|
|
70
|
-
|
|
71
|
-
private allRows: InternalRow<RowData>[] = [];
|
|
72
|
-
filteredRows: InternalRow<RowData>[] = [];
|
|
73
|
-
pagedRows: InternalRow<RowData>[] = [];
|
|
74
|
-
|
|
75
|
-
searchTerm = '';
|
|
76
|
-
currentPage = 1;
|
|
77
|
-
rowsPerPage = DEFAULT_CONFIG.defaultRowsPerPage;
|
|
78
|
-
totalPages = 1;
|
|
79
|
-
sortState: InternalSortState = { key: null, direction: 'asc' };
|
|
80
|
-
selectedRows = new Set<string>();
|
|
81
|
-
openActionMenuRowId: string | null = null;
|
|
82
|
-
|
|
83
|
-
ngOnChanges(_changes: SimpleChanges): void {
|
|
84
|
-
this.refresh();
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
refresh(): void {
|
|
88
|
-
this.applyConfig();
|
|
89
|
-
this.normalizeRows();
|
|
90
|
-
this.applyFiltersSortAndPagination();
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
updateData(newData: RowData[]): void {
|
|
94
|
-
this.data = Array.isArray(newData) ? [...newData] : [];
|
|
95
|
-
this.refresh();
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
updateConfig(newConfig: DynamicDataTableConfig<RowData>): void {
|
|
99
|
-
this.config = { ...this.config, ...newConfig };
|
|
100
|
-
this.refresh();
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
getSelectedRows(): RowData[] {
|
|
104
|
-
return this.allRows.filter((row) => this.selectedRows.has(row.id)).map((row) => row.data);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
clearSelection(): void {
|
|
108
|
-
this.selectedRows.clear();
|
|
109
|
-
this.selectionChange.emit([]);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
isFeatureOn(feature: keyof NonNullable<DynamicDataTableConfig['features']>): boolean {
|
|
113
|
-
return Boolean(this.mergedConfig.features?.[feature]);
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
isExportFormatEnabled(format: 'csv' | 'excel'): boolean {
|
|
117
|
-
const formats = this.mergedConfig.exportOptions?.formats ?? [];
|
|
118
|
-
return formats.includes(format);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
onSearchInput(event: Event): void {
|
|
122
|
-
const input = event.target as HTMLInputElement;
|
|
123
|
-
this.searchTerm = (input.value || '').trim().toLowerCase();
|
|
124
|
-
this.currentPage = 1;
|
|
125
|
-
this.applyFiltersSortAndPagination();
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
onRowsPerPageChange(event: Event): void {
|
|
129
|
-
const select = event.target as HTMLSelectElement;
|
|
130
|
-
const value = Number(select.value);
|
|
131
|
-
this.rowsPerPage = Number.isFinite(value) && value > 0 ? value : this.getInitialRowsPerPage();
|
|
132
|
-
this.currentPage = 1;
|
|
133
|
-
this.applyFiltersSortAndPagination();
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
toggleSort(column: DataTableColumn<RowData>): void {
|
|
137
|
-
if (!this.isFeatureOn('sorting') || !column.sortable) {
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const key = String(column.key);
|
|
142
|
-
if (this.sortState.key === key) {
|
|
143
|
-
this.sortState.direction = this.sortState.direction === 'asc' ? 'desc' : 'asc';
|
|
144
|
-
} else {
|
|
145
|
-
this.sortState = { key, direction: 'asc' };
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
this.currentPage = 1;
|
|
149
|
-
this.sortChange.emit({ ...this.sortState });
|
|
150
|
-
this.applyFiltersSortAndPagination();
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
getSortIndicator(columnKey: string): string {
|
|
154
|
-
if (this.sortState.key !== columnKey) {
|
|
155
|
-
return '↕';
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
return this.sortState.direction === 'asc' ? '↑' : '↓';
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
goToPage(page: number): void {
|
|
162
|
-
if (!this.isFeatureOn('pagination')) {
|
|
163
|
-
return;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
const bounded = Math.min(Math.max(page, 1), this.totalPages);
|
|
167
|
-
if (bounded === this.currentPage) {
|
|
168
|
-
return;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
this.currentPage = bounded;
|
|
172
|
-
this.pageChange.emit(this.currentPage);
|
|
173
|
-
this.updatePagedRowsOnly();
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
getPaginationPages(): number[] {
|
|
177
|
-
return Array.from({ length: this.totalPages }, (_, idx) => idx + 1);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
isRowSelected(rowId: string): boolean {
|
|
181
|
-
return this.selectedRows.has(rowId);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
toggleRowSelection(row: InternalRow<RowData>, event: Event): void {
|
|
185
|
-
const input = event.target as HTMLInputElement;
|
|
186
|
-
if (input.checked) {
|
|
187
|
-
this.selectedRows.add(row.id);
|
|
188
|
-
} else {
|
|
189
|
-
this.selectedRows.delete(row.id);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
this.emitSelection();
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
toggleSelectAllCurrentPage(event: Event): void {
|
|
196
|
-
const input = event.target as HTMLInputElement;
|
|
197
|
-
this.pagedRows.forEach((row) => {
|
|
198
|
-
if (input.checked) {
|
|
199
|
-
this.selectedRows.add(row.id);
|
|
200
|
-
} else {
|
|
201
|
-
this.selectedRows.delete(row.id);
|
|
202
|
-
}
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
this.emitSelection();
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
isCurrentPageFullySelected(): boolean {
|
|
209
|
-
if (!this.pagedRows.length) {
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return this.pagedRows.every((row) => this.selectedRows.has(row.id));
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
toggleActionMenu(rowId: string, event: Event): void {
|
|
217
|
-
event.stopPropagation();
|
|
218
|
-
this.openActionMenuRowId = this.openActionMenuRowId === rowId ? null : rowId;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
closeActionMenu(): void {
|
|
222
|
-
this.openActionMenuRowId = null;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
isActionMenuOpen(rowId: string): boolean {
|
|
226
|
-
return this.openActionMenuRowId === rowId;
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
onActionItemClick(actionIndex: number, row: InternalRow<RowData>): void {
|
|
230
|
-
if (!Number.isInteger(actionIndex) || actionIndex < 0) {
|
|
231
|
-
return;
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
const action = this.mergedConfig.actions?.[actionIndex];
|
|
235
|
-
if (!action) {
|
|
236
|
-
return;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const rowIndex = this.filteredRows.findIndex((item) => item.id === row.id);
|
|
240
|
-
this.actionSelected.emit({ action, row: row.data, rowIndex });
|
|
241
|
-
this.closeActionMenu();
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
@HostListener('document:click')
|
|
245
|
-
handleDocumentClick(): void {
|
|
246
|
-
this.closeActionMenu();
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
exportCSV(): void {
|
|
250
|
-
const rows = this.getRowsForExport();
|
|
251
|
-
this.exportTriggered.emit({ format: 'csv', rows });
|
|
252
|
-
|
|
253
|
-
const headers = this.mergedColumns.map((column) => this.escapeCsv(column.label));
|
|
254
|
-
const lines = rows.map((row) => {
|
|
255
|
-
return this.mergedColumns
|
|
256
|
-
.map((column) => this.escapeCsv(this.getCellTextValue(row, column)))
|
|
257
|
-
.join(',');
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const csv = [headers.join(','), ...lines].join('\n');
|
|
261
|
-
this.downloadFile(csv, `${this.getExportFileName()}.csv`, 'text/csv;charset=utf-8;');
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
exportExcel(): void {
|
|
265
|
-
const rows = this.getRowsForExport();
|
|
266
|
-
this.exportTriggered.emit({ format: 'excel', rows });
|
|
267
|
-
|
|
268
|
-
const headerCells = this.mergedColumns.map((column) => `<th>${this.escapeHtml(column.label)}</th>`).join('');
|
|
269
|
-
const bodyRows = rows
|
|
270
|
-
.map((row) => {
|
|
271
|
-
const cells = this.mergedColumns
|
|
272
|
-
.map((column) => `<td>${this.escapeHtml(this.getCellTextValue(row, column))}</td>`)
|
|
273
|
-
.join('');
|
|
274
|
-
return `<tr>${cells}</tr>`;
|
|
275
|
-
})
|
|
276
|
-
.join('');
|
|
277
|
-
|
|
278
|
-
const html = `<html><head><meta charset="UTF-8"></head><body><table border="1"><thead><tr>${headerCells}</tr></thead><tbody>${bodyRows}</tbody></table></body></html>`;
|
|
279
|
-
this.downloadFile(html, `${this.getExportFileName()}.xls`, 'application/vnd.ms-excel;charset=utf-8;');
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
getRenderedCell(row: RowData, column: DataTableColumn<RowData>): string {
|
|
283
|
-
if (typeof column.render === 'function') {
|
|
284
|
-
return column.render(row);
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const key = String(column.key);
|
|
288
|
-
const value = row[key];
|
|
289
|
-
return this.escapeHtml(value == null ? '' : String(value));
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
toColumnWidth(width: string | number | undefined): string {
|
|
293
|
-
if (typeof width === 'number') {
|
|
294
|
-
return `${width}px`;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
if (typeof width === 'string' && width.trim()) {
|
|
298
|
-
return width.trim();
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
return 'auto';
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
getColumnCount(): number {
|
|
305
|
-
let count = this.mergedColumns.length;
|
|
306
|
-
if (this.isFeatureOn('checkboxSelection')) {
|
|
307
|
-
count += 1;
|
|
308
|
-
}
|
|
309
|
-
if (this.isFeatureOn('actions')) {
|
|
310
|
-
count += 1;
|
|
311
|
-
}
|
|
312
|
-
return count;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
getStartEntry(): number {
|
|
316
|
-
if (!this.filteredRows.length) {
|
|
317
|
-
return 0;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
if (!this.isFeatureOn('pagination')) {
|
|
321
|
-
return 1;
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
return (this.currentPage - 1) * this.rowsPerPage + 1;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
getEndEntry(): number {
|
|
328
|
-
if (!this.filteredRows.length) {
|
|
329
|
-
return 0;
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
if (!this.isFeatureOn('pagination')) {
|
|
333
|
-
return this.filteredRows.length;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
return Math.min(this.currentPage * this.rowsPerPage, this.filteredRows.length);
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
private applyConfig(): void {
|
|
340
|
-
const features = {
|
|
341
|
-
...DEFAULT_CONFIG.features,
|
|
342
|
-
...(this.config.features ?? {})
|
|
343
|
-
};
|
|
344
|
-
|
|
345
|
-
const exportOptions = {
|
|
346
|
-
...DEFAULT_CONFIG.exportOptions,
|
|
347
|
-
...(this.config.exportOptions ?? {})
|
|
348
|
-
};
|
|
349
|
-
|
|
350
|
-
const rowsPerPageOptions = (this.config.rowsPerPageOptions ?? DEFAULT_CONFIG.rowsPerPageOptions).filter(
|
|
351
|
-
(item) => Number.isFinite(item) && item > 0
|
|
352
|
-
);
|
|
353
|
-
|
|
354
|
-
this.mergedConfig = {
|
|
355
|
-
title: this.config.title ?? DEFAULT_CONFIG.title,
|
|
356
|
-
features,
|
|
357
|
-
rowsPerPageOptions: rowsPerPageOptions.length ? rowsPerPageOptions : [...DEFAULT_CONFIG.rowsPerPageOptions],
|
|
358
|
-
defaultRowsPerPage: this.config.defaultRowsPerPage ?? DEFAULT_CONFIG.defaultRowsPerPage,
|
|
359
|
-
exportOptions,
|
|
360
|
-
actions: [...(this.config.actions ?? [])],
|
|
361
|
-
columns: []
|
|
362
|
-
};
|
|
363
|
-
|
|
364
|
-
this.mergedColumns = [...(this.columns.length ? this.columns : (this.config.columns ?? []))];
|
|
365
|
-
this.rowsPerPage = this.getInitialRowsPerPage();
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
private normalizeRows(): void {
|
|
369
|
-
this.allRows = (Array.isArray(this.data) ? this.data : []).map((row, index) => {
|
|
370
|
-
const rowId = row['id'];
|
|
371
|
-
const id = rowId == null ? `row-${index + 1}` : String(rowId);
|
|
372
|
-
return { id, data: row };
|
|
373
|
-
});
|
|
374
|
-
|
|
375
|
-
const validIds = new Set(this.allRows.map((row) => row.id));
|
|
376
|
-
for (const selectedId of Array.from(this.selectedRows)) {
|
|
377
|
-
if (!validIds.has(selectedId)) {
|
|
378
|
-
this.selectedRows.delete(selectedId);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
private applyFiltersSortAndPagination(): void {
|
|
384
|
-
if (!this.isFeatureOn('searching') || !this.searchTerm) {
|
|
385
|
-
this.filteredRows = [...this.allRows];
|
|
386
|
-
} else {
|
|
387
|
-
this.filteredRows = this.allRows.filter((row) => {
|
|
388
|
-
const searchSpace = this.mergedColumns
|
|
389
|
-
.map((column) => this.getCellTextValue(row.data, column).toLowerCase())
|
|
390
|
-
.join(' ');
|
|
391
|
-
return searchSpace.includes(this.searchTerm);
|
|
392
|
-
});
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
if (this.isFeatureOn('sorting') && this.sortState.key) {
|
|
396
|
-
const column = this.mergedColumns.find((item) => String(item.key) === this.sortState.key);
|
|
397
|
-
if (column?.sortable) {
|
|
398
|
-
this.filteredRows.sort((a, b) => {
|
|
399
|
-
const aVal = this.getCellTextValue(a.data, column);
|
|
400
|
-
const bVal = this.getCellTextValue(b.data, column);
|
|
401
|
-
const compare = this.compareValues(aVal, bVal);
|
|
402
|
-
return this.sortState.direction === 'asc' ? compare : -compare;
|
|
403
|
-
});
|
|
404
|
-
}
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
this.totalPages = this.isFeatureOn('pagination')
|
|
408
|
-
? Math.max(1, Math.ceil(this.filteredRows.length / this.rowsPerPage))
|
|
409
|
-
: 1;
|
|
410
|
-
|
|
411
|
-
if (this.currentPage > this.totalPages) {
|
|
412
|
-
this.currentPage = this.totalPages;
|
|
413
|
-
}
|
|
414
|
-
|
|
415
|
-
this.updatePagedRowsOnly();
|
|
416
|
-
this.emitSelection();
|
|
417
|
-
}
|
|
418
|
-
|
|
419
|
-
private updatePagedRowsOnly(): void {
|
|
420
|
-
if (!this.isFeatureOn('pagination')) {
|
|
421
|
-
this.pagedRows = [...this.filteredRows];
|
|
422
|
-
return;
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
const start = (this.currentPage - 1) * this.rowsPerPage;
|
|
426
|
-
this.pagedRows = this.filteredRows.slice(start, start + this.rowsPerPage);
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
private getCellTextValue(row: RowData, column: DataTableColumn<RowData>): string {
|
|
430
|
-
const key = String(column.key);
|
|
431
|
-
if (row[key] != null) {
|
|
432
|
-
return String(row[key]);
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
const rendered = this.getRenderedCell(row, column);
|
|
436
|
-
return this.stripHtml(rendered);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
private compareValues(a: string, b: string): number {
|
|
440
|
-
const aText = a.trim();
|
|
441
|
-
const bText = b.trim();
|
|
442
|
-
|
|
443
|
-
const aNum = this.parseStrictNumber(aText);
|
|
444
|
-
const bNum = this.parseStrictNumber(bText);
|
|
445
|
-
if (aNum !== null && bNum !== null) {
|
|
446
|
-
return aNum - bNum;
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
const aDate = this.parseDateValue(aText);
|
|
450
|
-
const bDate = this.parseDateValue(bText);
|
|
451
|
-
if (aDate !== null && bDate !== null) {
|
|
452
|
-
return aDate - bDate;
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
return aText.localeCompare(bText, undefined, { numeric: true, sensitivity: 'base' });
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
private parseStrictNumber(value: string): number | null {
|
|
459
|
-
const normalized = value.replace(/[$,\s]/g, '');
|
|
460
|
-
if (!/^[-+]?\d*\.?\d+$/.test(normalized)) {
|
|
461
|
-
return null;
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
const numberValue = Number(normalized);
|
|
465
|
-
return Number.isNaN(numberValue) ? null : numberValue;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
private parseDateValue(value: string): number | null {
|
|
469
|
-
const usDate = value.match(/^(\d{1,2})\/(\d{1,2})\/(\d{4})$/);
|
|
470
|
-
if (usDate) {
|
|
471
|
-
const month = Number(usDate[1]);
|
|
472
|
-
const day = Number(usDate[2]);
|
|
473
|
-
const year = Number(usDate[3]);
|
|
474
|
-
const utc = Date.UTC(year, month - 1, day);
|
|
475
|
-
return Number.isNaN(utc) ? null : utc;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
const isoDate = value.match(/^(\d{4})-(\d{1,2})-(\d{1,2})$/);
|
|
479
|
-
if (isoDate) {
|
|
480
|
-
const year = Number(isoDate[1]);
|
|
481
|
-
const month = Number(isoDate[2]);
|
|
482
|
-
const day = Number(isoDate[3]);
|
|
483
|
-
const utc = Date.UTC(year, month - 1, day);
|
|
484
|
-
return Number.isNaN(utc) ? null : utc;
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
return null;
|
|
488
|
-
}
|
|
489
|
-
|
|
490
|
-
private getRowsForExport(): RowData[] {
|
|
491
|
-
const selected = this.getSelectedRows();
|
|
492
|
-
if (selected.length) {
|
|
493
|
-
return selected;
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
return this.filteredRows.map((item) => item.data);
|
|
497
|
-
}
|
|
498
|
-
|
|
499
|
-
private getExportFileName(): string {
|
|
500
|
-
return this.mergedConfig.exportOptions?.fileName || 'datatable-export';
|
|
501
|
-
}
|
|
502
|
-
|
|
503
|
-
private downloadFile(content: string, fileName: string, mimeType: string): void {
|
|
504
|
-
if (typeof document === 'undefined') {
|
|
505
|
-
return;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
const blob = new Blob([content], { type: mimeType });
|
|
509
|
-
const url = URL.createObjectURL(blob);
|
|
510
|
-
const anchor = document.createElement('a');
|
|
511
|
-
anchor.href = url;
|
|
512
|
-
anchor.download = fileName;
|
|
513
|
-
document.body.appendChild(anchor);
|
|
514
|
-
anchor.click();
|
|
515
|
-
document.body.removeChild(anchor);
|
|
516
|
-
URL.revokeObjectURL(url);
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
private escapeCsv(value: string): string {
|
|
520
|
-
if (/[",\n]/.test(value)) {
|
|
521
|
-
return `"${value.replace(/"/g, '""')}"`;
|
|
522
|
-
}
|
|
523
|
-
|
|
524
|
-
return value;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
private stripHtml(input: string): string {
|
|
528
|
-
if (typeof document === 'undefined') {
|
|
529
|
-
return input;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
const temp = document.createElement('div');
|
|
533
|
-
temp.innerHTML = input;
|
|
534
|
-
return temp.textContent || temp.innerText || '';
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
private escapeHtml(value: string): string {
|
|
538
|
-
return String(value)
|
|
539
|
-
.replace(/&/g, '&')
|
|
540
|
-
.replace(/</g, '<')
|
|
541
|
-
.replace(/>/g, '>')
|
|
542
|
-
.replace(/"/g, '"')
|
|
543
|
-
.replace(/'/g, ''');
|
|
544
|
-
}
|
|
545
|
-
|
|
546
|
-
private getInitialRowsPerPage(): number {
|
|
547
|
-
const options = this.mergedConfig.rowsPerPageOptions ?? DEFAULT_CONFIG.rowsPerPageOptions;
|
|
548
|
-
const preferred = Number(this.mergedConfig.defaultRowsPerPage ?? DEFAULT_CONFIG.defaultRowsPerPage);
|
|
549
|
-
return options.includes(preferred) ? preferred : options[0] ?? DEFAULT_CONFIG.defaultRowsPerPage;
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
private emitSelection(): void {
|
|
553
|
-
this.selectionChange.emit(this.getSelectedRows());
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
}
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { TestBed } from '@angular/core/testing';
|
|
2
|
-
|
|
3
|
-
import { DynamicDatatableService } from './dynamic-datatable.service';
|
|
4
|
-
|
|
5
|
-
describe('DynamicDatatableService', () => {
|
|
6
|
-
let service: DynamicDatatableService;
|
|
7
|
-
|
|
8
|
-
beforeEach(() => {
|
|
9
|
-
TestBed.configureTestingModule({});
|
|
10
|
-
service = TestBed.inject(DynamicDatatableService);
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('should be created', () => {
|
|
14
|
-
expect(service).toBeTruthy();
|
|
15
|
-
});
|
|
16
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
export type DataTableExportFormat = 'csv' | 'excel';
|
|
2
|
-
|
|
3
|
-
export interface DataTableFeatureConfig {
|
|
4
|
-
export?: boolean;
|
|
5
|
-
sorting?: boolean;
|
|
6
|
-
searching?: boolean;
|
|
7
|
-
pagination?: boolean;
|
|
8
|
-
rowsPerPage?: boolean;
|
|
9
|
-
actions?: boolean;
|
|
10
|
-
checkboxSelection?: boolean;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface DataTableExportOptions {
|
|
14
|
-
fileName?: string;
|
|
15
|
-
formats?: DataTableExportFormat[];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface DataTableColumn<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
19
|
-
key: keyof T | string;
|
|
20
|
-
label: string;
|
|
21
|
-
sortable?: boolean;
|
|
22
|
-
width?: string | number;
|
|
23
|
-
render?: (row: T) => string;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface DataTableAction<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
27
|
-
label: string;
|
|
28
|
-
icon?: string;
|
|
29
|
-
danger?: boolean;
|
|
30
|
-
id?: string;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface DynamicDataTableConfig<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
34
|
-
title?: string;
|
|
35
|
-
features?: DataTableFeatureConfig;
|
|
36
|
-
rowsPerPageOptions?: number[];
|
|
37
|
-
defaultRowsPerPage?: number;
|
|
38
|
-
exportOptions?: DataTableExportOptions;
|
|
39
|
-
columns?: DataTableColumn<T>[];
|
|
40
|
-
actions?: DataTableAction<T>[];
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface DataTableActionEvent<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
44
|
-
action: DataTableAction<T>;
|
|
45
|
-
row: T;
|
|
46
|
-
rowIndex: number;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface DataTableExportEvent<T extends Record<string, unknown> = Record<string, unknown>> {
|
|
50
|
-
format: DataTableExportFormat;
|
|
51
|
-
rows: T[];
|
|
52
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
-
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
-
{
|
|
4
|
-
"extends": "../../tsconfig.json",
|
|
5
|
-
"compilerOptions": {
|
|
6
|
-
"outDir": "../../out-tsc/lib",
|
|
7
|
-
"declaration": true,
|
|
8
|
-
"declarationMap": true,
|
|
9
|
-
"inlineSources": true,
|
|
10
|
-
"types": []
|
|
11
|
-
},
|
|
12
|
-
"exclude": [
|
|
13
|
-
"**/*.spec.ts"
|
|
14
|
-
]
|
|
15
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
-
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
-
{
|
|
4
|
-
"extends": "./tsconfig.lib.json",
|
|
5
|
-
"compilerOptions": {
|
|
6
|
-
"declarationMap": false
|
|
7
|
-
},
|
|
8
|
-
"angularCompilerOptions": {
|
|
9
|
-
"compilationMode": "partial"
|
|
10
|
-
}
|
|
11
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
/* To learn more about Typescript configuration file: https://www.typescriptlang.org/docs/handbook/tsconfig-json.html. */
|
|
2
|
-
/* To learn more about Angular compiler options: https://angular.dev/reference/configs/angular-compiler-options. */
|
|
3
|
-
{
|
|
4
|
-
"extends": "../../tsconfig.json",
|
|
5
|
-
"compilerOptions": {
|
|
6
|
-
"outDir": "../../out-tsc/spec",
|
|
7
|
-
"types": [
|
|
8
|
-
"jasmine"
|
|
9
|
-
]
|
|
10
|
-
},
|
|
11
|
-
"include": [
|
|
12
|
-
"**/*.spec.ts",
|
|
13
|
-
"**/*.d.ts"
|
|
14
|
-
]
|
|
15
|
-
}
|