@oneteme/jquery-table 0.0.1 → 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 +439 -122
- package/esm2022/lib/component/export.manager.mjs +45 -0
- package/esm2022/lib/component/group-by.manager.mjs +99 -0
- package/esm2022/lib/component/lazy-column-manager.mjs +99 -0
- package/esm2022/lib/component/slice-panel/slice-panel.component.mjs +43 -37
- package/esm2022/lib/component/slice-panel/slice-panel.model.mjs +1 -1
- package/esm2022/lib/component/table-preferences.manager.mjs +38 -0
- package/esm2022/lib/component/table.component.mjs +681 -370
- package/esm2022/lib/component/table.constants.mjs +31 -0
- package/esm2022/lib/component/table.utils.mjs +2 -2
- package/esm2022/lib/component/view/view.facade.mjs +298 -0
- package/esm2022/lib/jqt-i18n.token.mjs +41 -0
- package/esm2022/lib/jquery-table.model.mjs +14 -2
- package/esm2022/public-api.mjs +2 -1
- package/fesm2022/oneteme-jquery-table.mjs +1377 -408
- package/fesm2022/oneteme-jquery-table.mjs.map +1 -1
- package/lib/component/export.manager.d.ts +16 -0
- package/lib/component/group-by.manager.d.ts +44 -0
- package/lib/component/lazy-column-manager.d.ts +26 -0
- package/lib/component/slice-panel/slice-panel.component.d.ts +11 -3
- package/lib/component/slice-panel/slice-panel.model.d.ts +11 -1
- package/lib/component/table-preferences.manager.d.ts +17 -0
- package/lib/component/table.component.d.ts +162 -27
- package/lib/component/table.constants.d.ts +30 -0
- package/lib/component/view/view.facade.d.ts +93 -0
- package/lib/jqt-i18n.token.d.ts +87 -0
- package/lib/jquery-table.model.d.ts +79 -0
- package/package.json +2 -2
- package/public-api.d.ts +1 -0
|
@@ -1,25 +1,82 @@
|
|
|
1
|
+
import * as i0 from '@angular/core';
|
|
2
|
+
import { InjectionToken, Directive, Input, EventEmitter, inject, ChangeDetectorRef, Component, ChangeDetectionStrategy, Output, ViewChild, ElementRef, NgZone, ContentChildren } from '@angular/core';
|
|
1
3
|
import * as i1 from '@angular/common';
|
|
2
4
|
import { CommonModule } from '@angular/common';
|
|
3
|
-
import * as i0 from '@angular/core';
|
|
4
|
-
import { Directive, Input, EventEmitter, inject, ChangeDetectorRef, Component, ChangeDetectionStrategy, Output, ViewChild, ElementRef, ContentChildren } from '@angular/core';
|
|
5
5
|
import * as i2$1 from '@angular/forms';
|
|
6
6
|
import { FormsModule } from '@angular/forms';
|
|
7
7
|
import { MatButtonModule } from '@angular/material/button';
|
|
8
|
+
import * as i4 from '@angular/material/divider';
|
|
9
|
+
import { MatDividerModule } from '@angular/material/divider';
|
|
8
10
|
import * as i2 from '@angular/material/icon';
|
|
9
11
|
import { MatIconModule } from '@angular/material/icon';
|
|
10
|
-
import * as
|
|
12
|
+
import * as i7 from '@angular/material/menu';
|
|
11
13
|
import { MatMenuModule } from '@angular/material/menu';
|
|
12
|
-
import * as
|
|
14
|
+
import * as i5 from '@angular/material/paginator';
|
|
13
15
|
import { MatPaginatorIntl, MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
|
|
14
|
-
import * as
|
|
16
|
+
import * as i10 from '@angular/material/select';
|
|
15
17
|
import { MatSelectModule } from '@angular/material/select';
|
|
16
|
-
import * as
|
|
18
|
+
import * as i6 from '@angular/material/sort';
|
|
17
19
|
import { MatSort, MatSortModule } from '@angular/material/sort';
|
|
18
20
|
import * as i3 from '@angular/material/table';
|
|
19
21
|
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
|
|
20
22
|
import { Subject, take, takeUntil } from 'rxjs';
|
|
21
|
-
import
|
|
22
|
-
import * as
|
|
23
|
+
import '@oneteme/jquery-core';
|
|
24
|
+
import * as i9 from '@angular/material/form-field';
|
|
25
|
+
import * as i11 from '@angular/material/core';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Raccourci pour déclarer une colonne de base.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* columns: [
|
|
32
|
+
* col('name', 'Nom'),
|
|
33
|
+
* col('status', 'Statut', { sortable: false }),
|
|
34
|
+
* ]
|
|
35
|
+
* ```
|
|
36
|
+
*/
|
|
37
|
+
function col(key, header, overrides) {
|
|
38
|
+
return { key, header, ...overrides };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Valeurs par défaut (français). Toutes les propriétés sont garanties non nulles. */
|
|
42
|
+
const JQT_I18N_DEFAULTS = {
|
|
43
|
+
searchPlaceholder: 'Rechercher...',
|
|
44
|
+
expandFilterTitle: 'Déplier le filtre',
|
|
45
|
+
collapseFilterTitle: 'Replier le filtre',
|
|
46
|
+
viewMenuFields: 'Champs',
|
|
47
|
+
viewMenuGroupBy: 'Grouper par',
|
|
48
|
+
viewMenuSliceBy: 'Filtrer par',
|
|
49
|
+
viewNoneSelected: 'Aucun',
|
|
50
|
+
groupRemove: 'Retirer le groupe',
|
|
51
|
+
groupSortLabel: 'Trier :',
|
|
52
|
+
groupSortAsc: 'A → Z',
|
|
53
|
+
groupSortDesc: 'Z → A',
|
|
54
|
+
groupRowsPerGroup: 'Lignes par groupe :',
|
|
55
|
+
groupShowAll: 'Toutes',
|
|
56
|
+
groupTooMany: (count) => `⚠ Trop de groupes (${count}) — affinez les données ou choisissez une autre colonne`,
|
|
57
|
+
lazyRetry: 'Réessayer',
|
|
58
|
+
lazyErrorTitle: 'Erreur de chargement — cliquer pour réessayer',
|
|
59
|
+
lazyErrorMenuTitle: 'Erreur de chargement',
|
|
60
|
+
cellEmpty: 'N/A',
|
|
61
|
+
emptyState: 'Aucune donnée',
|
|
62
|
+
loadingState: 'Chargement des données...',
|
|
63
|
+
filterPanelTitle: (count) => count > 1 ? 'Filtres' : 'Filtre',
|
|
64
|
+
filterBlockDefaultTitle: 'Filtrer',
|
|
65
|
+
filterEmptyState: 'Aucune donnée',
|
|
66
|
+
exportButtonLabel: 'Exporter',
|
|
67
|
+
preferencesMenuLabel: 'Préférences',
|
|
68
|
+
preferencesEdit: 'Mode édition',
|
|
69
|
+
preferencesSave: 'Enregistrer la configuration',
|
|
70
|
+
preferencesClear: 'Effacer la configuration',
|
|
71
|
+
preferencesSavedMessage: 'Configuration enregistrée. Vous pouvez la réinitialiser à tout moment via Préférences → Effacer.',
|
|
72
|
+
preferencesClearedMessage: 'Configuration réinitialisée.',
|
|
73
|
+
preferencesConfirmEdit: 'Valider les modifications',
|
|
74
|
+
};
|
|
75
|
+
/**
|
|
76
|
+
* Token d'injection pour surcharger tout ou partie des labels de jquery-table.
|
|
77
|
+
* Accepte un `Partial<JqtI18n>` — seules les clés fournies écrasent les défauts.
|
|
78
|
+
*/
|
|
79
|
+
const JQT_I18N = new InjectionToken('JQT_I18N');
|
|
23
80
|
|
|
24
81
|
/**
|
|
25
82
|
* Directive permettant de définir un template de cellule personnalisé pour une colonne.
|
|
@@ -72,26 +129,21 @@ class SlicePanelComponent {
|
|
|
72
129
|
this.dynamicSliceKeysChange = new EventEmitter();
|
|
73
130
|
this.collapsedChange = new EventEmitter();
|
|
74
131
|
this._cdr = inject(ChangeDetectorRef);
|
|
132
|
+
this._i18nRaw = inject(JQT_I18N, { optional: true });
|
|
133
|
+
this.i18n = { ...JQT_I18N_DEFAULTS, ...(this._i18nRaw ?? {}) };
|
|
75
134
|
this.isCollapsed = false;
|
|
76
135
|
this.activeKeysBySlice = new Map();
|
|
77
136
|
this._expandedSlices = new Set();
|
|
78
137
|
this._dynamicSlices = [];
|
|
79
|
-
|
|
80
|
-
// ── Cache ─────────────────────────────────────────────────────
|
|
138
|
+
// ── Cache
|
|
81
139
|
this._cachedSlices = [];
|
|
82
140
|
this._countCache = new Map();
|
|
83
141
|
this.trackBySliceIndex = (index, _) => index;
|
|
84
142
|
this.trackByCategoryKey = (_, cat) => cat.key;
|
|
85
143
|
}
|
|
86
|
-
// ── Lifecycle
|
|
144
|
+
// ── Lifecycle
|
|
87
145
|
ngOnInit() {
|
|
88
146
|
this.isCollapsed = this.collapsedByDefault;
|
|
89
|
-
const initiallyHidden = (this.sliceConfigs || [])
|
|
90
|
-
.filter(s => s.hidden === true && !!s.columnKey)
|
|
91
|
-
.map(s => s.columnKey);
|
|
92
|
-
if (initiallyHidden.length) {
|
|
93
|
-
this._hiddenStaticSliceKeys = new Set([...this._hiddenStaticSliceKeys, ...initiallyHidden]);
|
|
94
|
-
}
|
|
95
147
|
}
|
|
96
148
|
ngOnChanges(changes) {
|
|
97
149
|
if (changes['sliceConfigs'] || changes['data'] || changes['columns'] || changes['lazyData'] || changes['lazyStatus']) {
|
|
@@ -100,10 +152,9 @@ class SlicePanelComponent {
|
|
|
100
152
|
this._cdr.markForCheck();
|
|
101
153
|
}
|
|
102
154
|
}
|
|
103
|
-
// ── Computed
|
|
155
|
+
// ── Computed
|
|
104
156
|
get showPanel() {
|
|
105
|
-
const
|
|
106
|
-
const hasCfg = visibleStaticCount > 0 || this._dynamicSlices.length > 0;
|
|
157
|
+
const hasCfg = (this.sliceConfigs || []).length > 0 || this._dynamicSlices.length > 0;
|
|
107
158
|
if (this.alwaysShow)
|
|
108
159
|
return hasCfg;
|
|
109
160
|
return hasCfg && (this.data?.length ?? 0) > 0;
|
|
@@ -112,7 +163,7 @@ class SlicePanelComponent {
|
|
|
112
163
|
return this._cachedSlices;
|
|
113
164
|
}
|
|
114
165
|
get staticSliceCount() {
|
|
115
|
-
return (this.sliceConfigs || []).
|
|
166
|
+
return (this.sliceConfigs || []).length;
|
|
116
167
|
}
|
|
117
168
|
get staticSlicesForMenu() {
|
|
118
169
|
return (this.sliceConfigs || [])
|
|
@@ -137,19 +188,19 @@ class SlicePanelComponent {
|
|
|
137
188
|
});
|
|
138
189
|
}
|
|
139
190
|
get activeSliceByLabel() {
|
|
140
|
-
const staticActiveCount = (this.sliceConfigs || []).
|
|
191
|
+
const staticActiveCount = (this.sliceConfigs || []).length;
|
|
141
192
|
const total = staticActiveCount + this._dynamicSlices.length;
|
|
142
193
|
if (total === 0)
|
|
143
194
|
return 'Aucun';
|
|
144
195
|
if (total === 1) {
|
|
145
|
-
const first = (this.sliceConfigs || [])
|
|
196
|
+
const first = (this.sliceConfigs || [])[0];
|
|
146
197
|
if (first)
|
|
147
198
|
return first.title || first.columnKey || '1';
|
|
148
199
|
return this.activeDynamicSliceColumns[0]?.header || '1';
|
|
149
200
|
}
|
|
150
201
|
return `${total} actifs`;
|
|
151
202
|
}
|
|
152
|
-
// ── UI state
|
|
203
|
+
// ── UI state
|
|
153
204
|
isSliceCollapsed(sliceIndex) {
|
|
154
205
|
if (this._cachedSlices.length === 1) {
|
|
155
206
|
return this._expandedSlices.has(-1 - sliceIndex);
|
|
@@ -206,21 +257,13 @@ class SlicePanelComponent {
|
|
|
206
257
|
const status = this.lazyStatus.get(key) ?? 'idle';
|
|
207
258
|
return status === 'loading' || status === 'idle';
|
|
208
259
|
}
|
|
209
|
-
isStaticSliceVisible(
|
|
210
|
-
return
|
|
260
|
+
isStaticSliceVisible(_columnKey) {
|
|
261
|
+
return true;
|
|
211
262
|
}
|
|
212
|
-
toggleStaticSlice(
|
|
213
|
-
|
|
214
|
-
if (next.has(columnKey))
|
|
215
|
-
next.delete(columnKey);
|
|
216
|
-
else
|
|
217
|
-
next.add(columnKey);
|
|
218
|
-
this._hiddenStaticSliceKeys = next;
|
|
219
|
-
this._rebuildCache();
|
|
220
|
-
this._emitFilter();
|
|
221
|
-
this._cdr.markForCheck();
|
|
263
|
+
toggleStaticSlice(_columnKey) {
|
|
264
|
+
// Délégué à table.component via _staticSliceHiddenKeys — ne rien faire ici.
|
|
222
265
|
}
|
|
223
|
-
// ── Selection
|
|
266
|
+
// ── Selection
|
|
224
267
|
getSliceIcon(slice) {
|
|
225
268
|
if (slice.icon)
|
|
226
269
|
return slice.icon;
|
|
@@ -289,7 +332,24 @@ class SlicePanelComponent {
|
|
|
289
332
|
this._emitFilter();
|
|
290
333
|
this._cdr.markForCheck();
|
|
291
334
|
}
|
|
292
|
-
|
|
335
|
+
/** Retourne l'état des filtres actifs sous forme sérialisable (sliceIndex → tableau de clés).
|
|
336
|
+
* Inclut les slices sans sélection (tableau vide) pour distinguer "aucune tranche sélectionnée"
|
|
337
|
+
* (= filtre ouvert, rien coché) de "slice absente". */
|
|
338
|
+
getActiveFilters() {
|
|
339
|
+
const result = {};
|
|
340
|
+
this._cachedSlices.forEach((_, idx) => {
|
|
341
|
+
const keys = this.activeKeysBySlice.get(idx);
|
|
342
|
+
result[idx] = keys ? [...keys] : [];
|
|
343
|
+
});
|
|
344
|
+
return result;
|
|
345
|
+
}
|
|
346
|
+
/** Restaure les filtres actifs depuis un snapshot sérialisé et réémet le filtre. */
|
|
347
|
+
restoreFilters(filters) {
|
|
348
|
+
this.activeKeysBySlice = new Map(Object.entries(filters).map(([idx, keys]) => [Number(idx), new Set(keys)]));
|
|
349
|
+
this._emitFilter();
|
|
350
|
+
this._cdr.markForCheck();
|
|
351
|
+
}
|
|
352
|
+
// ── Dynamic slice management
|
|
293
353
|
addDynamicSlice(column) {
|
|
294
354
|
const distinctValues = this._computeDistinctValues(column.key);
|
|
295
355
|
const newSlice = {
|
|
@@ -328,7 +388,7 @@ class SlicePanelComponent {
|
|
|
328
388
|
const sliceIndex = this.staticSliceCount + dynamicIndex;
|
|
329
389
|
this.removeDynamicSlice(sliceIndex);
|
|
330
390
|
}
|
|
331
|
-
// ── Private helpers
|
|
391
|
+
// ── Private helpers
|
|
332
392
|
_emitFilter() {
|
|
333
393
|
const slices = this.slices;
|
|
334
394
|
const activeKeysBySlice = this.activeKeysBySlice;
|
|
@@ -342,7 +402,10 @@ class SlicePanelComponent {
|
|
|
342
402
|
return true;
|
|
343
403
|
return [...activeKeys].some((key) => {
|
|
344
404
|
const category = (slice.categories || []).find((c) => c.key === key);
|
|
345
|
-
|
|
405
|
+
// Si pas de filter défini (usage chart), on ne filtre pas en mémoire
|
|
406
|
+
if (!category?.filter)
|
|
407
|
+
return true;
|
|
408
|
+
return category.filter(row);
|
|
346
409
|
});
|
|
347
410
|
});
|
|
348
411
|
this.filterChange.emit(pred);
|
|
@@ -416,8 +479,7 @@ class SlicePanelComponent {
|
|
|
416
479
|
return row[columnKey];
|
|
417
480
|
}
|
|
418
481
|
_rebuildCache() {
|
|
419
|
-
const
|
|
420
|
-
const allSlices = [...staticSlices, ...this._dynamicSlices.map((d) => d.slice)];
|
|
482
|
+
const allSlices = [...(this.sliceConfigs || []), ...this._dynamicSlices.map((d) => d.slice)];
|
|
421
483
|
this._cachedSlices = allSlices.map((slice) => this._materializeSlice(slice));
|
|
422
484
|
this._countCache.clear();
|
|
423
485
|
}
|
|
@@ -445,11 +507,11 @@ class SlicePanelComponent {
|
|
|
445
507
|
}, 0);
|
|
446
508
|
}
|
|
447
509
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SlicePanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
448
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: SlicePanelComponent, isStandalone: true, selector: "slice-panel", inputs: { sliceConfigs: "sliceConfigs", columns: "columns", data: "data", lazyData: "lazyData", lazyStatus: "lazyStatus", showToggle: "showToggle", showCounts: "showCounts", collapsedByDefault: "collapsedByDefault", alwaysShow: "alwaysShow" }, outputs: { filterChange: "filterChange", activeKeysChange: "activeKeysChange", dynamicSliceKeysChange: "dynamicSliceKeysChange", collapsedChange: "collapsedChange" }, viewQueries: [{ propertyName: "slicePanelBodyRef", first: true, predicate: ["slicePanelBody"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"slice-panel-wrapper\" [class.collapsed]=\"isCollapsed\">\n <aside class=\"slice-panel\" [class.collapsed]=\"isCollapsed\">\n <div class=\"slice-panel-header\">\n <span *ngIf=\"!isCollapsed\" class=\"slice-panel-title\">\n <mat-icon class=\"slice-panel-title-icon\"
|
|
510
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: SlicePanelComponent, isStandalone: true, selector: "slice-panel", inputs: { sliceConfigs: "sliceConfigs", columns: "columns", data: "data", lazyData: "lazyData", lazyStatus: "lazyStatus", showToggle: "showToggle", showCounts: "showCounts", collapsedByDefault: "collapsedByDefault", alwaysShow: "alwaysShow" }, outputs: { filterChange: "filterChange", activeKeysChange: "activeKeysChange", dynamicSliceKeysChange: "dynamicSliceKeysChange", collapsedChange: "collapsedChange" }, viewQueries: [{ propertyName: "slicePanelBodyRef", first: true, predicate: ["slicePanelBody"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"slice-panel-wrapper\" [class.collapsed]=\"isCollapsed\">\n <aside class=\"slice-panel\" [class.collapsed]=\"isCollapsed\">\n <div class=\"slice-panel-header\">\n <span *ngIf=\"!isCollapsed\" class=\"slice-panel-title\">\n <mat-icon class=\"slice-panel-title-icon\">filter_list</mat-icon>\n {{ i18n.filterPanelTitle(slices.length) }}\n </span>\n <button\n *ngIf=\"showToggle\"\n type=\"button\"\n class=\"slice-toggle\"\n [attr.aria-label]=\"isCollapsed ? i18n.expandFilterTitle : i18n.collapseFilterTitle\"\n (click)=\"togglePanel()\"\n >\n <mat-icon class=\"slice-toggle-mat-icon\">{{ isCollapsed ? 'menu' : 'menu_open' }}</mat-icon>\n </button>\n </div>\n\n <div class=\"slice-panel-body\" *ngIf=\"!isCollapsed\" #slicePanelBody>\n <ng-container *ngFor=\"let slice of slices; let si = index; trackBy: trackBySliceIndex\">\n <div class=\"slice-title-row\" [attr.data-slice-index]=\"si\" (click)=\"toggleSliceCollapse(si)\">\n <span class=\"slice-title-chevron\" [class.expanded]=\"!isSliceCollapsed(si)\">\u203A</span>\n <mat-icon *ngIf=\"getSliceIcon(slice)\" class=\"slice-title-icon\">{{ getSliceIcon(slice) }}</mat-icon>\n <p class=\"slice-title\">{{ slice.title || i18n.filterBlockDefaultTitle }}</p>\n <button\n *ngIf=\"isSliceDynamic(si)\"\n type=\"button\"\n class=\"slice-remove-btn\"\n (click)=\"$event.stopPropagation(); removeDynamicSlice(si)\"\n >\u00D7</button>\n </div>\n\n <div class=\"slice-section-items\" *ngIf=\"!isSliceCollapsed(si)\">\n <ng-container *ngIf=\"isSliceLoading(si); else sliceContent\">\n <div class=\"slice-lazy-loading\">\n <span class=\"lazy-skeleton\"></span>\n <span class=\"lazy-skeleton\"></span>\n <span class=\"lazy-skeleton\"></span>\n </div>\n </ng-container>\n <ng-template #sliceContent>\n <ng-container *ngFor=\"let category of slice.categories; trackBy: trackByCategoryKey\">\n <button\n *ngIf=\"sliceCategoryCount(si, category.key) > 0\"\n class=\"slice-item\"\n type=\"button\"\n [class.active]=\"isSliceCategoryActive(si, category.key)\"\n [attr.data-category-key]=\"category.key\"\n [title]=\"category.label\"\n (click)=\"onSliceCategorySelected(si, category.key)\"\n >\n <span>{{ category.label }}</span>\n <span *ngIf=\"showCounts\">{{ sliceCategoryCount(si, category.key) }}</span>\n </button>\n </ng-container>\n <div *ngIf=\"isSliceEmpty(si)\" class=\"slice-empty-state\">{{ i18n.filterEmptyState }}</div>\n </ng-template>\n </div>\n\n <hr *ngIf=\"si < slices.length - 1\" class=\"slice-divider\" />\n </ng-container>\n </div>\n </aside>\n</div>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0;box-sizing:border-box}.slice-panel-wrapper{height:100%;max-height:100%;overflow-y:auto;box-sizing:border-box}.slice-panel{margin-top:0;border:1px solid var(--jqt-border-color);border-radius:10px;background:var(--jqt-surface-color);display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden;box-sizing:border-box}.slice-panel-header{flex-shrink:0;display:flex;justify-content:flex-end;align-items:center;height:34px;padding:0 8px;margin-bottom:8px}.slice-panel-header:has(.slice-panel-title){justify-content:space-between;padding-left:12px}.slice-panel-body{flex:1 1 0;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:4px;padding:4px 10px 10px;scrollbar-width:thin;scrollbar-color:var(--jqt-border-color) transparent}.slice-panel-body::-webkit-scrollbar{width:2px}.slice-panel-body::-webkit-scrollbar-thumb{background:var(--jqt-primary-color);border-radius:4px}.slice-panel-title{margin:0;padding-top:2px;font-size:11px;font-weight:700;line-height:1;text-transform:uppercase;letter-spacing:.08em;color:var(--jqt-text-color);-webkit-user-select:none;user-select:none;align-self:center;display:inline-flex;align-items:center;gap:5px}.slice-panel-title .slice-panel-title-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.slice-section-items{display:flex;flex-direction:column;gap:2px;flex:0 0 auto;scrollbar-width:thin;scrollbar-color:var(--jqt-border-color) transparent}.slice-section-items::-webkit-scrollbar{width:2px}.slice-section-items .slice-empty-state{padding:8px 10px;font-size:12px;color:var(--jqt-text-muted, #9ca3af);font-style:italic;text-align:center}.slice-section-items::-webkit-scrollbar-thumb{background:var(--jqt-border-color);border-radius:4px}.slice-toggle{border:0;background:var(--jqt-surface-color);color:var(--jqt-text-color);border-radius:6px;width:22px;height:22px;padding:0;font-size:14px;text-align:center;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;line-height:1}.slice-toggle-mat-icon{font-size:16px;width:16px;height:16px;line-height:1}.slice-panel.collapsed{border-color:transparent;background:transparent;overflow:visible}.slice-panel.collapsed .slice-panel-header{height:34px;padding:0;justify-content:center;align-items:center}.slice-panel.collapsed .slice-toggle{width:34px;height:34px;border:1px solid var(--jqt-border-color);background:var(--jqt-surface-color);border-radius:8px}.slice-panel.collapsed .slice-toggle:hover{border-color:var(--jqt-text-muted)}.slice-panel.collapsed .slice-toggle .slice-toggle-mat-icon{font-size:18px;width:18px;height:18px}.slice-title-row{display:flex;align-items:center;justify-content:space-between;gap:6px;margin:4px 0 6px;cursor:pointer;-webkit-user-select:none;user-select:none}.slice-title-row:hover .slice-title{color:var(--jqt-text-color)}.slice-title-row:hover .slice-title-chevron{color:var(--jqt-text-secondary)}.slice-title-row:first-of-type .slice-title{margin-top:0}.slice-title-chevron{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;font-size:16px;line-height:1;color:var(--jqt-text-muted);transition:transform .15s ease;transform:rotate(0);position:relative;top:-1px}.slice-title-chevron.expanded{transform:rotate(90deg)}.slice-title{flex:1 1 auto;margin:0;font-size:14px;font-weight:600;color:var(--jqt-title-color)}.slice-title-icon{flex-shrink:0;font-size:14px!important;width:14px!important;height:14px!important;color:var(--jqt-title-color)}.slice-remove-btn{background:none;border:none;cursor:pointer;color:var(--jqt-border-color);font-size:12px;padding:0 2px;line-height:1;flex-shrink:0;opacity:.5;transition:opacity .15s,color .15s}.slice-remove-btn:hover{color:var(--jqt-text-secondary);opacity:1}.slice-title-row:hover .slice-remove-btn{opacity:.8}.slice-item{display:flex;justify-content:space-between;align-items:center;text-align:left;width:100%;padding:5px 10px 5px 16px;background:transparent;border:0;cursor:pointer;border-radius:8px;color:var(--jqt-text-color);box-sizing:border-box;min-width:0;gap:8px}.slice-item.active{background:var(--jqt-row-hover-bg);font-weight:600}.slice-item.slice-multi{padding-left:6px}.slice-item span:first-child{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.slice-item span:last-child{flex:0 0 auto;min-width:1.2em;text-align:right}.slice-check-box{flex:0 0 16px;width:16px;height:16px;border:1.5px solid var(--jqt-text-muted);border-radius:4px;background:var(--jqt-surface-color);display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;transition:background .15s,border-color .15s}.slice-check-box.checked{background:var(--jqt-text-secondary);border-color:var(--jqt-text-secondary)}.slice-check-box.checked:after{content:\"\";display:block;width:9px;height:5px;border-left:2px solid #fff;border-bottom:2px solid #fff;transform:rotate(-45deg) translateY(-1px)}.slice-divider{border:none;border-top:1px solid var(--jqt-border-color);margin:6px 0}.lazy-skeleton{display:block;height:12px;width:70%;border-radius:4px;background:linear-gradient(90deg,var(--jqt-border-color) 25%,var(--jqt-surface-alt-color) 50%,var(--jqt-border-color) 75%);background-size:200% 100%;animation:lazy-shimmer 1.4s ease-in-out infinite}.slice-lazy-loading{display:flex;flex-direction:column;gap:8px;padding:8px 10px}.slice-lazy-loading .lazy-skeleton{width:80%}.slice-lazy-loading .lazy-skeleton:nth-child(2){width:60%}.slice-lazy-loading .lazy-skeleton:nth-child(3){width:45%}@keyframes lazy-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\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: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
449
511
|
}
|
|
450
512
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SlicePanelComponent, decorators: [{
|
|
451
513
|
type: Component,
|
|
452
|
-
args: [{ standalone: true, selector: 'slice-panel', imports: [CommonModule, MatIconModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"slice-panel-wrapper\" [class.collapsed]=\"isCollapsed\">\n <aside class=\"slice-panel\" [class.collapsed]=\"isCollapsed\">\n <div class=\"slice-panel-header\">\n <span *ngIf=\"!isCollapsed\" class=\"slice-panel-title\">\n <mat-icon class=\"slice-panel-title-icon\"
|
|
514
|
+
args: [{ standalone: true, selector: 'slice-panel', imports: [CommonModule, MatIconModule], changeDetection: ChangeDetectionStrategy.OnPush, template: "<div class=\"slice-panel-wrapper\" [class.collapsed]=\"isCollapsed\">\n <aside class=\"slice-panel\" [class.collapsed]=\"isCollapsed\">\n <div class=\"slice-panel-header\">\n <span *ngIf=\"!isCollapsed\" class=\"slice-panel-title\">\n <mat-icon class=\"slice-panel-title-icon\">filter_list</mat-icon>\n {{ i18n.filterPanelTitle(slices.length) }}\n </span>\n <button\n *ngIf=\"showToggle\"\n type=\"button\"\n class=\"slice-toggle\"\n [attr.aria-label]=\"isCollapsed ? i18n.expandFilterTitle : i18n.collapseFilterTitle\"\n (click)=\"togglePanel()\"\n >\n <mat-icon class=\"slice-toggle-mat-icon\">{{ isCollapsed ? 'menu' : 'menu_open' }}</mat-icon>\n </button>\n </div>\n\n <div class=\"slice-panel-body\" *ngIf=\"!isCollapsed\" #slicePanelBody>\n <ng-container *ngFor=\"let slice of slices; let si = index; trackBy: trackBySliceIndex\">\n <div class=\"slice-title-row\" [attr.data-slice-index]=\"si\" (click)=\"toggleSliceCollapse(si)\">\n <span class=\"slice-title-chevron\" [class.expanded]=\"!isSliceCollapsed(si)\">\u203A</span>\n <mat-icon *ngIf=\"getSliceIcon(slice)\" class=\"slice-title-icon\">{{ getSliceIcon(slice) }}</mat-icon>\n <p class=\"slice-title\">{{ slice.title || i18n.filterBlockDefaultTitle }}</p>\n <button\n *ngIf=\"isSliceDynamic(si)\"\n type=\"button\"\n class=\"slice-remove-btn\"\n (click)=\"$event.stopPropagation(); removeDynamicSlice(si)\"\n >\u00D7</button>\n </div>\n\n <div class=\"slice-section-items\" *ngIf=\"!isSliceCollapsed(si)\">\n <ng-container *ngIf=\"isSliceLoading(si); else sliceContent\">\n <div class=\"slice-lazy-loading\">\n <span class=\"lazy-skeleton\"></span>\n <span class=\"lazy-skeleton\"></span>\n <span class=\"lazy-skeleton\"></span>\n </div>\n </ng-container>\n <ng-template #sliceContent>\n <ng-container *ngFor=\"let category of slice.categories; trackBy: trackByCategoryKey\">\n <button\n *ngIf=\"sliceCategoryCount(si, category.key) > 0\"\n class=\"slice-item\"\n type=\"button\"\n [class.active]=\"isSliceCategoryActive(si, category.key)\"\n [attr.data-category-key]=\"category.key\"\n [title]=\"category.label\"\n (click)=\"onSliceCategorySelected(si, category.key)\"\n >\n <span>{{ category.label }}</span>\n <span *ngIf=\"showCounts\">{{ sliceCategoryCount(si, category.key) }}</span>\n </button>\n </ng-container>\n <div *ngIf=\"isSliceEmpty(si)\" class=\"slice-empty-state\">{{ i18n.filterEmptyState }}</div>\n </ng-template>\n </div>\n\n <hr *ngIf=\"si < slices.length - 1\" class=\"slice-divider\" />\n </ng-container>\n </div>\n </aside>\n</div>\n", styles: [":host{display:block;width:100%;height:100%;min-height:0;box-sizing:border-box}.slice-panel-wrapper{height:100%;max-height:100%;overflow-y:auto;box-sizing:border-box}.slice-panel{margin-top:0;border:1px solid var(--jqt-border-color);border-radius:10px;background:var(--jqt-surface-color);display:flex;flex-direction:column;min-width:0;height:100%;overflow:hidden;box-sizing:border-box}.slice-panel-header{flex-shrink:0;display:flex;justify-content:flex-end;align-items:center;height:34px;padding:0 8px;margin-bottom:8px}.slice-panel-header:has(.slice-panel-title){justify-content:space-between;padding-left:12px}.slice-panel-body{flex:1 1 0;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:4px;padding:4px 10px 10px;scrollbar-width:thin;scrollbar-color:var(--jqt-border-color) transparent}.slice-panel-body::-webkit-scrollbar{width:2px}.slice-panel-body::-webkit-scrollbar-thumb{background:var(--jqt-primary-color);border-radius:4px}.slice-panel-title{margin:0;padding-top:2px;font-size:11px;font-weight:700;line-height:1;text-transform:uppercase;letter-spacing:.08em;color:var(--jqt-text-color);-webkit-user-select:none;user-select:none;align-self:center;display:inline-flex;align-items:center;gap:5px}.slice-panel-title .slice-panel-title-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.slice-section-items{display:flex;flex-direction:column;gap:2px;flex:0 0 auto;scrollbar-width:thin;scrollbar-color:var(--jqt-border-color) transparent}.slice-section-items::-webkit-scrollbar{width:2px}.slice-section-items .slice-empty-state{padding:8px 10px;font-size:12px;color:var(--jqt-text-muted, #9ca3af);font-style:italic;text-align:center}.slice-section-items::-webkit-scrollbar-thumb{background:var(--jqt-border-color);border-radius:4px}.slice-toggle{border:0;background:var(--jqt-surface-color);color:var(--jqt-text-color);border-radius:6px;width:22px;height:22px;padding:0;font-size:14px;text-align:center;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;line-height:1}.slice-toggle-mat-icon{font-size:16px;width:16px;height:16px;line-height:1}.slice-panel.collapsed{border-color:transparent;background:transparent;overflow:visible}.slice-panel.collapsed .slice-panel-header{height:34px;padding:0;justify-content:center;align-items:center}.slice-panel.collapsed .slice-toggle{width:34px;height:34px;border:1px solid var(--jqt-border-color);background:var(--jqt-surface-color);border-radius:8px}.slice-panel.collapsed .slice-toggle:hover{border-color:var(--jqt-text-muted)}.slice-panel.collapsed .slice-toggle .slice-toggle-mat-icon{font-size:18px;width:18px;height:18px}.slice-title-row{display:flex;align-items:center;justify-content:space-between;gap:6px;margin:4px 0 6px;cursor:pointer;-webkit-user-select:none;user-select:none}.slice-title-row:hover .slice-title{color:var(--jqt-text-color)}.slice-title-row:hover .slice-title-chevron{color:var(--jqt-text-secondary)}.slice-title-row:first-of-type .slice-title{margin-top:0}.slice-title-chevron{flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;font-size:16px;line-height:1;color:var(--jqt-text-muted);transition:transform .15s ease;transform:rotate(0);position:relative;top:-1px}.slice-title-chevron.expanded{transform:rotate(90deg)}.slice-title{flex:1 1 auto;margin:0;font-size:14px;font-weight:600;color:var(--jqt-title-color)}.slice-title-icon{flex-shrink:0;font-size:14px!important;width:14px!important;height:14px!important;color:var(--jqt-title-color)}.slice-remove-btn{background:none;border:none;cursor:pointer;color:var(--jqt-border-color);font-size:12px;padding:0 2px;line-height:1;flex-shrink:0;opacity:.5;transition:opacity .15s,color .15s}.slice-remove-btn:hover{color:var(--jqt-text-secondary);opacity:1}.slice-title-row:hover .slice-remove-btn{opacity:.8}.slice-item{display:flex;justify-content:space-between;align-items:center;text-align:left;width:100%;padding:5px 10px 5px 16px;background:transparent;border:0;cursor:pointer;border-radius:8px;color:var(--jqt-text-color);box-sizing:border-box;min-width:0;gap:8px}.slice-item.active{background:var(--jqt-row-hover-bg);font-weight:600}.slice-item.slice-multi{padding-left:6px}.slice-item span:first-child{flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.slice-item span:last-child{flex:0 0 auto;min-width:1.2em;text-align:right}.slice-check-box{flex:0 0 16px;width:16px;height:16px;border:1.5px solid var(--jqt-text-muted);border-radius:4px;background:var(--jqt-surface-color);display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;transition:background .15s,border-color .15s}.slice-check-box.checked{background:var(--jqt-text-secondary);border-color:var(--jqt-text-secondary)}.slice-check-box.checked:after{content:\"\";display:block;width:9px;height:5px;border-left:2px solid #fff;border-bottom:2px solid #fff;transform:rotate(-45deg) translateY(-1px)}.slice-divider{border:none;border-top:1px solid var(--jqt-border-color);margin:6px 0}.lazy-skeleton{display:block;height:12px;width:70%;border-radius:4px;background:linear-gradient(90deg,var(--jqt-border-color) 25%,var(--jqt-surface-alt-color) 50%,var(--jqt-border-color) 75%);background-size:200% 100%;animation:lazy-shimmer 1.4s ease-in-out infinite}.slice-lazy-loading{display:flex;flex-direction:column;gap:8px;padding:8px 10px}.slice-lazy-loading .lazy-skeleton{width:80%}.slice-lazy-loading .lazy-skeleton:nth-child(2){width:60%}.slice-lazy-loading .lazy-skeleton:nth-child(3){width:45%}@keyframes lazy-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}\n"] }]
|
|
453
515
|
}], propDecorators: { sliceConfigs: [{
|
|
454
516
|
type: Input
|
|
455
517
|
}], columns: [{
|
|
@@ -512,7 +574,7 @@ function normalizeCellValue(value) {
|
|
|
512
574
|
return '';
|
|
513
575
|
}
|
|
514
576
|
if (value instanceof Date) {
|
|
515
|
-
return value.toLocaleString(
|
|
577
|
+
return value.toLocaleString();
|
|
516
578
|
}
|
|
517
579
|
if (Array.isArray(value)) {
|
|
518
580
|
return value.map((item) => normalizeCellValue(item)).join(', ');
|
|
@@ -539,6 +601,611 @@ function humanizeKey(key) {
|
|
|
539
601
|
.replace(/^./, (value) => value.toUpperCase());
|
|
540
602
|
}
|
|
541
603
|
|
|
604
|
+
// ── Facade
|
|
605
|
+
/**
|
|
606
|
+
* Centralise l'état et les actions du panneau View (Champs / Group by / Slice by).
|
|
607
|
+
* Pas de dépendance Angular/DOM : peut être extraite vers jquery-core à terme.
|
|
608
|
+
*
|
|
609
|
+
* Usage :
|
|
610
|
+
* this._view = new ViewFacade();
|
|
611
|
+
* this._view.events$.subscribe(e => { ... });
|
|
612
|
+
* this._view.update({ config, columns, sliceConfigs });
|
|
613
|
+
*/
|
|
614
|
+
class ViewFacade {
|
|
615
|
+
constructor() {
|
|
616
|
+
// ── State
|
|
617
|
+
this.fields = {
|
|
618
|
+
activeColumns: [],
|
|
619
|
+
userCustomized: false,
|
|
620
|
+
};
|
|
621
|
+
this.groupBy = {
|
|
622
|
+
activeKey: null,
|
|
623
|
+
};
|
|
624
|
+
this.sliceBy = {
|
|
625
|
+
activeDynamicColumns: [],
|
|
626
|
+
availableForDynamic: [],
|
|
627
|
+
allDynamicColumns: [],
|
|
628
|
+
activeLabel: 'Aucun',
|
|
629
|
+
};
|
|
630
|
+
this._panelConfig = { columns: [], sliceConfigs: [] };
|
|
631
|
+
this._hiddenStaticKeys = new Set();
|
|
632
|
+
// ── Events
|
|
633
|
+
this._events$ = new Subject();
|
|
634
|
+
this.events$ = this._events$.asObservable();
|
|
635
|
+
}
|
|
636
|
+
// ── Computed flags
|
|
637
|
+
get enabled() {
|
|
638
|
+
return this._panelConfig.config?.enabled === true;
|
|
639
|
+
}
|
|
640
|
+
get showFields() {
|
|
641
|
+
return this.enabled;
|
|
642
|
+
}
|
|
643
|
+
get showGroupBySection() {
|
|
644
|
+
return this.enabled && this.groupByColumns.length > 0;
|
|
645
|
+
}
|
|
646
|
+
get showSliceBySection() {
|
|
647
|
+
return this.enabled && ((this._panelConfig.sliceConfigs?.length ?? 0) > 0 ||
|
|
648
|
+
this.sliceBy.allDynamicColumns.length > 0);
|
|
649
|
+
}
|
|
650
|
+
get allowColumnRemoval() {
|
|
651
|
+
return this._panelConfig.config?.enableColumnRemoval !== false;
|
|
652
|
+
}
|
|
653
|
+
get isColumnDragDropEnabled() {
|
|
654
|
+
return this._panelConfig.config?.enableColumnDragDrop === true;
|
|
655
|
+
}
|
|
656
|
+
get menuBaseColumns() {
|
|
657
|
+
return this._uniqueByKey((this._panelConfig.columns || []).filter(c => !c.optional));
|
|
658
|
+
}
|
|
659
|
+
get menuOptionalColumns() {
|
|
660
|
+
return this._uniqueByKey((this._panelConfig.columns || []).filter(c => c.optional));
|
|
661
|
+
}
|
|
662
|
+
get totalColumnCount() {
|
|
663
|
+
return new Set((this._panelConfig.columns || []).map(c => c.key)).size;
|
|
664
|
+
}
|
|
665
|
+
get groupByColumns() {
|
|
666
|
+
const seen = new Set();
|
|
667
|
+
return (this._panelConfig.columns || []).filter(c => {
|
|
668
|
+
if (c.groupable === false || seen.has(c.key))
|
|
669
|
+
return false;
|
|
670
|
+
if (!c.header && c.groupable !== true)
|
|
671
|
+
return false;
|
|
672
|
+
seen.add(c.key);
|
|
673
|
+
return true;
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
get activeGroupByLabel() {
|
|
677
|
+
if (!this.groupBy.activeKey)
|
|
678
|
+
return this._panelConfig.noneLabel ?? 'Aucun';
|
|
679
|
+
const col = this.groupByColumns.find(c => c.key === this.groupBy.activeKey);
|
|
680
|
+
return col?.header || this.groupBy.activeKey;
|
|
681
|
+
}
|
|
682
|
+
get staticSlicesForMenu() {
|
|
683
|
+
return (this._panelConfig.sliceConfigs ?? [])
|
|
684
|
+
.filter(s => !!s.columnKey)
|
|
685
|
+
.map(s => {
|
|
686
|
+
const colIcon = (this._panelConfig.columns ?? []).find(c => c.key === s.columnKey)?.icon;
|
|
687
|
+
return { key: s.columnKey, title: s.title || humanizeKey(s.columnKey), icon: s.icon ?? colIcon };
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
colLabel(col) {
|
|
691
|
+
return col.header || humanizeKey(col.key);
|
|
692
|
+
}
|
|
693
|
+
// ── Mise à jour de la config (appelée depuis refreshViewModel)
|
|
694
|
+
update(panelConfig) {
|
|
695
|
+
this._panelConfig = panelConfig;
|
|
696
|
+
this._refreshDefaultColumns();
|
|
697
|
+
this._refreshDynamicSliceMeta();
|
|
698
|
+
}
|
|
699
|
+
// ── Actions Fields
|
|
700
|
+
setActiveColumns(columns) {
|
|
701
|
+
this.fields.activeColumns = columns;
|
|
702
|
+
this.fields.userCustomized = true;
|
|
703
|
+
}
|
|
704
|
+
setUserCustomized(value) {
|
|
705
|
+
this.fields.userCustomized = value;
|
|
706
|
+
}
|
|
707
|
+
/** Remet la vue dans son état initial : colonnes par défaut, pas de groupBy, pas de dynamic slices. */
|
|
708
|
+
resetToDefaults() {
|
|
709
|
+
this.fields.userCustomized = false;
|
|
710
|
+
this._refreshDefaultColumns();
|
|
711
|
+
this.groupBy.activeKey = null;
|
|
712
|
+
this.sliceBy.activeDynamicColumns = [];
|
|
713
|
+
this._refreshDynamicSliceMeta();
|
|
714
|
+
}
|
|
715
|
+
addColumn(column) {
|
|
716
|
+
const configCols = this._panelConfig.columns ?? [];
|
|
717
|
+
const configIndex = configCols.findIndex(c => c.key === column.key);
|
|
718
|
+
if (configIndex === -1) {
|
|
719
|
+
this.fields.activeColumns = [...this.fields.activeColumns, column];
|
|
720
|
+
}
|
|
721
|
+
else {
|
|
722
|
+
const insertBefore = this.fields.activeColumns.findIndex(c => {
|
|
723
|
+
const ci = configCols.findIndex(cc => cc.key === c.key);
|
|
724
|
+
return ci !== -1 && ci > configIndex;
|
|
725
|
+
});
|
|
726
|
+
if (insertBefore === -1) {
|
|
727
|
+
this.fields.activeColumns = [...this.fields.activeColumns, column];
|
|
728
|
+
}
|
|
729
|
+
else {
|
|
730
|
+
const copy = [...this.fields.activeColumns];
|
|
731
|
+
copy.splice(insertBefore, 0, column);
|
|
732
|
+
this.fields.activeColumns = copy;
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
this.fields.userCustomized = true;
|
|
736
|
+
this._events$.next({ type: 'fieldsChanged', fieldIds: this.fields.activeColumns.map(c => c.key) });
|
|
737
|
+
}
|
|
738
|
+
removeColumn(columnKey) {
|
|
739
|
+
const col = this.fields.activeColumns.find(c => c.key === columnKey);
|
|
740
|
+
if (!col)
|
|
741
|
+
return false;
|
|
742
|
+
if (!col.optional && !this.allowColumnRemoval)
|
|
743
|
+
return false;
|
|
744
|
+
if (this.fields.activeColumns.length <= 1)
|
|
745
|
+
return false;
|
|
746
|
+
this.fields.activeColumns = this.fields.activeColumns.filter(c => c.key !== columnKey);
|
|
747
|
+
this.fields.userCustomized = true;
|
|
748
|
+
this._events$.next({ type: 'fieldsChanged', fieldIds: this.fields.activeColumns.map(c => c.key) });
|
|
749
|
+
return true;
|
|
750
|
+
}
|
|
751
|
+
toggleColumnVisibility(column, visible) {
|
|
752
|
+
const isVisible = this.isColumnVisible(column.key);
|
|
753
|
+
if (visible && !isVisible) {
|
|
754
|
+
this.addColumn(column);
|
|
755
|
+
}
|
|
756
|
+
else if (!visible && isVisible) {
|
|
757
|
+
this.removeColumn(column.key);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
reorderColumns(fromKey, toKey) {
|
|
761
|
+
const prev = this.fields.activeColumns.findIndex(c => c.key === fromKey);
|
|
762
|
+
const curr = this.fields.activeColumns.findIndex(c => c.key === toKey);
|
|
763
|
+
if (prev === -1 || curr === -1 || prev === curr)
|
|
764
|
+
return false;
|
|
765
|
+
const reordered = [...this.fields.activeColumns];
|
|
766
|
+
const [moved] = reordered.splice(prev, 1);
|
|
767
|
+
reordered.splice(curr, 0, moved);
|
|
768
|
+
this.fields.activeColumns = reordered;
|
|
769
|
+
this.fields.userCustomized = true;
|
|
770
|
+
this._events$.next({ type: 'fieldsChanged', fieldIds: this.fields.activeColumns.map(c => c.key) });
|
|
771
|
+
return true;
|
|
772
|
+
}
|
|
773
|
+
isColumnVisible(key) {
|
|
774
|
+
return this.fields.activeColumns.some(c => c.key === key);
|
|
775
|
+
}
|
|
776
|
+
// ── Actions Group by
|
|
777
|
+
setGroupBy(key) {
|
|
778
|
+
this.groupBy.activeKey = key;
|
|
779
|
+
this._events$.next({ type: 'groupByChanged', key });
|
|
780
|
+
}
|
|
781
|
+
// ── Actions Slice by (dynamique)
|
|
782
|
+
/** Appelée quand le SlicePanelComponent émet dynamicSliceKeysChange */
|
|
783
|
+
onDynamicSliceKeysChange(keys) {
|
|
784
|
+
const allCols = this._panelConfig.columns ?? [];
|
|
785
|
+
const staticKeys = new Set((this._panelConfig.sliceConfigs ?? []).map(s => s.columnKey).filter((k) => !!k));
|
|
786
|
+
const dynamicKeySet = new Set(keys);
|
|
787
|
+
const existingKeys = new Set([...staticKeys, ...dynamicKeySet]);
|
|
788
|
+
this.sliceBy.activeDynamicColumns = keys
|
|
789
|
+
.map(k => allCols.find(c => c.key === k))
|
|
790
|
+
.filter((c) => !!c);
|
|
791
|
+
const seen = new Set();
|
|
792
|
+
this.sliceBy.availableForDynamic = allCols.filter(c => {
|
|
793
|
+
if (c.sliceable === false || existingKeys.has(c.key) || seen.has(c.key))
|
|
794
|
+
return false;
|
|
795
|
+
seen.add(c.key);
|
|
796
|
+
return true;
|
|
797
|
+
});
|
|
798
|
+
this._computeSliceLabel();
|
|
799
|
+
this._events$.next({ type: 'dynamicSlicesChanged', keys });
|
|
800
|
+
}
|
|
801
|
+
isDynamicSliceActive(key) {
|
|
802
|
+
return this.sliceBy.activeDynamicColumns.some(c => c.key === key);
|
|
803
|
+
}
|
|
804
|
+
// ── Contrat core
|
|
805
|
+
/** Retourne un snapshot ViewState (contrat core) de l'état courant. */
|
|
806
|
+
toViewState() {
|
|
807
|
+
return {
|
|
808
|
+
selectedFieldIds: this.fields.activeColumns.map(c => c.key),
|
|
809
|
+
groupByKey: this.groupBy.activeKey,
|
|
810
|
+
dynamicSliceKeys: this.sliceBy.activeDynamicColumns.map(c => c.key),
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
/** Convertit une TableColumnProvider en ViewField (contrat core). */
|
|
814
|
+
static toViewField(col) {
|
|
815
|
+
return {
|
|
816
|
+
id: col.key,
|
|
817
|
+
label: col.header,
|
|
818
|
+
icon: col.icon,
|
|
819
|
+
groupable: col.groupable,
|
|
820
|
+
sliceable: col.sliceable,
|
|
821
|
+
optional: col.optional,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
// ── Destructor
|
|
825
|
+
destroy() {
|
|
826
|
+
this._events$.complete();
|
|
827
|
+
}
|
|
828
|
+
// ── Helpers privés
|
|
829
|
+
_refreshDefaultColumns() {
|
|
830
|
+
const defaultCols = (this._panelConfig.columns || []).filter(c => !c.optional);
|
|
831
|
+
if (!this.fields.userCustomized || this.fields.activeColumns.length === 0) {
|
|
832
|
+
this.fields.activeColumns = [...defaultCols];
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
const allowedKeys = new Set((this._panelConfig.columns || []).map(c => c.key));
|
|
836
|
+
this.fields.activeColumns = this.fields.activeColumns.filter(c => allowedKeys.has(c.key));
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
_refreshDynamicSliceMeta() {
|
|
840
|
+
const allCols = this._panelConfig.columns ?? [];
|
|
841
|
+
const staticKeys = new Set((this._panelConfig.sliceConfigs ?? []).map(s => s.columnKey).filter((k) => !!k));
|
|
842
|
+
const activeDynKeys = new Set(this.sliceBy.activeDynamicColumns.map(c => c.key));
|
|
843
|
+
const seen1 = new Set();
|
|
844
|
+
this.sliceBy.availableForDynamic = allCols.filter(c => {
|
|
845
|
+
if (c.sliceable === false || staticKeys.has(c.key) || activeDynKeys.has(c.key) || seen1.has(c.key))
|
|
846
|
+
return false;
|
|
847
|
+
if (!c.header && c.sliceable !== true)
|
|
848
|
+
return false;
|
|
849
|
+
seen1.add(c.key);
|
|
850
|
+
return true;
|
|
851
|
+
});
|
|
852
|
+
const seen2 = new Set();
|
|
853
|
+
this.sliceBy.allDynamicColumns = allCols.filter(c => {
|
|
854
|
+
if (c.sliceable === false || staticKeys.has(c.key) || seen2.has(c.key))
|
|
855
|
+
return false;
|
|
856
|
+
if (!c.header && c.sliceable !== true)
|
|
857
|
+
return false;
|
|
858
|
+
seen2.add(c.key);
|
|
859
|
+
return true;
|
|
860
|
+
});
|
|
861
|
+
this._computeSliceLabel();
|
|
862
|
+
}
|
|
863
|
+
/** Notifie la facade des slices statiques actuellement cachées (géré par table.component).
|
|
864
|
+
* Recalcule le label slice afin qu'il reflète uniquement les slices visibles. */
|
|
865
|
+
updateHiddenStaticKeys(keys) {
|
|
866
|
+
this._hiddenStaticKeys = keys;
|
|
867
|
+
this._computeSliceLabel();
|
|
868
|
+
}
|
|
869
|
+
_computeSliceLabel() {
|
|
870
|
+
const visibleStatics = (this._panelConfig.sliceConfigs ?? []).filter(s => !s.columnKey || !this._hiddenStaticKeys.has(s.columnKey));
|
|
871
|
+
const visibleStaticCount = visibleStatics.length;
|
|
872
|
+
const total = visibleStaticCount + this.sliceBy.activeDynamicColumns.length;
|
|
873
|
+
if (total === 0) {
|
|
874
|
+
this.sliceBy.activeLabel = this._panelConfig.noneLabel ?? 'Aucun';
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
877
|
+
if (total === 1) {
|
|
878
|
+
if (visibleStaticCount === 1) {
|
|
879
|
+
const first = visibleStatics[0];
|
|
880
|
+
this.sliceBy.activeLabel = first?.title || first?.columnKey || '1';
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
this.sliceBy.activeLabel = this.sliceBy.activeDynamicColumns[0]?.header || '1';
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
this.sliceBy.activeLabel = `${total} actifs`;
|
|
887
|
+
}
|
|
888
|
+
_uniqueByKey(cols) {
|
|
889
|
+
const seen = new Set();
|
|
890
|
+
return cols.filter(c => {
|
|
891
|
+
if (seen.has(c.key))
|
|
892
|
+
return false;
|
|
893
|
+
seen.add(c.key);
|
|
894
|
+
return true;
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
/**
|
|
900
|
+
* Constantes sentinel utilisées pour les identifiants de colonnes et les valeurs
|
|
901
|
+
* projetées dans le datasource interne de la table.
|
|
902
|
+
*
|
|
903
|
+
* Centraliser ici évite les typos silencieuses et facilite les tests unitaires.
|
|
904
|
+
*/
|
|
905
|
+
/** Identifiant de la colonne virtuelle rendue sur toute la largeur pour les en-têtes de groupe. */
|
|
906
|
+
const GROUP_ROW_COLUMN = '__group_row';
|
|
907
|
+
/** Marqueur posé sur les objets de ligne pour signaler une ligne d'en-tête de groupe. */
|
|
908
|
+
const GROUP_HEADER_MARKER = '__groupHeader';
|
|
909
|
+
/** Clé du groupe spéciale « chargement lazy en cours ». */
|
|
910
|
+
const GROUP_KEY_LAZY_LOADING = '__lazy_loading__';
|
|
911
|
+
/** Clé du groupe spéciale « trop de groupes ». */
|
|
912
|
+
const GROUP_KEY_TOO_MANY = '__too_many_groups__';
|
|
913
|
+
/** Propriété injectée dans chaque ligne projetée : référence vers la ligne brute d'origine. */
|
|
914
|
+
const ROW_RAW_KEY = '__raw';
|
|
915
|
+
/** Propriété injectée dans chaque ligne projetée : index global de la ligne dans le jeu de données filtré. */
|
|
916
|
+
const ROW_INDEX_KEY = '__index';
|
|
917
|
+
/** Propriété injectée dans chaque ligne projetée : clé du groupe auquel appartient la ligne d'en-tête. */
|
|
918
|
+
const ROW_GROUP_KEY = '__groupKey';
|
|
919
|
+
/** Propriété injectée dans chaque ligne projetée : nombre de lignes dans le groupe. */
|
|
920
|
+
const ROW_GROUP_COUNT = '__groupCount';
|
|
921
|
+
/** Propriété injectée dans chaque ligne projetée : page courante du groupe. */
|
|
922
|
+
const ROW_GROUP_PAGE = '__groupPage';
|
|
923
|
+
/** Propriété injectée dans chaque ligne projetée : nombre de pages du groupe. */
|
|
924
|
+
const ROW_GROUP_PAGE_COUNT = '__groupPageCount';
|
|
925
|
+
/** Valeur sentinelle posée dans une cellule lazy en cours de chargement. */
|
|
926
|
+
const LAZY_LOADING_VALUE = '__lazy_loading__';
|
|
927
|
+
/** Valeur sentinelle posée dans une cellule lazy en erreur. */
|
|
928
|
+
const LAZY_ERROR_VALUE = '__lazy_error__';
|
|
929
|
+
|
|
930
|
+
/**
|
|
931
|
+
* Gère le cycle de vie des colonnes à chargement différé (lazy) :
|
|
932
|
+
* statuts, données, annulation des requêtes en cours.
|
|
933
|
+
*
|
|
934
|
+
* Pas de dépendance Angular/DOM — peut être instancié manuellement dans le composant.
|
|
935
|
+
*/
|
|
936
|
+
class LazyColumnManager {
|
|
937
|
+
constructor() {
|
|
938
|
+
this._status = new Map();
|
|
939
|
+
this._data = new Map();
|
|
940
|
+
this._cancels = new Map();
|
|
941
|
+
}
|
|
942
|
+
get status() {
|
|
943
|
+
return this._status;
|
|
944
|
+
}
|
|
945
|
+
get data() {
|
|
946
|
+
return this._data;
|
|
947
|
+
}
|
|
948
|
+
getStatus(key) {
|
|
949
|
+
return this._status.get(key) ?? 'idle';
|
|
950
|
+
}
|
|
951
|
+
getRenderType(rowValue) {
|
|
952
|
+
if (rowValue === LAZY_LOADING_VALUE)
|
|
953
|
+
return 'loading';
|
|
954
|
+
if (rowValue === LAZY_ERROR_VALUE)
|
|
955
|
+
return 'error';
|
|
956
|
+
return 'value';
|
|
957
|
+
}
|
|
958
|
+
/** Annule tous les fetches en cours et réinitialise l'état (appeler lors d'un changement de données). */
|
|
959
|
+
cancelAll() {
|
|
960
|
+
this._cancels.forEach(s => { s.next(); s.complete(); });
|
|
961
|
+
this._cancels.clear();
|
|
962
|
+
this._status = new Map();
|
|
963
|
+
this._data = new Map();
|
|
964
|
+
}
|
|
965
|
+
/** Réinitialise une colonne et relance son fetch. */
|
|
966
|
+
retry(column, resolvedData, callbacks) {
|
|
967
|
+
const k = column.key;
|
|
968
|
+
const prev = this._cancels.get(k);
|
|
969
|
+
if (prev) {
|
|
970
|
+
prev.next();
|
|
971
|
+
prev.complete();
|
|
972
|
+
}
|
|
973
|
+
this._cancels.delete(k);
|
|
974
|
+
const newStatus = new Map(this._status);
|
|
975
|
+
newStatus.delete(k);
|
|
976
|
+
this._status = newStatus;
|
|
977
|
+
const newData = new Map(this._data);
|
|
978
|
+
newData.delete(k);
|
|
979
|
+
this._data = newData;
|
|
980
|
+
this.fetch(column, resolvedData, callbacks);
|
|
981
|
+
}
|
|
982
|
+
/** Lance le chargement différé d'une colonne. No-op si déjà en cours. */
|
|
983
|
+
fetch(column, resolvedData, callbacks) {
|
|
984
|
+
if (!column.lazy)
|
|
985
|
+
return;
|
|
986
|
+
const k = column.key;
|
|
987
|
+
if (this._status.get(k) === 'loading')
|
|
988
|
+
return;
|
|
989
|
+
const prev = this._cancels.get(k);
|
|
990
|
+
if (prev) {
|
|
991
|
+
prev.next();
|
|
992
|
+
prev.complete();
|
|
993
|
+
}
|
|
994
|
+
const cancel$ = new Subject();
|
|
995
|
+
this._cancels.set(k, cancel$);
|
|
996
|
+
const newStatus = new Map(this._status);
|
|
997
|
+
newStatus.set(k, 'loading');
|
|
998
|
+
this._status = newStatus;
|
|
999
|
+
callbacks.onRefresh();
|
|
1000
|
+
column.lazy.fetchFn().pipe(take(1), takeUntil(cancel$)).subscribe({
|
|
1001
|
+
next: (values) => {
|
|
1002
|
+
const rowMap = new Map();
|
|
1003
|
+
values.forEach((value, i) => {
|
|
1004
|
+
if (i < resolvedData.length)
|
|
1005
|
+
rowMap.set(resolvedData[i], value);
|
|
1006
|
+
});
|
|
1007
|
+
const newData = new Map(this._data);
|
|
1008
|
+
newData.set(k, rowMap);
|
|
1009
|
+
this._data = newData;
|
|
1010
|
+
const newStatus2 = new Map(this._status);
|
|
1011
|
+
newStatus2.set(k, 'loaded');
|
|
1012
|
+
this._status = newStatus2;
|
|
1013
|
+
callbacks.onRefresh();
|
|
1014
|
+
callbacks.onMarkForCheck();
|
|
1015
|
+
},
|
|
1016
|
+
error: () => {
|
|
1017
|
+
const newStatusErr = new Map(this._status);
|
|
1018
|
+
newStatusErr.set(k, 'error');
|
|
1019
|
+
this._status = newStatusErr;
|
|
1020
|
+
callbacks.onRefresh();
|
|
1021
|
+
callbacks.onMarkForCheck();
|
|
1022
|
+
},
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
const STORAGE_PREFIX = 'jqt_prefs_';
|
|
1028
|
+
/**
|
|
1029
|
+
* Gère la persistance de la configuration d'un tableau dans le localStorage.
|
|
1030
|
+
* Identifie chaque tableau par un `tableId` unique fourni par l'utilisateur.
|
|
1031
|
+
*/
|
|
1032
|
+
class TablePreferencesManager {
|
|
1033
|
+
constructor(tableId) {
|
|
1034
|
+
this.key = STORAGE_PREFIX + tableId;
|
|
1035
|
+
}
|
|
1036
|
+
/** Retourne la configuration sauvegardée, ou `null` si absente / corrompue. */
|
|
1037
|
+
load() {
|
|
1038
|
+
try {
|
|
1039
|
+
const raw = localStorage.getItem(this.key);
|
|
1040
|
+
return raw ? JSON.parse(raw) : null;
|
|
1041
|
+
}
|
|
1042
|
+
catch {
|
|
1043
|
+
return null;
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/** Persiste la configuration dans le localStorage. */
|
|
1047
|
+
save(config) {
|
|
1048
|
+
try {
|
|
1049
|
+
localStorage.setItem(this.key, JSON.stringify(config));
|
|
1050
|
+
}
|
|
1051
|
+
catch {
|
|
1052
|
+
// localStorage peut être indisponible (mode privé strict, quota dépassé…)
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
/** Supprime la configuration sauvegardée. */
|
|
1056
|
+
clear() {
|
|
1057
|
+
localStorage.removeItem(this.key);
|
|
1058
|
+
}
|
|
1059
|
+
/** Indique si une configuration est actuellement sauvegardée. */
|
|
1060
|
+
hasSaved() {
|
|
1061
|
+
return localStorage.getItem(this.key) !== null;
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
/**
|
|
1066
|
+
* GroupByManager
|
|
1067
|
+
*
|
|
1068
|
+
* Encapsule tout l'état et la logique liés au mode Group By :
|
|
1069
|
+
* - état collapsed / expanded des groupes
|
|
1070
|
+
* - pagination interne par groupe
|
|
1071
|
+
* - tri des groupes (asc / desc)
|
|
1072
|
+
* - scroll vers un groupe
|
|
1073
|
+
*
|
|
1074
|
+
* Le composant TableComponent l'instancie et lui passe deux callbacks :
|
|
1075
|
+
* - `onNeedProjection` : demande une reprojection légère de la page courante
|
|
1076
|
+
* - `onNeedRender` : demande un rebuild complet (tri changé etc.)
|
|
1077
|
+
*/
|
|
1078
|
+
class GroupByManager {
|
|
1079
|
+
constructor(onNeedProjection, onNeedRender) {
|
|
1080
|
+
this.onNeedProjection = onNeedProjection;
|
|
1081
|
+
this.onNeedRender = onNeedRender;
|
|
1082
|
+
/** Groupes actuellement ouverts (mode default-collapsed) ou fermés (mode default-expanded). */
|
|
1083
|
+
this._collapsedGroups = new Set();
|
|
1084
|
+
/** Page courante par clé de groupe. */
|
|
1085
|
+
this._groupPages = new Map();
|
|
1086
|
+
/** Taille de page pour la pagination interne des groupes. */
|
|
1087
|
+
this.groupPageSize = 5;
|
|
1088
|
+
/** Ordre de tri des groupes ('asc' | 'desc'). */
|
|
1089
|
+
this.groupSortOrder = 'asc';
|
|
1090
|
+
/**
|
|
1091
|
+
* Quand `true`, tous les groupes sont collapsés par défaut.
|
|
1092
|
+
* `_collapsedGroups` contient alors le seul groupe ouvert.
|
|
1093
|
+
*/
|
|
1094
|
+
this._defaultGroupCollapsed = false;
|
|
1095
|
+
}
|
|
1096
|
+
// ── API publique
|
|
1097
|
+
/** Initialise le mode par défaut selon qu'un groupBy vient d'être activé. */
|
|
1098
|
+
setDefaultCollapsed(collapsed) {
|
|
1099
|
+
this._defaultGroupCollapsed = collapsed;
|
|
1100
|
+
}
|
|
1101
|
+
/** Réinitialise l'état (à appeler quand le groupBy change de colonne). */
|
|
1102
|
+
reset() {
|
|
1103
|
+
this._collapsedGroups.clear();
|
|
1104
|
+
this._groupPages.clear();
|
|
1105
|
+
}
|
|
1106
|
+
isGroupCollapsed(groupKey) {
|
|
1107
|
+
return this._defaultGroupCollapsed
|
|
1108
|
+
? !this._collapsedGroups.has(groupKey)
|
|
1109
|
+
: this._collapsedGroups.has(groupKey);
|
|
1110
|
+
}
|
|
1111
|
+
toggleGroupCollapse(groupKey, scrollTo) {
|
|
1112
|
+
if (this._collapsedGroups.has(groupKey)) {
|
|
1113
|
+
this._collapsedGroups.delete(groupKey);
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
this._collapsedGroups.clear();
|
|
1117
|
+
this._collapsedGroups.add(groupKey);
|
|
1118
|
+
scrollTo(groupKey);
|
|
1119
|
+
}
|
|
1120
|
+
this.onNeedProjection();
|
|
1121
|
+
}
|
|
1122
|
+
/** Retourne le groupe actuellement ouvert (seulement en mode default-collapsed avec exactement 1 ouvert). */
|
|
1123
|
+
getSingleOpenGroup() {
|
|
1124
|
+
return this._defaultGroupCollapsed && this._collapsedGroups.size === 1
|
|
1125
|
+
? [...this._collapsedGroups][0]
|
|
1126
|
+
: null;
|
|
1127
|
+
}
|
|
1128
|
+
// ── Pagination par groupe
|
|
1129
|
+
getPage(groupKey) {
|
|
1130
|
+
return this._groupPages.get(groupKey) ?? 0;
|
|
1131
|
+
}
|
|
1132
|
+
groupPageBack(groupKey) {
|
|
1133
|
+
const current = this._groupPages.get(groupKey) ?? 0;
|
|
1134
|
+
if (current > 0) {
|
|
1135
|
+
this._groupPages.set(groupKey, current - 1);
|
|
1136
|
+
this.onNeedProjection();
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
groupPageForward(groupKey, totalCount) {
|
|
1140
|
+
const maxPage = Math.ceil(totalCount / this.groupPageSize) - 1;
|
|
1141
|
+
const current = this._groupPages.get(groupKey) ?? 0;
|
|
1142
|
+
if (current < maxPage) {
|
|
1143
|
+
this._groupPages.set(groupKey, current + 1);
|
|
1144
|
+
this.onNeedProjection();
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
onGroupPageSizeChange(size) {
|
|
1148
|
+
this.groupPageSize = size;
|
|
1149
|
+
this._groupPages.clear();
|
|
1150
|
+
this.onNeedRender();
|
|
1151
|
+
}
|
|
1152
|
+
onGroupSortToggle(dir, scrollTo) {
|
|
1153
|
+
if (this.groupSortOrder === dir)
|
|
1154
|
+
return;
|
|
1155
|
+
this.groupSortOrder = dir;
|
|
1156
|
+
this._groupPages.clear();
|
|
1157
|
+
this.onNeedRender();
|
|
1158
|
+
const openGroup = this.getSingleOpenGroup();
|
|
1159
|
+
if (openGroup)
|
|
1160
|
+
scrollTo(openGroup);
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
/**
|
|
1165
|
+
* ExportManager
|
|
1166
|
+
*
|
|
1167
|
+
* Encapsule l'export CSV : construction du fichier BOM/UTF-8, échappement et déclenchement du téléchargement.
|
|
1168
|
+
* Aucune dépendance Angular — indépendant du cycle de vie du composant.
|
|
1169
|
+
*/
|
|
1170
|
+
class ExportManager {
|
|
1171
|
+
constructor(getRows, getColumns, getConfig, resolveCellValue) {
|
|
1172
|
+
this.getRows = getRows;
|
|
1173
|
+
this.getColumns = getColumns;
|
|
1174
|
+
this.getConfig = getConfig;
|
|
1175
|
+
this.resolveCellValue = resolveCellValue;
|
|
1176
|
+
}
|
|
1177
|
+
export() {
|
|
1178
|
+
const cfg = this.getConfig();
|
|
1179
|
+
if (!cfg?.enabled)
|
|
1180
|
+
return;
|
|
1181
|
+
const rows = this.getRows();
|
|
1182
|
+
const columns = this.getColumns();
|
|
1183
|
+
const filename = (cfg.filename || 'export') + '.csv';
|
|
1184
|
+
const header = columns.map(c => this._csvEscape(c.header || c.key)).join(',');
|
|
1185
|
+
const lines = rows.map((row, idx) => {
|
|
1186
|
+
if (cfg.transform) {
|
|
1187
|
+
const t = cfg.transform(row);
|
|
1188
|
+
return columns.map(c => this._csvEscape(String(t[c.key] ?? ''))).join(',');
|
|
1189
|
+
}
|
|
1190
|
+
return columns.map(c => this._csvEscape(String(this.resolveCellValue(c, row, idx) ?? ''))).join(',');
|
|
1191
|
+
});
|
|
1192
|
+
// BOM UTF-8 pour une ouverture correcte sous Excel
|
|
1193
|
+
const blob = new Blob(['\uFEFF' + [header, ...lines].join('\n')], { type: 'text/csv;charset=utf-8' });
|
|
1194
|
+
const url = URL.createObjectURL(blob);
|
|
1195
|
+
const a = document.createElement('a');
|
|
1196
|
+
a.href = url;
|
|
1197
|
+
a.download = filename.endsWith('.csv') ? filename : filename + '.csv';
|
|
1198
|
+
a.click();
|
|
1199
|
+
URL.revokeObjectURL(url);
|
|
1200
|
+
}
|
|
1201
|
+
_csvEscape(value) {
|
|
1202
|
+
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
|
1203
|
+
return '"' + value.replace(/"/g, '""') + '"';
|
|
1204
|
+
}
|
|
1205
|
+
return value;
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
|
|
542
1209
|
class TableComponent {
|
|
543
1210
|
constructor() {
|
|
544
1211
|
this.isLoading = false;
|
|
@@ -547,8 +1214,35 @@ class TableComponent {
|
|
|
547
1214
|
this.columnRemoved = new EventEmitter();
|
|
548
1215
|
this.categorySelected = new EventEmitter();
|
|
549
1216
|
this.rowSelected = new EventEmitter();
|
|
1217
|
+
/** Émis à chaque changement de tri (y compris le tri initial). */
|
|
1218
|
+
this.sortChange = new EventEmitter();
|
|
1219
|
+
/** Émis à chaque changement de page ou de taille de page. */
|
|
1220
|
+
this.pageChange = new EventEmitter();
|
|
1221
|
+
/** Émis à chaque changement de la recherche texte. */
|
|
1222
|
+
this.searchChange = new EventEmitter();
|
|
1223
|
+
/** Émis quand le Group by change (clé de colonne, ou `null` si désactivé). */
|
|
1224
|
+
this.groupByChange = new EventEmitter();
|
|
1225
|
+
/** Émis quand la liste des colonnes visibles change (clés dans l’ordre d’affichage). */
|
|
1226
|
+
this.columnsChange = new EventEmitter();
|
|
1227
|
+
/** Map clé→TemplateRef pré-calculée depuis _cellDefs. Mise à jour dans ngAfterContentInit et sur _cellDefs.changes. */
|
|
1228
|
+
this._cellDefMap = new Map();
|
|
550
1229
|
this.renderedColumns = [];
|
|
551
1230
|
this.tableDataSource = new MatTableDataSource([]);
|
|
1231
|
+
/** Constante exposée au template pour le matColumnDef de la ligne de groupe. */
|
|
1232
|
+
this._GROUP_ROW_COLUMN = GROUP_ROW_COLUMN;
|
|
1233
|
+
/** Clés d'accès aux propriétés des lignes projetées — exposées pour le template. */
|
|
1234
|
+
this._ROW_RAW_KEY = ROW_RAW_KEY;
|
|
1235
|
+
this._ROW_INDEX_KEY = ROW_INDEX_KEY;
|
|
1236
|
+
this._ROW_GROUP_KEY = ROW_GROUP_KEY;
|
|
1237
|
+
this._ROW_GROUP_COUNT = ROW_GROUP_COUNT;
|
|
1238
|
+
this._ROW_GROUP_PAGE = ROW_GROUP_PAGE;
|
|
1239
|
+
this._ROW_GROUP_PAGE_COUNT = ROW_GROUP_PAGE_COUNT;
|
|
1240
|
+
/** Facade View : état et actions centralisés (Champs / Group by / Slice by) */
|
|
1241
|
+
this._view = new ViewFacade();
|
|
1242
|
+
/**
|
|
1243
|
+
* Snapshot stable synchronisé depuis _view.fields.activeColumns dans refreshViewModel().
|
|
1244
|
+
* Propriété (non getter) pour qu'Angular puisse la suivre sans NG0100.
|
|
1245
|
+
*/
|
|
552
1246
|
this.activeColumns = [];
|
|
553
1247
|
this.activeSliceFilter = () => true;
|
|
554
1248
|
// Propriétés stables pour les bindings du template (contre erreur NG0100)
|
|
@@ -556,55 +1250,69 @@ class TableComponent {
|
|
|
556
1250
|
this._sliceColumns = [];
|
|
557
1251
|
this._sliceData = [];
|
|
558
1252
|
this._sliceShowToggle = true;
|
|
559
|
-
|
|
560
|
-
this.
|
|
561
|
-
|
|
562
|
-
this.
|
|
563
|
-
this.
|
|
1253
|
+
/** Snapshot stable de la visibilité du panneau slice — jamais calculé depuis @ViewChild */
|
|
1254
|
+
this._showSlicePanel = false;
|
|
1255
|
+
/** Snapshot stable de l'état collapsed du panneau slice, mis à jour via (collapsedChange) */
|
|
1256
|
+
this._slicePanelCollapsed = false;
|
|
1257
|
+
this._sliceByMenuItems = [];
|
|
564
1258
|
this.searchQuery = '';
|
|
565
|
-
this.activeGroupByKey = null;
|
|
566
1259
|
this._matSortActive = '';
|
|
567
1260
|
this._matSortDirection = '';
|
|
568
|
-
this._collapsedGroups = new Set();
|
|
569
1261
|
this._activeSort = { active: '', direction: '' };
|
|
570
1262
|
this._sortSubscribed = null;
|
|
571
1263
|
this._destroy$ = new Subject();
|
|
572
|
-
this._defaultGroupCollapsed = false;
|
|
573
|
-
this._groupPages = new Map();
|
|
574
1264
|
this._totalFilteredCount = 0;
|
|
575
|
-
|
|
576
|
-
this.
|
|
577
|
-
|
|
1265
|
+
/** Gestionnaire du mode Group By (état collapsed, pagination, tri des groupes). */
|
|
1266
|
+
this._groupBy = new GroupByManager(() => { this._projectCurrentPage(); this._cdr.markForCheck(); }, () => this.refreshViewModel());
|
|
1267
|
+
/** Gestionnaire du cycle de vie des colonnes lazy (fetch, annulation, statuts, données). */
|
|
1268
|
+
this._lazy = new LazyColumnManager();
|
|
578
1269
|
this._cdr = inject(ChangeDetectorRef);
|
|
579
1270
|
this._el = inject((ElementRef));
|
|
580
|
-
this.
|
|
581
|
-
|
|
1271
|
+
this._ngZone = inject(NgZone);
|
|
1272
|
+
this._i18nRaw = inject(JQT_I18N, { optional: true });
|
|
1273
|
+
/** Labels de l’interface. Fusionnés depuis JQT_I18N_DEFAULTS et le token JQT_I18N injecté. */
|
|
1274
|
+
this.i18n = { ...JQT_I18N_DEFAULTS, ...(this._i18nRaw ?? {}) };
|
|
1275
|
+
this.isGroupHeader = (_, row) => row[GROUP_HEADER_MARKER] === true;
|
|
1276
|
+
/** Gestionnaire export CSV. */
|
|
1277
|
+
this._export = new ExportManager(() => this._allFilteredRows, () => this.activeColumns, () => this.resolvedConfig.export, (col, row, idx) => this.resolveCellValue(col, row, idx));
|
|
1278
|
+
// ── Lifecycle
|
|
582
1279
|
this.pageSize = 5;
|
|
583
1280
|
this.pageSizeOptions = [5, 10, 20];
|
|
584
1281
|
this.pageSizeOptionsGroupBy = [5, 10, 20];
|
|
585
|
-
this.groupPageSize = 5;
|
|
586
|
-
this.groupSortOrder = 'asc';
|
|
587
1282
|
this.draggedColumnKey = null;
|
|
588
1283
|
this.dragOverColumnKey = null;
|
|
589
|
-
|
|
1284
|
+
// ── Mode édition (Préférences)
|
|
1285
|
+
/** Vrai quand le mode édition est actif (resize + drag colonnes, interactions désactivées). */
|
|
1286
|
+
this._editMode = false;
|
|
1287
|
+
/** Largeurs de colonnes personnalisées par l'utilisateur (clé → px). */
|
|
1288
|
+
this._columnWidths = {};
|
|
1289
|
+
/** Message de feedback après save/clear. `null` = pas de message. */
|
|
1290
|
+
this._preferencesMessage = null;
|
|
1291
|
+
/** Vrai si une configuration est sauvegardée pour ce tableau. */
|
|
1292
|
+
this._hasSavedConfig = false;
|
|
1293
|
+
this._preferencesManager = null;
|
|
1294
|
+
this._resizeState = null;
|
|
1295
|
+
/** Dynamic slice keys à restaurer après ngAfterViewInit (slicePanelRef pas encore dispo). */
|
|
1296
|
+
this._pendingDynamicSliceKeys = null;
|
|
1297
|
+
/** Filtres de slice à restaurer après ngAfterViewInit (slicePanelRef pas encore dispo). */
|
|
1298
|
+
this._pendingSliceFilters = null;
|
|
590
1299
|
this.resolvedConfig = { columns: [] };
|
|
591
1300
|
this._columnMap = new Map();
|
|
592
1301
|
this._resolvedData = [];
|
|
593
1302
|
this._preservePageIndex = null;
|
|
594
1303
|
this._initialSearchApplied = false;
|
|
595
|
-
// ── Optimisation performance
|
|
1304
|
+
// ── Optimisation performance
|
|
596
1305
|
this._allFilteredRows = [];
|
|
597
1306
|
this._paginatorSubscribed = null;
|
|
598
|
-
|
|
599
|
-
this.
|
|
600
|
-
this._groupCacheKey = null;
|
|
601
|
-
this._groupCacheSortOrder = 'asc';
|
|
602
|
-
this._groupCacheSortActive = '';
|
|
603
|
-
this._groupCacheSortDir = '';
|
|
604
|
-
this._groupCacheOrder = [];
|
|
605
|
-
this._groupCacheMap = new Map();
|
|
1307
|
+
/** Cache groupBy — invalidé dès que rows/key/tri changent. */
|
|
1308
|
+
this._groupCache = null;
|
|
606
1309
|
this._pendingRender = null;
|
|
607
|
-
this.
|
|
1310
|
+
this._staticSliceHiddenKeys = new Set();
|
|
1311
|
+
this._configHiddenKeys = new Set();
|
|
1312
|
+
/** Clés des colonnes visibles du dernier émission columnsChange (pour éviter les émissions dupliquement). */
|
|
1313
|
+
this._prevColumnKeys = [];
|
|
1314
|
+
this.trackRowFn = (_index, row) => row[GROUP_HEADER_MARKER] ? `__group_${row[ROW_GROUP_KEY]}` : row[ROW_INDEX_KEY] ?? _index;
|
|
1315
|
+
this._preferencesMessageTimer = null;
|
|
608
1316
|
}
|
|
609
1317
|
set paginator(value) {
|
|
610
1318
|
this._paginator = value;
|
|
@@ -614,15 +1322,40 @@ class TableComponent {
|
|
|
614
1322
|
}
|
|
615
1323
|
}
|
|
616
1324
|
get paginator() { return this._paginator; }
|
|
1325
|
+
// Délégués vers _view (conservés pour compatibilité template sans refactoring HTML)
|
|
1326
|
+
get _staticSlicesForMenu() { return this._view.staticSlicesForMenu; }
|
|
1327
|
+
get _activeDynamicSliceColumns() { return this._view.sliceBy.activeDynamicColumns; }
|
|
1328
|
+
get _availableColumnsForDynamicSlice() { return this._view.sliceBy.availableForDynamic; }
|
|
1329
|
+
get _allDynamicSliceColumns() { return this._view.sliceBy.allDynamicColumns; }
|
|
1330
|
+
get _activeSliceByLabel() { return this._view.sliceBy.activeLabel; }
|
|
1331
|
+
get activeGroupByKey() { return this._view.groupBy.activeKey; }
|
|
1332
|
+
/** Délégués vers GroupByManager pour la compatibilité des templates. */
|
|
1333
|
+
get groupPageSize() { return this._groupBy.groupPageSize; }
|
|
1334
|
+
set groupPageSize(v) { this._groupBy.groupPageSize = v; }
|
|
1335
|
+
get groupSortOrder() { return this._groupBy.groupSortOrder; }
|
|
1336
|
+
set groupSortOrder(v) { this._groupBy.groupSortOrder = v; }
|
|
1337
|
+
/** Compatibilité template : retourne la map de statuts lazy. */
|
|
1338
|
+
get _lazyColumnStatus() { return this._lazy.status; }
|
|
1339
|
+
/** Compatibilité template : retourne la map de données lazy. */
|
|
1340
|
+
get _lazyColumnData() { return this._lazy.data; }
|
|
1341
|
+
/** Retourne true si la ligne de groupe est une ligne normale (ni lazy, ni too-many). */
|
|
1342
|
+
isNormalGroupRow(row) {
|
|
1343
|
+
return row[ROW_GROUP_KEY] !== GROUP_KEY_LAZY_LOADING && row[ROW_GROUP_KEY] !== GROUP_KEY_TOO_MANY;
|
|
1344
|
+
}
|
|
1345
|
+
isGroupRowLoading(row) { return row[ROW_GROUP_KEY] === GROUP_KEY_LAZY_LOADING; }
|
|
1346
|
+
isGroupRowTooMany(row) { return row[ROW_GROUP_KEY] === GROUP_KEY_TOO_MANY; }
|
|
1347
|
+
/** Gère le clic sur une ligne d'en-tête de groupe (no-op pour les états spéciaux). */
|
|
1348
|
+
onGroupHeaderClick(row) {
|
|
1349
|
+
if (!this.isNormalGroupRow(row))
|
|
1350
|
+
return;
|
|
1351
|
+
this.toggleGroupCollapse(row[ROW_GROUP_KEY]);
|
|
1352
|
+
}
|
|
617
1353
|
// Seuil au-delà duquel le rendu groupé est bloqué pour protéger le navigateur
|
|
618
1354
|
static { this.MAX_GROUP_COUNT = 500; }
|
|
619
1355
|
ngOnChanges(changes) {
|
|
620
1356
|
const dataChanged = !!(changes['config'] || changes['data'] || changes['dataSource']);
|
|
621
1357
|
if (dataChanged) {
|
|
622
|
-
this.
|
|
623
|
-
this._lazyFetchCancels.clear();
|
|
624
|
-
this._lazyColumnStatus = new Map();
|
|
625
|
-
this._lazyColumnData = new Map();
|
|
1358
|
+
this._lazy.cancelAll();
|
|
626
1359
|
}
|
|
627
1360
|
if (changes['config'] && !this._initialSearchApplied) {
|
|
628
1361
|
const initial = this.config?.search?.initialQuery;
|
|
@@ -636,28 +1369,81 @@ class TableComponent {
|
|
|
636
1369
|
this.activeColumns.filter(c => c.lazy).forEach(c => this.triggerLazyFetch(c));
|
|
637
1370
|
}
|
|
638
1371
|
}
|
|
1372
|
+
ngAfterContentInit() {
|
|
1373
|
+
this._rebuildCellDefMap();
|
|
1374
|
+
this._cellDefs.changes.pipe(takeUntil(this._destroy$)).subscribe(() => {
|
|
1375
|
+
this._rebuildCellDefMap();
|
|
1376
|
+
this._cdr.markForCheck();
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
639
1379
|
ngAfterViewInit() {
|
|
640
1380
|
this.attachPaginator();
|
|
641
1381
|
this.attachSort();
|
|
642
1382
|
this._measureHeaderHeight();
|
|
1383
|
+
this._applyPendingDynamicSliceKeys();
|
|
1384
|
+
}
|
|
1385
|
+
_applyPendingDynamicSliceKeys() {
|
|
1386
|
+
const hasPendingDynamic = !!this._pendingDynamicSliceKeys;
|
|
1387
|
+
const hasPendingFilters = !!this._pendingSliceFilters;
|
|
1388
|
+
if (!hasPendingDynamic && !hasPendingFilters)
|
|
1389
|
+
return;
|
|
1390
|
+
const allCols = this.resolvedConfig.columns ?? [];
|
|
1391
|
+
const keys = hasPendingDynamic ? [...this._pendingDynamicSliceKeys] : [];
|
|
1392
|
+
const filters = this._pendingSliceFilters ? { ...this._pendingSliceFilters } : null;
|
|
1393
|
+
this._pendingDynamicSliceKeys = null;
|
|
1394
|
+
this._pendingSliceFilters = null;
|
|
1395
|
+
// Tout est différé dans le même setTimeout pour :
|
|
1396
|
+
// 1. Éviter NG0100 (modifications après vérification dans ngAfterViewInit)
|
|
1397
|
+
// 2. Garantir que les dynamic slices sont ajoutées AVANT la restauration des filtres
|
|
1398
|
+
// (les indices dans sliceFilters dépendent de l'ordre final de _cachedSlices)
|
|
1399
|
+
setTimeout(() => {
|
|
1400
|
+
keys.forEach(key => {
|
|
1401
|
+
const col = allCols.find(c => c.key === key);
|
|
1402
|
+
if (col)
|
|
1403
|
+
this.addDynamicSlice(col);
|
|
1404
|
+
});
|
|
1405
|
+
if (filters) {
|
|
1406
|
+
if (this.slicePanelRef) {
|
|
1407
|
+
this.slicePanelRef.restoreFilters(filters);
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
// slicePanelRef toujours null (données pas encore arrivées) :
|
|
1411
|
+
// remettre en attente pour que _recomputeShowSlicePanel les applique
|
|
1412
|
+
this._pendingSliceFilters = filters;
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
this._cdr.markForCheck();
|
|
1416
|
+
});
|
|
643
1417
|
}
|
|
644
1418
|
ngOnDestroy() {
|
|
645
1419
|
if (this._pendingRender !== null) {
|
|
646
1420
|
clearTimeout(this._pendingRender);
|
|
647
1421
|
}
|
|
1422
|
+
if (this._preferencesMessageTimer !== null) {
|
|
1423
|
+
clearTimeout(this._preferencesMessageTimer);
|
|
1424
|
+
}
|
|
1425
|
+
// Nettoie un éventuel resize en cours (listeners window non retirés)
|
|
1426
|
+
this._resizeState = null;
|
|
648
1427
|
this._destroy$.next();
|
|
649
1428
|
this._destroy$.complete();
|
|
1429
|
+
this._view.destroy();
|
|
650
1430
|
}
|
|
651
1431
|
_measureHeaderHeight() {
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1432
|
+
const measure = () => {
|
|
1433
|
+
const headerRow = this._el.nativeElement.querySelector('tr.mat-mdc-header-row');
|
|
1434
|
+
if (headerRow) {
|
|
1435
|
+
const h = headerRow.getBoundingClientRect().height;
|
|
1436
|
+
if (h > 0) {
|
|
1437
|
+
this._el.nativeElement.style.setProperty('--actual-header-height', h + 'px');
|
|
1438
|
+
return;
|
|
1439
|
+
}
|
|
657
1440
|
}
|
|
658
|
-
|
|
1441
|
+
// Header pas encore rendu (ex: lazy Angular Material), on réessaie après le prochain frame
|
|
1442
|
+
this._ngZone.runOutsideAngular(() => requestAnimationFrame(() => measure()));
|
|
1443
|
+
};
|
|
1444
|
+
this._ngZone.runOutsideAngular(() => requestAnimationFrame(() => measure()));
|
|
659
1445
|
}
|
|
660
|
-
// ── Getters d'affichage
|
|
1446
|
+
// ── Getters d'affichage
|
|
661
1447
|
get title() {
|
|
662
1448
|
return this.resolvedConfig.title ?? '';
|
|
663
1449
|
}
|
|
@@ -668,60 +1454,48 @@ class TableComponent {
|
|
|
668
1454
|
return this.activeColumns;
|
|
669
1455
|
}
|
|
670
1456
|
get showSearchBar() { return this.resolvedConfig.search?.enabled === true; }
|
|
671
|
-
get showViewButton() { return this.
|
|
672
|
-
get
|
|
673
|
-
get
|
|
674
|
-
|
|
675
|
-
get filteredRowCount() {
|
|
676
|
-
return this.activeGroupByKey ? this._totalFilteredCount : this._allFilteredRows.length;
|
|
677
|
-
}
|
|
678
|
-
get activeGroupByLabel() {
|
|
679
|
-
if (!this.activeGroupByKey)
|
|
680
|
-
return 'Aucun';
|
|
681
|
-
const col = this.groupByColumns.find((c) => c.key === this.activeGroupByKey);
|
|
682
|
-
return col?.header || this.activeGroupByKey;
|
|
1457
|
+
get showViewButton() { return this._view.enabled || this.showExportButton || this.showPreferencesMenu; }
|
|
1458
|
+
get showSliceExpandBtn() { return this._showSlicePanel && this._slicePanelCollapsed && this._sliceShowToggle; }
|
|
1459
|
+
get showExportButton() {
|
|
1460
|
+
return this.resolvedConfig.export?.enabled === true;
|
|
683
1461
|
}
|
|
684
|
-
get
|
|
685
|
-
return
|
|
1462
|
+
get showPreferencesMenu() {
|
|
1463
|
+
return this.resolvedConfig.preferences?.enabled === true;
|
|
686
1464
|
}
|
|
687
|
-
get
|
|
688
|
-
|
|
1465
|
+
get hasSavedPreferences() { return this._hasSavedConfig; }
|
|
1466
|
+
get showToolbar() { return this.showSearchBar || this.showViewButton || this.showSliceExpandBtn; }
|
|
1467
|
+
expandSlicePanel() {
|
|
1468
|
+
this.slicePanelRef?.togglePanel();
|
|
689
1469
|
}
|
|
690
|
-
get
|
|
691
|
-
|
|
1470
|
+
get showFields() { return this._view.showFields; }
|
|
1471
|
+
get showGroupBySection() { return this._view.showGroupBySection; }
|
|
1472
|
+
get filteredRowCount() {
|
|
1473
|
+
return this.activeGroupByKey ? this._totalFilteredCount : this._allFilteredRows.length;
|
|
692
1474
|
}
|
|
1475
|
+
get activeGroupByLabel() { return this._view.activeGroupByLabel; }
|
|
1476
|
+
get totalColumnCount() { return this._view.totalColumnCount; }
|
|
1477
|
+
get activeSliceByLabel() { return this._view.sliceBy.activeLabel; }
|
|
1478
|
+
get activeDynamicSliceColumns() { return this._view.sliceBy.activeDynamicColumns; }
|
|
693
1479
|
removeDynamicSliceByKey(key) {
|
|
694
1480
|
this.slicePanelRef?.removeDynamicSliceByKey(key);
|
|
695
1481
|
}
|
|
696
|
-
get groupByColumns() {
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
return all.filter((c) => {
|
|
700
|
-
if (c.groupable === false || seen.has(c.key))
|
|
701
|
-
return false;
|
|
702
|
-
if (!c.header && c.groupable !== true)
|
|
703
|
-
return false;
|
|
704
|
-
seen.add(c.key);
|
|
705
|
-
return true;
|
|
706
|
-
});
|
|
707
|
-
}
|
|
708
|
-
colLabel(col) {
|
|
709
|
-
return col.header || humanizeKey(col.key);
|
|
710
|
-
}
|
|
711
|
-
// ── Actions utilisateur ────────────────────────────────────────────────────
|
|
1482
|
+
get groupByColumns() { return this._view.groupByColumns; }
|
|
1483
|
+
colLabel(col) { return this._view.colLabel(col); }
|
|
1484
|
+
// ── Actions utilisateur
|
|
712
1485
|
onSearchChange() {
|
|
713
1486
|
this._preservePageIndex = 0;
|
|
714
1487
|
this.refreshViewModel();
|
|
715
1488
|
this.paginator?.firstPage();
|
|
1489
|
+
this.searchChange.emit(this.searchQuery);
|
|
716
1490
|
}
|
|
717
1491
|
onGroupByChange(key) {
|
|
718
|
-
this.
|
|
719
|
-
this.
|
|
720
|
-
this.
|
|
721
|
-
this.
|
|
722
|
-
this.groupPageSize = this.resolveDefaultGroupPageSize();
|
|
1492
|
+
this._view.setGroupBy(key);
|
|
1493
|
+
this._groupBy.setDefaultCollapsed(key !== null);
|
|
1494
|
+
this._groupBy.reset();
|
|
1495
|
+
this._groupBy.groupPageSize = this.resolveDefaultGroupPageSize();
|
|
723
1496
|
this.refreshViewModel();
|
|
724
1497
|
this.paginator?.firstPage();
|
|
1498
|
+
this.groupByChange.emit(key);
|
|
725
1499
|
if (key) {
|
|
726
1500
|
this.viewMenuTrigger?.closeMenu();
|
|
727
1501
|
const colDef = (this.resolvedConfig.columns || []).find(c => c.key === key);
|
|
@@ -730,113 +1504,43 @@ class TableComponent {
|
|
|
730
1504
|
}
|
|
731
1505
|
}
|
|
732
1506
|
else {
|
|
733
|
-
setTimeout(() => {
|
|
1507
|
+
this._ngZone.runOutsideAngular(() => setTimeout(() => {
|
|
734
1508
|
this.tableBodyScrollRef?.nativeElement.scrollTo({ top: 0, behavior: 'smooth' });
|
|
735
|
-
}, 0);
|
|
1509
|
+
}, 0));
|
|
736
1510
|
}
|
|
737
1511
|
}
|
|
738
1512
|
onGroupPageSizeChange(size) {
|
|
739
|
-
this.
|
|
740
|
-
this._groupPages.clear();
|
|
741
|
-
this.refreshViewModel();
|
|
1513
|
+
this._groupBy.onGroupPageSizeChange(Number(size));
|
|
742
1514
|
}
|
|
743
1515
|
onGroupSortToggle(dir) {
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
this._groupPages.clear();
|
|
748
|
-
this.refreshViewModel();
|
|
749
|
-
const openGroup = this._defaultGroupCollapsed
|
|
750
|
-
? (this._collapsedGroups.size === 1 ? [...this._collapsedGroups][0] : null)
|
|
751
|
-
: null;
|
|
752
|
-
if (openGroup) {
|
|
753
|
-
setTimeout(() => this.scrollToGroupHeader(openGroup), 0);
|
|
754
|
-
}
|
|
1516
|
+
this._groupBy.onGroupSortToggle(dir, (key) => {
|
|
1517
|
+
this._ngZone.runOutsideAngular(() => setTimeout(() => this.scrollToGroupHeader(key), 0));
|
|
1518
|
+
});
|
|
755
1519
|
}
|
|
756
|
-
// ── Lazy loading
|
|
1520
|
+
// ── Lazy loading
|
|
757
1521
|
getLazyColumnStatus(key) {
|
|
758
|
-
return this.
|
|
1522
|
+
return this._lazy.getStatus(key);
|
|
759
1523
|
}
|
|
760
1524
|
getLazyRenderType(rowValue) {
|
|
761
|
-
|
|
762
|
-
return 'loading';
|
|
763
|
-
if (rowValue === '__lazy_error__')
|
|
764
|
-
return 'error';
|
|
765
|
-
return 'value';
|
|
1525
|
+
return this._lazy.getRenderType(rowValue);
|
|
766
1526
|
}
|
|
767
1527
|
retryLazyColumn(column) {
|
|
768
|
-
|
|
769
|
-
const prev = this._lazyFetchCancels.get(k);
|
|
770
|
-
if (prev) {
|
|
771
|
-
prev.next();
|
|
772
|
-
prev.complete();
|
|
773
|
-
}
|
|
774
|
-
this._lazyFetchCancels.delete(k);
|
|
775
|
-
const newStatus = new Map(this._lazyColumnStatus);
|
|
776
|
-
newStatus.delete(k);
|
|
777
|
-
this._lazyColumnStatus = newStatus;
|
|
778
|
-
const newData = new Map(this._lazyColumnData);
|
|
779
|
-
newData.delete(k);
|
|
780
|
-
this._lazyColumnData = newData;
|
|
781
|
-
this.triggerLazyFetch(column);
|
|
1528
|
+
this._lazy.retry(column, this._resolvedData, this._lazyCallbacks());
|
|
782
1529
|
}
|
|
783
1530
|
triggerLazyFetch(column) {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
prev.next();
|
|
792
|
-
prev.complete();
|
|
793
|
-
}
|
|
794
|
-
const cancel$ = new Subject();
|
|
795
|
-
this._lazyFetchCancels.set(k, cancel$);
|
|
796
|
-
const newStatus1 = new Map(this._lazyColumnStatus);
|
|
797
|
-
newStatus1.set(k, 'loading');
|
|
798
|
-
this._lazyColumnStatus = newStatus1;
|
|
799
|
-
this.refreshViewModel();
|
|
800
|
-
column.lazy.fetchFn().pipe(take(1), takeUntil(cancel$)).subscribe({
|
|
801
|
-
next: (values) => {
|
|
802
|
-
const rowMap = new Map();
|
|
803
|
-
const data = this._resolvedData;
|
|
804
|
-
values.forEach((value, i) => {
|
|
805
|
-
if (i < data.length)
|
|
806
|
-
rowMap.set(data[i], value);
|
|
807
|
-
});
|
|
808
|
-
const newData = new Map(this._lazyColumnData);
|
|
809
|
-
newData.set(k, rowMap);
|
|
810
|
-
this._lazyColumnData = newData;
|
|
811
|
-
const newStatus2 = new Map(this._lazyColumnStatus);
|
|
812
|
-
newStatus2.set(k, 'loaded');
|
|
813
|
-
this._lazyColumnStatus = newStatus2;
|
|
814
|
-
this.refreshViewModel();
|
|
815
|
-
this._cdr.markForCheck();
|
|
816
|
-
},
|
|
817
|
-
error: () => {
|
|
818
|
-
const newStatusErr = new Map(this._lazyColumnStatus);
|
|
819
|
-
newStatusErr.set(k, 'error');
|
|
820
|
-
this._lazyColumnStatus = newStatusErr;
|
|
821
|
-
this.refreshViewModel();
|
|
822
|
-
this._cdr.markForCheck();
|
|
823
|
-
},
|
|
824
|
-
});
|
|
1531
|
+
this._lazy.fetch(column, this._resolvedData, this._lazyCallbacks());
|
|
1532
|
+
}
|
|
1533
|
+
_lazyCallbacks() {
|
|
1534
|
+
return {
|
|
1535
|
+
onRefresh: () => this.refreshViewModel(),
|
|
1536
|
+
onMarkForCheck: () => this._cdr.markForCheck(),
|
|
1537
|
+
};
|
|
825
1538
|
}
|
|
826
|
-
// ── Groupement
|
|
1539
|
+
// ── Groupement
|
|
827
1540
|
toggleGroupCollapse(groupKey) {
|
|
828
|
-
|
|
829
|
-
this.
|
|
830
|
-
}
|
|
831
|
-
else {
|
|
832
|
-
this._collapsedGroups.clear();
|
|
833
|
-
this._collapsedGroups.add(groupKey);
|
|
834
|
-
setTimeout(() => this.scrollToGroupHeader(groupKey), 0);
|
|
835
|
-
}
|
|
836
|
-
// Court-circuit de refreshViewModel : la data et les colonnes n'ont pas changé,
|
|
837
|
-
// seul l'état collapsed change — reprojection directe sans rebuild complet
|
|
838
|
-
this._projectCurrentPage();
|
|
839
|
-
this._cdr.markForCheck();
|
|
1541
|
+
this._groupBy.toggleGroupCollapse(groupKey, (key) => {
|
|
1542
|
+
this._ngZone.runOutsideAngular(() => setTimeout(() => this.scrollToGroupHeader(key), 0));
|
|
1543
|
+
});
|
|
840
1544
|
}
|
|
841
1545
|
scrollToGroupHeader(groupKey) {
|
|
842
1546
|
const container = this.tableBodyScrollRef?.nativeElement;
|
|
@@ -853,63 +1557,86 @@ class TableComponent {
|
|
|
853
1557
|
container.scrollTo({ top: targetScrollTop, behavior: 'smooth' });
|
|
854
1558
|
}
|
|
855
1559
|
isGroupCollapsed(groupKey) {
|
|
856
|
-
return this.
|
|
1560
|
+
return this._groupBy.isGroupCollapsed(groupKey);
|
|
857
1561
|
}
|
|
858
1562
|
groupPageBack(groupKey) {
|
|
859
|
-
|
|
860
|
-
if (current > 0) {
|
|
861
|
-
this._groupPages.set(groupKey, current - 1);
|
|
862
|
-
this._projectCurrentPage();
|
|
863
|
-
this._cdr.markForCheck();
|
|
864
|
-
}
|
|
1563
|
+
this._groupBy.groupPageBack(groupKey);
|
|
865
1564
|
}
|
|
866
1565
|
groupPageForward(groupKey, totalCount) {
|
|
867
|
-
|
|
868
|
-
const current = this._groupPages.get(groupKey) ?? 0;
|
|
869
|
-
if (current < maxPage) {
|
|
870
|
-
this._groupPages.set(groupKey, current + 1);
|
|
871
|
-
this._projectCurrentPage();
|
|
872
|
-
this._cdr.markForCheck();
|
|
873
|
-
}
|
|
1566
|
+
this._groupBy.groupPageForward(groupKey, totalCount);
|
|
874
1567
|
}
|
|
875
|
-
// ── Colonnes
|
|
1568
|
+
// ── Colonnes
|
|
876
1569
|
get hasAddableColumns() {
|
|
877
|
-
return this.menuBaseColumns.length > 0 || this.menuOptionalColumns.length > 0;
|
|
1570
|
+
return this._view.menuBaseColumns.length > 0 || this._view.menuOptionalColumns.length > 0;
|
|
878
1571
|
}
|
|
879
1572
|
get menuBaseColumns() {
|
|
880
|
-
return this.
|
|
1573
|
+
return this._view.menuBaseColumns;
|
|
881
1574
|
}
|
|
882
1575
|
get menuOptionalColumns() {
|
|
883
|
-
return this.
|
|
1576
|
+
return this._view.menuOptionalColumns;
|
|
884
1577
|
}
|
|
885
1578
|
get availableColumnsToAdd() {
|
|
886
1579
|
const activeKeys = new Set(this.activeColumns.map((column) => column.key));
|
|
887
1580
|
return (this.resolvedConfig.columns || []).filter((c) => c.optional && !activeKeys.has(c.key));
|
|
888
1581
|
}
|
|
889
1582
|
get allowColumnRemoval() {
|
|
890
|
-
return this.
|
|
1583
|
+
return this._view.allowColumnRemoval;
|
|
891
1584
|
}
|
|
892
|
-
// ── Slices
|
|
1585
|
+
// ── Slices
|
|
893
1586
|
get hasSliceConfig() {
|
|
894
1587
|
return (this.resolvedConfig.slices?.length ?? 0) > 0;
|
|
895
1588
|
}
|
|
896
1589
|
get showSlicePanel() {
|
|
897
|
-
return this.
|
|
1590
|
+
return this._showSlicePanel;
|
|
1591
|
+
}
|
|
1592
|
+
onSlicePanelCollapsedChange(collapsed) {
|
|
1593
|
+
this._slicePanelCollapsed = collapsed;
|
|
1594
|
+
this._cdr.markForCheck();
|
|
1595
|
+
}
|
|
1596
|
+
_recomputeShowSlicePanel() {
|
|
1597
|
+
const hasCfg = this._sliceConfigs.length > 0 || this._view.sliceBy.activeDynamicColumns.length > 0;
|
|
1598
|
+
const wasVisible = this._showSlicePanel;
|
|
1599
|
+
this._showSlicePanel = hasCfg && this._resolvedData.length > 0;
|
|
1600
|
+
// Le SlicePanelComponent vient d'être rendu pour la 1ère fois
|
|
1601
|
+
// (données arrivées) → appliquer les filtres en attente
|
|
1602
|
+
if (!wasVisible && this._showSlicePanel && this._pendingSliceFilters) {
|
|
1603
|
+
const filters = this._pendingSliceFilters;
|
|
1604
|
+
this._pendingSliceFilters = null;
|
|
1605
|
+
setTimeout(() => {
|
|
1606
|
+
this.slicePanelRef?.restoreFilters(filters);
|
|
1607
|
+
this._cdr.markForCheck();
|
|
1608
|
+
});
|
|
1609
|
+
}
|
|
898
1610
|
}
|
|
899
1611
|
get showAddSliceButton() {
|
|
900
|
-
return this.
|
|
1612
|
+
return this._view.showSliceBySection;
|
|
901
1613
|
}
|
|
902
1614
|
get staticSlicesForMenu() {
|
|
903
|
-
return this.
|
|
1615
|
+
return this._view.staticSlicesForMenu;
|
|
904
1616
|
}
|
|
905
1617
|
get availableColumnsForDynamicSlice() {
|
|
906
|
-
return this.
|
|
1618
|
+
return this._view.sliceBy.availableForDynamic;
|
|
907
1619
|
}
|
|
908
1620
|
isStaticSliceVisible(columnKey) {
|
|
909
|
-
return this.
|
|
1621
|
+
return !this._staticSliceHiddenKeys.has(columnKey);
|
|
910
1622
|
}
|
|
911
1623
|
toggleStaticSlice(columnKey) {
|
|
912
|
-
this.
|
|
1624
|
+
const next = new Set(this._staticSliceHiddenKeys);
|
|
1625
|
+
if (next.has(columnKey))
|
|
1626
|
+
next.delete(columnKey);
|
|
1627
|
+
else
|
|
1628
|
+
next.add(columnKey);
|
|
1629
|
+
this._staticSliceHiddenKeys = next;
|
|
1630
|
+
this._view.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
|
|
1631
|
+
this._sliceConfigs = this._computeSliceConfigs();
|
|
1632
|
+
// Si une slice vient d'être cachée, réinitialise le filtre actif pour éviter
|
|
1633
|
+
// un filtre orphelin (la slice n'est plus visible mais son filtre resterait actif)
|
|
1634
|
+
if (next.has(columnKey)) {
|
|
1635
|
+
this.activeSliceFilter = () => true;
|
|
1636
|
+
}
|
|
1637
|
+
this._recomputeShowSlicePanel();
|
|
1638
|
+
this._scheduleRender();
|
|
1639
|
+
this._cdr.markForCheck();
|
|
913
1640
|
}
|
|
914
1641
|
addDynamicSlice(column) {
|
|
915
1642
|
this.slicePanelRef?.addDynamicSlice(column);
|
|
@@ -921,7 +1648,7 @@ class TableComponent {
|
|
|
921
1648
|
return this._allDynamicSliceColumns;
|
|
922
1649
|
}
|
|
923
1650
|
isDynamicSliceActive(key) {
|
|
924
|
-
return this.
|
|
1651
|
+
return this._view.isDynamicSliceActive(key);
|
|
925
1652
|
}
|
|
926
1653
|
toggleDynamicSlice(col) {
|
|
927
1654
|
if (this.isDynamicSliceActive(col.key)) {
|
|
@@ -938,34 +1665,22 @@ class TableComponent {
|
|
|
938
1665
|
this.paginator?.firstPage();
|
|
939
1666
|
}
|
|
940
1667
|
onDynamicSliceKeysChange(keys) {
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
const dynamicKeySet = new Set(keys);
|
|
944
|
-
const existingKeys = new Set([...staticKeys, ...dynamicKeySet]);
|
|
945
|
-
this._activeDynamicSliceColumns = keys
|
|
946
|
-
.map(k => allCols.find(c => c.key === k))
|
|
947
|
-
.filter((c) => !!c);
|
|
948
|
-
const seen = new Set();
|
|
949
|
-
this._availableColumnsForDynamicSlice = allCols.filter(c => {
|
|
950
|
-
if (c.sliceable === false || existingKeys.has(c.key) || seen.has(c.key))
|
|
951
|
-
return false;
|
|
952
|
-
seen.add(c.key);
|
|
953
|
-
return true;
|
|
954
|
-
});
|
|
955
|
-
this._computeSliceLabel();
|
|
1668
|
+
this._view.onDynamicSliceKeysChange(keys);
|
|
1669
|
+
this._recomputeShowSlicePanel();
|
|
956
1670
|
this._cdr.markForCheck();
|
|
957
1671
|
}
|
|
958
1672
|
get emptyStateLabel() {
|
|
959
|
-
return this.resolvedConfig.labels?.empty ||
|
|
1673
|
+
return this.resolvedConfig.labels?.empty || this.i18n.emptyState;
|
|
960
1674
|
}
|
|
961
1675
|
get loadingStateLabel() {
|
|
962
|
-
return this.resolvedConfig.labels?.loading ||
|
|
1676
|
+
return this.resolvedConfig.labels?.loading || this.i18n.loadingState;
|
|
963
1677
|
}
|
|
964
1678
|
get effectiveIsLoading() {
|
|
965
|
-
return this.isLoading
|
|
1679
|
+
return this.isLoading;
|
|
966
1680
|
}
|
|
967
1681
|
get isEmpty() {
|
|
968
|
-
|
|
1682
|
+
// _pendingRender : le rendu interne n'est pas encore terminé → pas de message vide prématuré
|
|
1683
|
+
return !this.isLoading && this._pendingRender === null && this._allFilteredRows.length === 0;
|
|
969
1684
|
}
|
|
970
1685
|
get showPagination() {
|
|
971
1686
|
if (this.activeGroupByKey !== null)
|
|
@@ -973,35 +1688,16 @@ class TableComponent {
|
|
|
973
1688
|
return this.resolvedConfig.pagination?.enabled !== false;
|
|
974
1689
|
}
|
|
975
1690
|
get isColumnDragDropEnabled() {
|
|
976
|
-
return this.
|
|
1691
|
+
return this._view.isColumnDragDropEnabled;
|
|
977
1692
|
}
|
|
978
1693
|
get totalRows() {
|
|
979
1694
|
return this.activeGroupByKey
|
|
980
|
-
? this.tableDataSource.data.filter((r) => !r
|
|
1695
|
+
? this.tableDataSource.data.filter((r) => !r[GROUP_HEADER_MARKER]).length
|
|
981
1696
|
: this._allFilteredRows.length;
|
|
982
1697
|
}
|
|
983
|
-
// ── Gestion des colonnes
|
|
1698
|
+
// ── Gestion des colonnes
|
|
984
1699
|
onAddColumn(column) {
|
|
985
|
-
this.
|
|
986
|
-
const configCols = this.resolvedConfig.columns ?? [];
|
|
987
|
-
const configIndex = configCols.findIndex(c => c.key === column.key);
|
|
988
|
-
if (configIndex === -1) {
|
|
989
|
-
this.activeColumns = [...this.activeColumns, column];
|
|
990
|
-
}
|
|
991
|
-
else {
|
|
992
|
-
const insertBefore = this.activeColumns.findIndex(c => {
|
|
993
|
-
const ci = configCols.findIndex(cc => cc.key === c.key);
|
|
994
|
-
return ci !== -1 && ci > configIndex;
|
|
995
|
-
});
|
|
996
|
-
if (insertBefore === -1) {
|
|
997
|
-
this.activeColumns = [...this.activeColumns, column];
|
|
998
|
-
}
|
|
999
|
-
else {
|
|
1000
|
-
const copy = [...this.activeColumns];
|
|
1001
|
-
copy.splice(insertBefore, 0, column);
|
|
1002
|
-
this.activeColumns = copy;
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1700
|
+
this._view.addColumn(column);
|
|
1005
1701
|
this._preservePageIndex = this.paginator?.pageIndex ?? null;
|
|
1006
1702
|
this.refreshViewModel();
|
|
1007
1703
|
if (column.lazy) {
|
|
@@ -1011,70 +1707,87 @@ class TableComponent {
|
|
|
1011
1707
|
this.columnAdded.emit(column);
|
|
1012
1708
|
}
|
|
1013
1709
|
onColumnVisibilityChange(column, checked) {
|
|
1014
|
-
const isVisible = this.isColumnVisible(column.key);
|
|
1710
|
+
const isVisible = this._view.isColumnVisible(column.key);
|
|
1015
1711
|
if (checked && !isVisible) {
|
|
1016
1712
|
this.onAddColumn(column);
|
|
1017
1713
|
return;
|
|
1018
1714
|
}
|
|
1019
1715
|
if (!checked && isVisible) {
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
}
|
|
1023
|
-
if (this.activeColumns.length <= 1) {
|
|
1716
|
+
const removed = this._view.removeColumn(column.key);
|
|
1717
|
+
if (!removed)
|
|
1024
1718
|
return;
|
|
1025
|
-
}
|
|
1026
|
-
this.userCustomizedColumns = true;
|
|
1027
|
-
this.activeColumns = this.activeColumns.filter((item) => item.key !== column.key);
|
|
1028
1719
|
this._preservePageIndex = this.paginator?.pageIndex ?? null;
|
|
1029
1720
|
this.refreshViewModel();
|
|
1030
1721
|
this.columnRemoved.emit(column);
|
|
1031
1722
|
}
|
|
1032
1723
|
}
|
|
1033
1724
|
isColumnVisible(columnKey) {
|
|
1034
|
-
return this.activeColumns.some(
|
|
1725
|
+
return this.activeColumns.some(c => c.key === columnKey);
|
|
1035
1726
|
}
|
|
1036
1727
|
onRemoveColumn(columnKey, event) {
|
|
1037
1728
|
event?.stopPropagation();
|
|
1038
|
-
|
|
1729
|
+
const column = this.activeColumns.find(item => item.key === columnKey);
|
|
1730
|
+
if (!column)
|
|
1039
1731
|
return;
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (!column || !this.canRemoveColumn(column)) {
|
|
1732
|
+
const removed = this._view.removeColumn(columnKey);
|
|
1733
|
+
if (!removed)
|
|
1043
1734
|
return;
|
|
1044
|
-
}
|
|
1045
|
-
this.userCustomizedColumns = true;
|
|
1046
|
-
this.activeColumns = this.activeColumns.filter((item) => item.key !== columnKey);
|
|
1047
1735
|
this.refreshViewModel();
|
|
1048
1736
|
this.columnRemoved.emit(column);
|
|
1049
1737
|
}
|
|
1050
|
-
// ── Lignes
|
|
1738
|
+
// ── Lignes
|
|
1739
|
+
_rebuildCellDefMap() {
|
|
1740
|
+
this._cellDefMap = new Map((this._cellDefs ?? []).map(d => [d.columnKey, d.template]));
|
|
1741
|
+
}
|
|
1051
1742
|
getCellTemplate(key) {
|
|
1052
|
-
return this.
|
|
1743
|
+
return this._cellDefMap.get(key) ?? null;
|
|
1053
1744
|
}
|
|
1054
1745
|
get hasCustomCellDefs() {
|
|
1055
|
-
return
|
|
1746
|
+
return this._cellDefMap.size > 0;
|
|
1056
1747
|
}
|
|
1057
1748
|
get hasRowClickHandler() {
|
|
1058
|
-
return this.rowSelected.observed || !!this.
|
|
1749
|
+
return this.rowSelected.observed || !!this.resolvedConfig.onRowSelected;
|
|
1750
|
+
}
|
|
1751
|
+
/** Largeur minimale du tableau en px, calculée depuis les largeurs explicites des colonnes actives.
|
|
1752
|
+
* Retourne null si aucune colonne n'a de largeur définie (le tableau remplit alors 100% du conteneur).
|
|
1753
|
+
* Seules les valeurs explicitement en `px` sont sommables ; les pourcentages ou autres unités sont ignorés. */
|
|
1754
|
+
get tableMinWidth() {
|
|
1755
|
+
const hasSomeExplicitWidth = this.activeColumns.some(c => c.width);
|
|
1756
|
+
if (!hasSomeExplicitWidth)
|
|
1757
|
+
return null;
|
|
1758
|
+
let sum = 0;
|
|
1759
|
+
for (const col of this.activeColumns) {
|
|
1760
|
+
if (col.width) {
|
|
1761
|
+
const isPx = col.width.trim().toLowerCase().endsWith('px');
|
|
1762
|
+
const px = Number.parseFloat(col.width);
|
|
1763
|
+
if (isPx && !Number.isNaN(px)) {
|
|
1764
|
+
sum += px;
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
else {
|
|
1768
|
+
sum += 120; // largeur minimale estimée pour une colonne sans largeur explicite
|
|
1769
|
+
}
|
|
1770
|
+
}
|
|
1771
|
+
return sum + 'px';
|
|
1059
1772
|
}
|
|
1060
1773
|
getRowClass(row) {
|
|
1061
1774
|
const fn = this.resolvedConfig.rowClass;
|
|
1062
|
-
if (!fn || row?.
|
|
1775
|
+
if (!fn || row?.[GROUP_HEADER_MARKER])
|
|
1063
1776
|
return {};
|
|
1064
|
-
return fn(row
|
|
1777
|
+
return fn(row[ROW_RAW_KEY] ?? row, row[ROW_INDEX_KEY] ?? 0);
|
|
1065
1778
|
}
|
|
1066
1779
|
onRowClick(row, event) {
|
|
1067
|
-
if (row?.
|
|
1780
|
+
if (row?.[GROUP_HEADER_MARKER])
|
|
1068
1781
|
return;
|
|
1069
1782
|
if (!this.hasRowClickHandler)
|
|
1070
1783
|
return;
|
|
1071
|
-
const resolved = (row?.
|
|
1784
|
+
const resolved = (row?.[ROW_RAW_KEY] ?? row);
|
|
1785
|
+
this.resolvedConfig.onRowSelected?.(resolved, event);
|
|
1072
1786
|
this.rowSelected.emit(resolved);
|
|
1073
|
-
this.config?.onRowSelected?.(resolved, event);
|
|
1074
1787
|
}
|
|
1075
|
-
// ── Drag & drop
|
|
1788
|
+
// ── Drag & drop
|
|
1076
1789
|
onHeaderDragStart(columnKey, event) {
|
|
1077
|
-
if (!this.isColumnDragDropEnabled) {
|
|
1790
|
+
if (!this.isColumnDragDropEnabled && !this._editMode) {
|
|
1078
1791
|
return;
|
|
1079
1792
|
}
|
|
1080
1793
|
this.draggedColumnKey = columnKey;
|
|
@@ -1085,28 +1798,22 @@ class TableComponent {
|
|
|
1085
1798
|
}
|
|
1086
1799
|
}
|
|
1087
1800
|
onHeaderDragOver(columnKey, event) {
|
|
1088
|
-
if (!this.isColumnDragDropEnabled || !this.draggedColumnKey) {
|
|
1801
|
+
if ((!this.isColumnDragDropEnabled && !this._editMode) || !this.draggedColumnKey) {
|
|
1089
1802
|
return;
|
|
1090
1803
|
}
|
|
1091
1804
|
event.preventDefault();
|
|
1092
1805
|
this.dragOverColumnKey = columnKey;
|
|
1093
1806
|
}
|
|
1094
1807
|
onHeaderDrop(targetColumnKey, event) {
|
|
1095
|
-
if (!this.isColumnDragDropEnabled || !this.draggedColumnKey) {
|
|
1808
|
+
if ((!this.isColumnDragDropEnabled && !this._editMode) || !this.draggedColumnKey) {
|
|
1096
1809
|
return;
|
|
1097
1810
|
}
|
|
1098
1811
|
event.preventDefault();
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
if (previousIndex === -1 || currentIndex === -1 || previousIndex === currentIndex) {
|
|
1812
|
+
const moved = this._view.reorderColumns(this.draggedColumnKey, targetColumnKey);
|
|
1813
|
+
if (!moved) {
|
|
1102
1814
|
this.onHeaderDragEnd();
|
|
1103
1815
|
return;
|
|
1104
1816
|
}
|
|
1105
|
-
this.userCustomizedColumns = true;
|
|
1106
|
-
const reordered = [...this.activeColumns];
|
|
1107
|
-
const [moved] = reordered.splice(previousIndex, 1);
|
|
1108
|
-
reordered.splice(currentIndex, 0, moved);
|
|
1109
|
-
this.activeColumns = reordered;
|
|
1110
1817
|
this.refreshViewModel();
|
|
1111
1818
|
this.onHeaderDragEnd();
|
|
1112
1819
|
}
|
|
@@ -1117,8 +1824,177 @@ class TableComponent {
|
|
|
1117
1824
|
isHeaderDropTarget(columnKey) {
|
|
1118
1825
|
return !!this.draggedColumnKey && this.dragOverColumnKey === columnKey && this.draggedColumnKey !== columnKey;
|
|
1119
1826
|
}
|
|
1827
|
+
// ── Préférences : mode édition
|
|
1828
|
+
enterEditMode() {
|
|
1829
|
+
this._editMode = true;
|
|
1830
|
+
this._preferencesMessage = null;
|
|
1831
|
+
this._cdr.markForCheck();
|
|
1832
|
+
}
|
|
1833
|
+
confirmEditMode() {
|
|
1834
|
+
this._editMode = false;
|
|
1835
|
+
this._cdr.markForCheck();
|
|
1836
|
+
}
|
|
1837
|
+
/** Démarre le resize d'une colonne (mousedown sur la poignée droite). */
|
|
1838
|
+
onResizeStart(columnKey, event) {
|
|
1839
|
+
if (!this._editMode)
|
|
1840
|
+
return;
|
|
1841
|
+
event.preventDefault();
|
|
1842
|
+
event.stopPropagation();
|
|
1843
|
+
const th = event.target.closest('th');
|
|
1844
|
+
if (!th)
|
|
1845
|
+
return;
|
|
1846
|
+
this._resizeState = { key: columnKey, startX: event.clientX, startWidth: th.offsetWidth };
|
|
1847
|
+
const onMove = (e) => {
|
|
1848
|
+
if (!this._resizeState) {
|
|
1849
|
+
window.removeEventListener('mousemove', onMove);
|
|
1850
|
+
window.removeEventListener('mouseup', onUp);
|
|
1851
|
+
return;
|
|
1852
|
+
}
|
|
1853
|
+
const delta = e.clientX - this._resizeState.startX;
|
|
1854
|
+
const newWidth = Math.max(60, this._resizeState.startWidth + delta);
|
|
1855
|
+
this._columnWidths = { ...this._columnWidths, [this._resizeState.key]: newWidth };
|
|
1856
|
+
// detectChanges() pour forcer la mise à jour immédiate en mode OnPush
|
|
1857
|
+
// (les listeners window tournent hors zone Angular avec runOutsideAngular)
|
|
1858
|
+
this._cdr.detectChanges();
|
|
1859
|
+
};
|
|
1860
|
+
const onUp = () => {
|
|
1861
|
+
this._resizeState = null;
|
|
1862
|
+
window.removeEventListener('mousemove', onMove);
|
|
1863
|
+
window.removeEventListener('mouseup', onUp);
|
|
1864
|
+
};
|
|
1865
|
+
// Listeners hors zone Angular pour éviter des cycles CD à chaque mousemove
|
|
1866
|
+
this._ngZone.runOutsideAngular(() => {
|
|
1867
|
+
window.addEventListener('mousemove', onMove);
|
|
1868
|
+
window.addEventListener('mouseup', onUp);
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
/** Retourne la largeur CSS d'une colonne (px si personnalisée, sinon la valeur de config ou auto). */
|
|
1872
|
+
getColumnWidth(key) {
|
|
1873
|
+
if (this._columnWidths[key] != null)
|
|
1874
|
+
return this._columnWidths[key] + 'px';
|
|
1875
|
+
const col = this._columnMap.get(key);
|
|
1876
|
+
return col?.width ?? 'auto';
|
|
1877
|
+
}
|
|
1878
|
+
// ── Préférences : save / clear
|
|
1879
|
+
savePreferences() {
|
|
1880
|
+
if (!this._preferencesManager)
|
|
1881
|
+
return;
|
|
1882
|
+
const dynamicSliceKeys = this._view.sliceBy.activeDynamicColumns.map(c => c.key);
|
|
1883
|
+
const config = {
|
|
1884
|
+
search: this.searchQuery || undefined,
|
|
1885
|
+
groupBy: this.activeGroupByKey,
|
|
1886
|
+
columnOrder: this.activeColumns.map(c => c.key),
|
|
1887
|
+
visibleColumns: this.activeColumns.map(c => c.key),
|
|
1888
|
+
columnWidths: Object.keys(this._columnWidths).length ? { ...this._columnWidths } : undefined,
|
|
1889
|
+
slicePanelCollapsed: this._slicePanelCollapsed,
|
|
1890
|
+
hiddenSliceKeys: [...this._staticSliceHiddenKeys],
|
|
1891
|
+
dynamicSliceKeys: dynamicSliceKeys.length ? dynamicSliceKeys : undefined,
|
|
1892
|
+
sliceFilters: this.slicePanelRef ? this.slicePanelRef.getActiveFilters() : undefined,
|
|
1893
|
+
};
|
|
1894
|
+
this._preferencesManager.save(config);
|
|
1895
|
+
this._hasSavedConfig = true;
|
|
1896
|
+
this._showPreferencesMessage(this.i18n.preferencesSavedMessage);
|
|
1897
|
+
}
|
|
1898
|
+
clearPreferences() {
|
|
1899
|
+
if (!this._preferencesManager)
|
|
1900
|
+
return;
|
|
1901
|
+
this._preferencesManager.clear();
|
|
1902
|
+
this._hasSavedConfig = false;
|
|
1903
|
+
// Capturer les dynamic slices actives AVANT le reset (resetToDefaults() les vide)
|
|
1904
|
+
const dynamicSlicesToRemove = this._view.sliceBy.activeDynamicColumns.slice();
|
|
1905
|
+
// Reset complet de l'état vers les valeurs par défaut de la config
|
|
1906
|
+
this.searchQuery = '';
|
|
1907
|
+
this._columnWidths = {};
|
|
1908
|
+
this._slicePanelCollapsed = false;
|
|
1909
|
+
this._groupBy.reset();
|
|
1910
|
+
this._groupBy.setDefaultCollapsed(false);
|
|
1911
|
+
// Slices statiques : revenir aux seules celles cachées par la config (pas par l'utilisateur)
|
|
1912
|
+
this._staticSliceHiddenKeys = new Set(this._configHiddenKeys);
|
|
1913
|
+
this._view.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
|
|
1914
|
+
// Colonnes et groupBy : reset via la facade (retire les optionnelles, remet l'ordre config)
|
|
1915
|
+
this._view.resetToDefaults();
|
|
1916
|
+
// Dynamic slices : les retirer du slice panel avec la liste capturée avant le reset
|
|
1917
|
+
dynamicSlicesToRemove.forEach(col => {
|
|
1918
|
+
this.slicePanelRef?.removeDynamicSliceByKey(col.key);
|
|
1919
|
+
});
|
|
1920
|
+
// Filtre de slice actif
|
|
1921
|
+
this.activeSliceFilter = () => true;
|
|
1922
|
+
if (this.slicePanelRef)
|
|
1923
|
+
this.slicePanelRef.restoreFilters({});
|
|
1924
|
+
// Rafraîchit toute la vue
|
|
1925
|
+
this.activeColumns = this._view.fields.activeColumns;
|
|
1926
|
+
this.renderedColumns = this.activeColumns.map(c => c.key);
|
|
1927
|
+
this._pendingDynamicSliceKeys = null;
|
|
1928
|
+
this._pendingSliceFilters = null;
|
|
1929
|
+
this.refreshViewModel();
|
|
1930
|
+
this._showPreferencesMessage(this.i18n.preferencesClearedMessage);
|
|
1931
|
+
}
|
|
1932
|
+
_showPreferencesMessage(msg) {
|
|
1933
|
+
if (this._preferencesMessageTimer !== null)
|
|
1934
|
+
clearTimeout(this._preferencesMessageTimer);
|
|
1935
|
+
this._preferencesMessage = msg;
|
|
1936
|
+
this._cdr.markForCheck();
|
|
1937
|
+
this._preferencesMessageTimer = setTimeout(() => {
|
|
1938
|
+
this._preferencesMessageTimer = null;
|
|
1939
|
+
this._preferencesMessage = null;
|
|
1940
|
+
this._cdr.markForCheck();
|
|
1941
|
+
}, 4000);
|
|
1942
|
+
}
|
|
1943
|
+
_applyPreferences(saved) {
|
|
1944
|
+
if (!saved)
|
|
1945
|
+
return;
|
|
1946
|
+
// Recherche
|
|
1947
|
+
if (saved.search)
|
|
1948
|
+
this.searchQuery = saved.search;
|
|
1949
|
+
// Largeurs colonnes
|
|
1950
|
+
if (saved.columnWidths)
|
|
1951
|
+
this._columnWidths = { ...saved.columnWidths };
|
|
1952
|
+
// Group by
|
|
1953
|
+
if (saved.groupBy !== undefined) {
|
|
1954
|
+
this._view.setGroupBy(saved.groupBy);
|
|
1955
|
+
if (saved.groupBy) {
|
|
1956
|
+
this._groupBy.setDefaultCollapsed(true);
|
|
1957
|
+
this._groupBy.reset();
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
// État panneau slice
|
|
1961
|
+
if (saved.slicePanelCollapsed !== undefined)
|
|
1962
|
+
this._slicePanelCollapsed = saved.slicePanelCollapsed;
|
|
1963
|
+
// Slices statiques cachées — on utilise la valeur exacte sauvegardée (pas les defaults de config)
|
|
1964
|
+
if (saved.hiddenSliceKeys !== undefined) {
|
|
1965
|
+
this._staticSliceHiddenKeys = new Set(saved.hiddenSliceKeys);
|
|
1966
|
+
this._view.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
|
|
1967
|
+
}
|
|
1968
|
+
// Colonnes visibles + ordre — reconstruit activeColumns en une seule passe.
|
|
1969
|
+
// On part de saved.columnOrder (ordre sauvegardé) si disponible, sinon saved.visibleColumns.
|
|
1970
|
+
// Cela gère à la fois : colonnes optionnelles ajoutées, colonnes retirées, et réordonnancement.
|
|
1971
|
+
const targetKeys = saved.columnOrder ?? saved.visibleColumns;
|
|
1972
|
+
if (targetKeys?.length) {
|
|
1973
|
+
const allByKey = new Map((this.resolvedConfig.columns ?? []).map(c => [c.key, c]));
|
|
1974
|
+
// Colonnes dans l'ordre sauvegardé (on ne garde que celles connues de la config)
|
|
1975
|
+
const ordered = targetKeys
|
|
1976
|
+
.map(key => allByKey.get(key))
|
|
1977
|
+
.filter((c) => c !== undefined);
|
|
1978
|
+
// Colonnes actives actuelllement non présentes dans targetKeys (non-optional passées sous silence)
|
|
1979
|
+
// → on les exclut : l'utilisateur avait explicitement choisi ce sous-ensemble
|
|
1980
|
+
if (ordered.length > 0) {
|
|
1981
|
+
this._view.setActiveColumns(ordered);
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
// Dynamic slices — différé : slicePanelRef pas encore disponible au moment de ngOnChanges
|
|
1985
|
+
if (saved.dynamicSliceKeys?.length) {
|
|
1986
|
+
this._pendingDynamicSliceKeys = saved.dynamicSliceKeys;
|
|
1987
|
+
}
|
|
1988
|
+
// Filtres actifs du slice panel
|
|
1989
|
+
// Filtres actifs du slice panel — toujours stockés en pending :
|
|
1990
|
+
// appliqués par _applyPendingDynamicSliceKeys (si dynamic slices) ou
|
|
1991
|
+
// par _recomputeShowSlicePanel (quand les données arrivent et rendent le panel visible)
|
|
1992
|
+
if (saved.sliceFilters && Object.keys(saved.sliceFilters).length) {
|
|
1993
|
+
this._pendingSliceFilters = saved.sliceFilters;
|
|
1994
|
+
}
|
|
1995
|
+
}
|
|
1120
1996
|
canRemoveColumn(column) {
|
|
1121
|
-
return (this.allowColumnRemoval &&
|
|
1997
|
+
return (this._view.allowColumnRemoval &&
|
|
1122
1998
|
column.removable !== false &&
|
|
1123
1999
|
this.activeColumns.length > 1);
|
|
1124
2000
|
}
|
|
@@ -1131,20 +2007,40 @@ class TableComponent {
|
|
|
1131
2007
|
}
|
|
1132
2008
|
return String(value);
|
|
1133
2009
|
}
|
|
1134
|
-
// ──
|
|
2010
|
+
// ── Export CSV
|
|
2011
|
+
onExport() {
|
|
2012
|
+
this._export.export();
|
|
2013
|
+
}
|
|
2014
|
+
// ── Pipeline de données (privé)
|
|
1135
2015
|
refreshViewModel() {
|
|
1136
2016
|
this._resolvedData = this.resolveData();
|
|
1137
2017
|
this.resolvedConfig = this.buildEffectiveConfig();
|
|
1138
2018
|
this._columnMap = new Map((this.resolvedConfig.columns || []).map(c => [c.key, c]));
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
this.
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
2019
|
+
// ── Initialiser le gestionnaire de préférences si l'option est activée
|
|
2020
|
+
const prefsId = this.resolvedConfig.preferences?.enabled
|
|
2021
|
+
? this.resolvedConfig.preferences.tableId
|
|
2022
|
+
: null;
|
|
2023
|
+
const currentId = prefsId ?? null;
|
|
2024
|
+
// Préférences à appliquer après _view.update() (quand activeColumns est déjà peuplé)
|
|
2025
|
+
let pendingPrefs = null;
|
|
2026
|
+
if (currentId && this._preferencesManager?.key !== ('jqt_prefs_' + currentId)) {
|
|
2027
|
+
this._preferencesManager = new TablePreferencesManager(currentId);
|
|
2028
|
+
this._hasSavedConfig = this._preferencesManager.hasSaved();
|
|
2029
|
+
pendingPrefs = this._preferencesManager.load();
|
|
2030
|
+
}
|
|
2031
|
+
else if (!currentId) {
|
|
2032
|
+
this._preferencesManager = null;
|
|
2033
|
+
this._hasSavedConfig = false;
|
|
2034
|
+
}
|
|
2035
|
+
// Délègue à la facade : mise à jour de l'état View (colonnes, groupBy meta, sliceBy meta)
|
|
2036
|
+
this._view.update({
|
|
2037
|
+
config: this.resolvedConfig.view,
|
|
2038
|
+
columns: this.resolvedConfig.columns ?? [],
|
|
2039
|
+
sliceConfigs: this.resolvedConfig.slices ?? [],
|
|
2040
|
+
});
|
|
2041
|
+
// Synchronise la propriété stable (évite NG0100 — la facade est source de vérité)
|
|
2042
|
+
this.activeColumns = this._view.fields.activeColumns;
|
|
2043
|
+
this.renderedColumns = this.activeColumns.map(c => c.key);
|
|
1148
2044
|
this.pageSize = this.paginator?.pageSize || this.resolvePageSize();
|
|
1149
2045
|
this.pageSizeOptions = this.resolvePageSizeOptions();
|
|
1150
2046
|
this.pageSizeOptionsGroupBy = this.resolvePageSizeOptionsGroupBy();
|
|
@@ -1153,72 +2049,100 @@ class TableComponent {
|
|
|
1153
2049
|
}
|
|
1154
2050
|
this.attachPaginator();
|
|
1155
2051
|
this.attachSort();
|
|
1156
|
-
//
|
|
1157
|
-
|
|
2052
|
+
// Réinitialise _staticSliceHiddenKeys seulement si les clés hidden de la config ont vraiment changé
|
|
2053
|
+
// ET qu'il n'y a pas de préférences sauvegardées (qui géreront hiddenSliceKeys elles-mêmes).
|
|
2054
|
+
const newConfigHidden = new Set((this.resolvedConfig.slices ?? []).filter(s => s.hidden && s.columnKey).map(s => s.columnKey));
|
|
2055
|
+
if (!TableComponent._setsEqual(newConfigHidden, this._configHiddenKeys)) {
|
|
2056
|
+
this._configHiddenKeys = newConfigHidden;
|
|
2057
|
+
// N'écraser _staticSliceHiddenKeys que si on n'est PAS en train d'appliquer des prefs
|
|
2058
|
+
// (pendingPrefs les gèrera lui-même via _applyPreferences → hiddenSliceKeys)
|
|
2059
|
+
if (!pendingPrefs) {
|
|
2060
|
+
this._staticSliceHiddenKeys = new Set(newConfigHidden);
|
|
2061
|
+
this._view.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
// Applique les préférences ICI, APRÈS _view.update() ET après _configHiddenKeys mis à jour,
|
|
2065
|
+
// mais avant _computeSliceConfigs() pour que hiddenSliceKeys soit déjà restauré.
|
|
2066
|
+
if (pendingPrefs) {
|
|
2067
|
+
this._applyPreferences(pendingPrefs);
|
|
2068
|
+
}
|
|
2069
|
+
// Propriétés stables pour les bindings du template
|
|
2070
|
+
// On recalcule _sliceConfigs en reflétant l'état courant des slices cachées,
|
|
2071
|
+
// pour que SlicePanelComponent.ngOnInit reçoive la bonne valeur de `hidden`
|
|
2072
|
+
// même quand le composant est créé pour la 1ère fois après un toggle.
|
|
2073
|
+
this._sliceConfigs = this._computeSliceConfigs();
|
|
1158
2074
|
this._sliceColumns = this.resolvedConfig.columns ?? [];
|
|
1159
2075
|
this._sliceData = this._resolvedData;
|
|
1160
2076
|
this._sliceShowToggle = this.resolvedConfig.enableSliceToggle !== false;
|
|
1161
|
-
this.
|
|
1162
|
-
.filter(s => !!s.columnKey)
|
|
1163
|
-
.map(s => {
|
|
1164
|
-
const colIcon = (this.resolvedConfig.columns ?? []).find(c => c.key === s.columnKey)?.icon;
|
|
1165
|
-
return { key: s.columnKey, title: s.title || humanizeKey(s.columnKey), icon: s.icon ?? colIcon };
|
|
1166
|
-
});
|
|
2077
|
+
this._recomputeShowSlicePanel();
|
|
1167
2078
|
if (!this._activeSort.active && this.resolvedConfig.defaultSort) {
|
|
1168
2079
|
this._matSortActive = this.resolvedConfig.defaultSort.active;
|
|
1169
2080
|
this._matSortDirection = this.resolvedConfig.defaultSort.direction;
|
|
1170
2081
|
}
|
|
1171
|
-
|
|
1172
|
-
const activeDynKeys = new Set(this._activeDynamicSliceColumns.map(c => c.key));
|
|
1173
|
-
const seen = new Set();
|
|
1174
|
-
this._availableColumnsForDynamicSlice = (this.resolvedConfig.columns ?? []).filter(c => {
|
|
1175
|
-
if (c.sliceable === false || staticKeys.has(c.key) || activeDynKeys.has(c.key) || seen.has(c.key))
|
|
1176
|
-
return false;
|
|
1177
|
-
if (!c.header && c.sliceable !== true)
|
|
1178
|
-
return false;
|
|
1179
|
-
seen.add(c.key);
|
|
1180
|
-
return true;
|
|
1181
|
-
});
|
|
1182
|
-
const seen2 = new Set();
|
|
1183
|
-
this._allDynamicSliceColumns = (this.resolvedConfig.columns ?? []).filter(c => {
|
|
1184
|
-
if (c.sliceable === false || staticKeys.has(c.key) || seen2.has(c.key))
|
|
1185
|
-
return false;
|
|
1186
|
-
if (!c.header && c.sliceable !== true)
|
|
1187
|
-
return false;
|
|
1188
|
-
seen2.add(c.key);
|
|
1189
|
-
return true;
|
|
1190
|
-
});
|
|
1191
|
-
this._computeSliceLabel();
|
|
2082
|
+
this._buildSliceByMenuItems();
|
|
1192
2083
|
this._scheduleRender();
|
|
1193
2084
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
2085
|
+
/** Retourne uniquement les slices visibles (non cachées par _staticSliceHiddenKeys).
|
|
2086
|
+
* Le SlicePanelComponent affiche tout ce qu'il reçoit — c'est ici que se fait le filtrage. */
|
|
2087
|
+
_computeSliceConfigs() {
|
|
2088
|
+
return (this.resolvedConfig.slices ?? []).filter(s => !s.columnKey || !this._staticSliceHiddenKeys.has(s.columnKey));
|
|
2089
|
+
}
|
|
2090
|
+
static _setsEqual(a, b) {
|
|
2091
|
+
if (a.size !== b.size)
|
|
2092
|
+
return false;
|
|
2093
|
+
for (const v of a)
|
|
2094
|
+
if (!b.has(v))
|
|
2095
|
+
return false;
|
|
2096
|
+
return true;
|
|
2097
|
+
}
|
|
2098
|
+
_buildSliceByMenuItems() {
|
|
2099
|
+
const staticSliceMap = new Map(this._staticSlicesForMenu.map(s => [s.key, s]));
|
|
2100
|
+
const dynamicSliceSet = new Set(this._allDynamicSliceColumns.map(c => c.key));
|
|
2101
|
+
const seen = new Set();
|
|
2102
|
+
const items = [];
|
|
2103
|
+
for (const col of this.resolvedConfig.columns ?? []) {
|
|
2104
|
+
if (seen.has(col.key))
|
|
2105
|
+
continue;
|
|
2106
|
+
if (staticSliceMap.has(col.key)) {
|
|
2107
|
+
const s = staticSliceMap.get(col.key);
|
|
2108
|
+
items.push({ isStatic: true, key: s.key, title: s.title, icon: s.icon });
|
|
2109
|
+
seen.add(col.key);
|
|
2110
|
+
}
|
|
2111
|
+
else if (dynamicSliceSet.has(col.key)) {
|
|
2112
|
+
items.push({ isStatic: false, key: col.key, title: this.colLabel(col), icon: col.icon, col });
|
|
2113
|
+
seen.add(col.key);
|
|
2114
|
+
}
|
|
1201
2115
|
}
|
|
1202
|
-
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1205
|
-
this._activeSliceByLabel = first?.title || first?.columnKey || '1';
|
|
1206
|
-
return;
|
|
2116
|
+
for (const s of this._staticSlicesForMenu) {
|
|
2117
|
+
if (!seen.has(s.key)) {
|
|
2118
|
+
items.push({ isStatic: true, key: s.key, title: s.title, icon: s.icon });
|
|
1207
2119
|
}
|
|
1208
|
-
this._activeSliceByLabel = this._activeDynamicSliceColumns[0]?.header || '1';
|
|
1209
|
-
return;
|
|
1210
2120
|
}
|
|
1211
|
-
this.
|
|
2121
|
+
this._sliceByMenuItems = items;
|
|
2122
|
+
}
|
|
2123
|
+
get sliceByMenuItems() { return this._sliceByMenuItems; }
|
|
2124
|
+
onSliceMenuItemClick(item) {
|
|
2125
|
+
if (item.isStatic) {
|
|
2126
|
+
this.toggleStaticSlice(item.key);
|
|
2127
|
+
}
|
|
2128
|
+
else if (item.col) {
|
|
2129
|
+
this.toggleDynamicSlice(item.col);
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
isSliceMenuItemActive(item) {
|
|
2133
|
+
return item.isStatic ? this.isStaticSliceVisible(item.key) : this.isDynamicSliceActive(item.key);
|
|
1212
2134
|
}
|
|
1213
2135
|
_scheduleRender() {
|
|
1214
2136
|
if (this._pendingRender !== null) {
|
|
1215
2137
|
clearTimeout(this._pendingRender);
|
|
1216
2138
|
this._pendingRender = null;
|
|
1217
2139
|
}
|
|
1218
|
-
this.
|
|
1219
|
-
this._pendingRender =
|
|
1220
|
-
|
|
1221
|
-
|
|
2140
|
+
this._ngZone.runOutsideAngular(() => {
|
|
2141
|
+
this._pendingRender = setTimeout(() => {
|
|
2142
|
+
this._pendingRender = null;
|
|
2143
|
+
this._ngZone.run(() => this._executeRender());
|
|
2144
|
+
}, 0);
|
|
2145
|
+
});
|
|
1222
2146
|
}
|
|
1223
2147
|
_executeRender() {
|
|
1224
2148
|
const categoryRows = this.getCategoryFilteredData();
|
|
@@ -1239,7 +2163,9 @@ class TableComponent {
|
|
|
1239
2163
|
this._preservePageIndex = null;
|
|
1240
2164
|
}
|
|
1241
2165
|
this._projectCurrentPage();
|
|
1242
|
-
|
|
2166
|
+
// detectChanges() au lieu de markForCheck() : met à jour uniquement CE composant
|
|
2167
|
+
// sans déclencher un cycle CD global qui provoquerait NG0100 dans les parents.
|
|
2168
|
+
this._cdr.detectChanges();
|
|
1243
2169
|
}
|
|
1244
2170
|
_projectCurrentPage() {
|
|
1245
2171
|
if (this.activeGroupByKey) {
|
|
@@ -1252,7 +2178,7 @@ class TableComponent {
|
|
|
1252
2178
|
const projected = [];
|
|
1253
2179
|
pageRows.forEach((row, i) => {
|
|
1254
2180
|
const idx = start + i;
|
|
1255
|
-
const p = {
|
|
2181
|
+
const p = { [ROW_RAW_KEY]: row, [ROW_INDEX_KEY]: idx };
|
|
1256
2182
|
this.activeColumns.forEach((col) => {
|
|
1257
2183
|
p[col.key] = this.resolveCellValue(col, row, idx);
|
|
1258
2184
|
});
|
|
@@ -1266,9 +2192,12 @@ class TableComponent {
|
|
|
1266
2192
|
const columns = config.columns ||
|
|
1267
2193
|
this.columnsConfig ||
|
|
1268
2194
|
this.resolveColumnsFromCompat(rows);
|
|
2195
|
+
// Mode HTML : [view] prend le dessus sur config.view si fourni (Phase 3).
|
|
2196
|
+
const view = this.view ?? config.view;
|
|
1269
2197
|
return {
|
|
1270
2198
|
...config,
|
|
1271
2199
|
columns,
|
|
2200
|
+
view,
|
|
1272
2201
|
slices: config.slices || [],
|
|
1273
2202
|
enableSliceToggle: config.enableSliceToggle ?? true,
|
|
1274
2203
|
};
|
|
@@ -1306,8 +2235,12 @@ class TableComponent {
|
|
|
1306
2235
|
let filtered = data.filter(this.activeSliceFilter);
|
|
1307
2236
|
const query = this.searchQuery?.trim().toLowerCase();
|
|
1308
2237
|
if (query) {
|
|
1309
|
-
|
|
1310
|
-
|
|
2238
|
+
const restrictedKeys = this.resolvedConfig.search?.searchColumns;
|
|
2239
|
+
const searchCols = restrictedKeys?.length
|
|
2240
|
+
? [...this._columnMap.values()].filter(col => restrictedKeys.includes(col.key))
|
|
2241
|
+
: [...this._columnMap.values()];
|
|
2242
|
+
filtered = filtered.filter((row) => searchCols.some((col) => {
|
|
2243
|
+
const val = this.getDisplayValueForSearch(col, row);
|
|
1311
2244
|
return val != null && String(val).toLowerCase().includes(query);
|
|
1312
2245
|
}));
|
|
1313
2246
|
}
|
|
@@ -1318,7 +2251,7 @@ class TableComponent {
|
|
|
1318
2251
|
if (!groupKey) {
|
|
1319
2252
|
const result = [];
|
|
1320
2253
|
rows.forEach((row, index) => {
|
|
1321
|
-
const projected = {
|
|
2254
|
+
const projected = { [ROW_RAW_KEY]: row, [ROW_INDEX_KEY]: index };
|
|
1322
2255
|
this.activeColumns.forEach((col) => {
|
|
1323
2256
|
projected[col.key] = this.resolveCellValue(col, row, index);
|
|
1324
2257
|
});
|
|
@@ -1330,22 +2263,23 @@ class TableComponent {
|
|
|
1330
2263
|
const groupLazyStatus = groupColDef?.lazy ? (this._lazyColumnStatus.get(groupKey) ?? 'idle') : 'loaded';
|
|
1331
2264
|
if (groupLazyStatus !== 'loaded') {
|
|
1332
2265
|
this._totalFilteredCount = rows.length;
|
|
1333
|
-
return [{
|
|
2266
|
+
return [{ [GROUP_HEADER_MARKER]: true, [ROW_GROUP_KEY]: GROUP_KEY_LAZY_LOADING, [ROW_GROUP_COUNT]: rows.length, [ROW_GROUP_PAGE]: 0, [ROW_GROUP_PAGE_COUNT]: 1 }];
|
|
1334
2267
|
}
|
|
1335
2268
|
const effectiveSort = this._activeSort.active && this._activeSort.direction
|
|
1336
2269
|
? this._activeSort
|
|
1337
2270
|
: this.resolvedConfig.defaultSort ?? { active: '', direction: '' };
|
|
1338
2271
|
// Vérifie si le cache est encore valide (même référence de rows + même paramètres de tri/groupe)
|
|
1339
|
-
const cacheValid = this.
|
|
1340
|
-
this.
|
|
1341
|
-
this.
|
|
1342
|
-
this.
|
|
1343
|
-
this.
|
|
2272
|
+
const cacheValid = this._groupCache !== null &&
|
|
2273
|
+
this._groupCache.rows === rows &&
|
|
2274
|
+
this._groupCache.key === groupKey &&
|
|
2275
|
+
this._groupCache.sortOrder === this._groupBy.groupSortOrder &&
|
|
2276
|
+
this._groupCache.sortActive === (effectiveSort.active ?? '') &&
|
|
2277
|
+
this._groupCache.sortDir === (effectiveSort.direction ?? '');
|
|
1344
2278
|
let groupOrder;
|
|
1345
2279
|
let groupMap;
|
|
1346
2280
|
if (cacheValid) {
|
|
1347
|
-
groupOrder = this.
|
|
1348
|
-
groupMap = this.
|
|
2281
|
+
groupOrder = this._groupCache.order;
|
|
2282
|
+
groupMap = this._groupCache.map;
|
|
1349
2283
|
}
|
|
1350
2284
|
else {
|
|
1351
2285
|
// Pré-calcul des valeurs de groupe en un seul passage (évite O(n) find() par comparaison)
|
|
@@ -1376,7 +2310,7 @@ class TableComponent {
|
|
|
1376
2310
|
const va = groupValueCache.get(a);
|
|
1377
2311
|
const vb = groupValueCache.get(b);
|
|
1378
2312
|
const cmp = va.localeCompare(vb);
|
|
1379
|
-
return this.groupSortOrder === 'desc' ? -cmp : cmp;
|
|
2313
|
+
return this._groupBy.groupSortOrder === 'desc' ? -cmp : cmp;
|
|
1380
2314
|
});
|
|
1381
2315
|
groupOrder = [];
|
|
1382
2316
|
groupMap = new Map();
|
|
@@ -1396,34 +2330,36 @@ class TableComponent {
|
|
|
1396
2330
|
});
|
|
1397
2331
|
}
|
|
1398
2332
|
// Mise en cache
|
|
1399
|
-
this.
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
2333
|
+
this._groupCache = {
|
|
2334
|
+
rows,
|
|
2335
|
+
key: groupKey,
|
|
2336
|
+
sortOrder: this._groupBy.groupSortOrder,
|
|
2337
|
+
sortActive: effectiveSort.active ?? '',
|
|
2338
|
+
sortDir: effectiveSort.direction ?? '',
|
|
2339
|
+
order: groupOrder,
|
|
2340
|
+
map: groupMap,
|
|
2341
|
+
};
|
|
1406
2342
|
}
|
|
1407
2343
|
this._totalFilteredCount = rows.length;
|
|
1408
2344
|
// Protection contre les jeux de données à trop haute cardinalité
|
|
1409
2345
|
if (groupOrder.length > TableComponent.MAX_GROUP_COUNT) {
|
|
1410
|
-
return [{
|
|
2346
|
+
return [{ [GROUP_HEADER_MARKER]: true, [ROW_GROUP_KEY]: GROUP_KEY_TOO_MANY, [ROW_GROUP_COUNT]: groupOrder.length, [ROW_GROUP_PAGE]: 0, [ROW_GROUP_PAGE_COUNT]: 1 }];
|
|
1411
2347
|
}
|
|
1412
2348
|
const pageSize = this.groupPageSize;
|
|
1413
2349
|
const result = [];
|
|
1414
2350
|
groupOrder.forEach((groupValue) => {
|
|
1415
2351
|
const groupRows = groupMap.get(groupValue);
|
|
1416
2352
|
const totalCount = groupRows.length;
|
|
1417
|
-
const currentPage = pageSize === 0 ? 0 :
|
|
2353
|
+
const currentPage = pageSize === 0 ? 0 : this._groupBy.getPage(groupValue);
|
|
1418
2354
|
const pageCount = pageSize === 0 ? 1 : Math.max(1, Math.ceil(totalCount / pageSize));
|
|
1419
2355
|
const collapsed = this.isGroupCollapsed(groupValue);
|
|
1420
|
-
result.push({
|
|
2356
|
+
result.push({ [GROUP_HEADER_MARKER]: true, [ROW_GROUP_KEY]: groupValue, [ROW_GROUP_COUNT]: totalCount, [ROW_GROUP_PAGE]: currentPage, [ROW_GROUP_PAGE_COUNT]: pageCount });
|
|
1421
2357
|
if (!collapsed) {
|
|
1422
2358
|
const start = pageSize === 0 ? 0 : currentPage * pageSize;
|
|
1423
2359
|
const end = pageSize === 0 ? totalCount : Math.min(start + pageSize, totalCount);
|
|
1424
2360
|
groupRows.slice(start, end).forEach((row, i) => {
|
|
1425
2361
|
const idx = start + i;
|
|
1426
|
-
const projected = {
|
|
2362
|
+
const projected = { [ROW_RAW_KEY]: row, [ROW_INDEX_KEY]: idx };
|
|
1427
2363
|
this.activeColumns.forEach((col) => {
|
|
1428
2364
|
projected[col.key] = this.resolveCellValue(col, row, idx);
|
|
1429
2365
|
});
|
|
@@ -1455,6 +2391,25 @@ class TableComponent {
|
|
|
1455
2391
|
return direction === 'asc' ? cmp : -cmp;
|
|
1456
2392
|
};
|
|
1457
2393
|
}
|
|
2394
|
+
/**
|
|
2395
|
+
* Retourne la valeur telle qu'elle est affichée dans la cellule, en suivant exactement
|
|
2396
|
+
* le même chemin que resolveCellValue — utilisée pour la recherche plein-texte.
|
|
2397
|
+
* Ne passe pas par sortValue (valeur brute de tri) contrairement à getLazyAwareValue.
|
|
2398
|
+
*/
|
|
2399
|
+
getDisplayValueForSearch(col, row) {
|
|
2400
|
+
if (col.lazy) {
|
|
2401
|
+
const status = this._lazyColumnStatus.get(col.key) ?? 'idle';
|
|
2402
|
+
if (status !== 'loaded')
|
|
2403
|
+
return null;
|
|
2404
|
+
const rowMap = this._lazyColumnData.get(col.key);
|
|
2405
|
+
return normalizeCellValue(rowMap?.get(row) ?? '');
|
|
2406
|
+
}
|
|
2407
|
+
if (col.searchValue) {
|
|
2408
|
+
return col.searchValue(row, 0);
|
|
2409
|
+
}
|
|
2410
|
+
const rawValue = col.value ? col.value(row, 0) : row?.[col.key];
|
|
2411
|
+
return normalizeCellValue(rawValue);
|
|
2412
|
+
}
|
|
1458
2413
|
getLazyAwareValue(columnKey, row) {
|
|
1459
2414
|
const colDef = this._columnMap.get(columnKey);
|
|
1460
2415
|
if (colDef?.lazy) {
|
|
@@ -1472,9 +2427,9 @@ class TableComponent {
|
|
|
1472
2427
|
if (col.lazy) {
|
|
1473
2428
|
const status = this._lazyColumnStatus.get(col.key) ?? 'idle';
|
|
1474
2429
|
if (status === 'loading')
|
|
1475
|
-
return
|
|
2430
|
+
return LAZY_LOADING_VALUE;
|
|
1476
2431
|
if (status === 'error')
|
|
1477
|
-
return
|
|
2432
|
+
return LAZY_ERROR_VALUE;
|
|
1478
2433
|
if (status === 'loaded') {
|
|
1479
2434
|
const rowMap = this._lazyColumnData.get(col.key);
|
|
1480
2435
|
return normalizeCellValue(rowMap?.get(row) ?? '');
|
|
@@ -1534,6 +2489,7 @@ class TableComponent {
|
|
|
1534
2489
|
this._paginatorSubscribed = this.paginator;
|
|
1535
2490
|
this.paginator.page.pipe(takeUntil(this._destroy$)).subscribe(() => {
|
|
1536
2491
|
this.pageSize = this.paginator.pageSize;
|
|
2492
|
+
this.pageChange.emit({ pageIndex: this.paginator.pageIndex, pageSize: this.paginator.pageSize });
|
|
1537
2493
|
if (this._pendingRender !== null)
|
|
1538
2494
|
return;
|
|
1539
2495
|
this._projectCurrentPage();
|
|
@@ -1552,16 +2508,17 @@ class TableComponent {
|
|
|
1552
2508
|
active: this.sort.active,
|
|
1553
2509
|
direction: this.sort.direction,
|
|
1554
2510
|
};
|
|
2511
|
+
this.sortChange.emit({ active: this.sort.active, direction: this.sort.direction });
|
|
1555
2512
|
this._scheduleRender();
|
|
1556
2513
|
});
|
|
1557
2514
|
}
|
|
1558
2515
|
}
|
|
1559
2516
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
1560
|
-
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: TableComponent, isStandalone: true, selector: "jquery-table", inputs: { config: "config", dataSource: "dataSource", data: "data", displayedColumns: "displayedColumns", columnsConfig: "columnsConfig", columnLabels: "columnLabels", isLoading: "isLoading" }, outputs: { addRequested: "addRequested", columnAdded: "columnAdded", columnRemoved: "columnRemoved", categorySelected: "categorySelected", rowSelected: "rowSelected" }, providers: [{ provide: MatPaginatorIntl, useFactory: getFrenchPaginatorIntl }], queries: [{ propertyName: "_cellDefs", predicate: JqtCellDefDirective }], viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true }, { propertyName: "sort", first: true, predicate: MatSort, descendants: true }, { propertyName: "tableBodyScrollRef", first: true, predicate: ["tableBodyScroll"], descendants: true }, { propertyName: "slicePanelRef", first: true, predicate: SlicePanelComponent, descendants: true }, { propertyName: "viewMenuTrigger", first: true, predicate: ["viewMenuTrigger"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<section class=\"jquery-table-wrapper\">\n <header class=\"table-header\" *ngIf=\"showTitle\">\n <h3>{{ title }}</h3>\n </header>\n\n <div\n class=\"table-layout\"\n [class.slice-collapsed]=\"showSlicePanel && (slicePanelRef?.isCollapsed ?? false)\"\n [class.no-slice]=\"!showSlicePanel\"\n >\n <slice-panel\n [style.display]=\"showSlicePanel ? null : 'none'\"\n [sliceConfigs]=\"_sliceConfigs\"\n [columns]=\"_sliceColumns\"\n [data]=\"_sliceData\"\n [lazyData]=\"_lazyColumnData\"\n [lazyStatus]=\"_lazyColumnStatus\"\n [showToggle]=\"_sliceShowToggle\"\n (filterChange)=\"onSliceFilterChange($event)\"\n (dynamicSliceKeysChange)=\"onDynamicSliceKeysChange($event)\"\n ></slice-panel>\n\n <div class=\"table-shell\">\n <div class=\"table-toolbar\" *ngIf=\"showToolbar\">\n <div class=\"table-toolbar-left\">\n <div *ngIf=\"showSearchBar\" class=\"search-input-wrapper\">\n <svg class=\"search-icon\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <circle cx=\"8.5\" cy=\"8.5\" r=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M13 13L17 17\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n <input\n class=\"search-input\"\n type=\"text\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\"\n placeholder=\"Rechercher...\"\n autocomplete=\"off\"\n />\n <button *ngIf=\"searchQuery\" type=\"button\" class=\"search-clear-btn\" (click)=\"searchQuery = ''; onSearchChange()\">\u00D7</button>\n <span class=\"search-count\">{{ filteredRowCount }}</span>\n </div>\n </div>\n <div *ngIf=\"showViewButton\" class=\"table-toolbar-right\">\n <button type=\"button\" class=\"view-btn\" [matMenuTriggerFor]=\"viewMenu\" #viewMenuTrigger=\"matMenuTrigger\">\n <mat-icon class=\"view-btn-icon\">settings</mat-icon>\n View\n </button>\n\n <mat-menu #viewMenu=\"matMenu\" [panelClass]=\"'view-menu-panel'\">\n <button *ngIf=\"showFields\" mat-menu-item [matMenuTriggerFor]=\"fieldsSubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">Champs</span>\n <span class=\"view-menu-sub-value\">{{ visibleColumns.length }}/{{ totalColumnCount }}</span>\n </button>\n <button *ngIf=\"showGroupBySection\" mat-menu-item [matMenuTriggerFor]=\"groupBySubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">Group by</span>\n <span class=\"view-menu-sub-value\">{{ activeGroupByLabel }}</span>\n </button>\n <button *ngIf=\"showAddSliceButton\" mat-menu-item [matMenuTriggerFor]=\"sliceBySubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">Slice by</span>\n <span class=\"view-menu-sub-value\">{{ activeSliceByLabel }}</span>\n </button>\n </mat-menu>\n\n <mat-menu #fieldsSubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let column of menuBaseColumns\"\n [disabled]=\"!column.optional && !allowColumnRemoval\"\n (click)=\"onColumnVisibilityChange(column, !isColumnVisible(column.key))\">\n <span class=\"view-menu-check\">{{ isColumnVisible(column.key) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"column.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ column.icon }}</mat-icon>\n {{ colLabel(column) }}\n </button>\n <ng-container *ngIf=\"menuOptionalColumns.length > 0\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let column of menuOptionalColumns\"\n [disabled]=\"(isColumnVisible(column.key) && visibleColumns.length === 1) || getLazyColumnStatus(column.key) === 'loading'\"\n (click)=\"onColumnVisibilityChange(column, !isColumnVisible(column.key))\">\n <span class=\"view-menu-check\">{{ isColumnVisible(column.key) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"column.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ column.icon }}</mat-icon>\n {{ colLabel(column) }}\n <span *ngIf=\"column.lazy && getLazyColumnStatus(column.key) === 'loading'\" class=\"lazy-menu-spinner\">\u21BB</span>\n <span *ngIf=\"column.lazy && getLazyColumnStatus(column.key) === 'error'\" class=\"lazy-menu-error\" title=\"Erreur de chargement\">\u26A0</span>\n </button>\n </ng-container>\n </div>\n </mat-menu>\n\n <mat-menu #groupBySubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let col of groupByColumns\"\n [class.active]=\"activeGroupByKey === col.key\"\n (click)=\"onGroupByChange(activeGroupByKey === col.key ? null : col.key)\">\n <span class=\"view-menu-check\">{{ activeGroupByKey === col.key ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"col.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ col.icon }}</mat-icon>\n {{ colLabel(col) }}\n </button>\n </div>\n </mat-menu>\n\n <mat-menu #sliceBySubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let s of staticSlicesForMenu\"\n (click)=\"toggleStaticSlice(s.key)\">\n <span class=\"view-menu-check\">{{ isStaticSliceVisible(s.key) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"s.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ s.icon }}</mat-icon>\n {{ s.title }}\n </button>\n <ng-container *ngIf=\"allDynamicSliceColumns.length > 0\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let col of allDynamicSliceColumns\"\n [class.active]=\"isDynamicSliceActive(col.key)\"\n (click)=\"toggleDynamicSlice(col)\">\n <span class=\"view-menu-check\">{{ isDynamicSliceActive(col.key) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"col.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ col.icon }}</mat-icon>\n {{ colLabel(col) }}\n </button>\n </ng-container>\n </div>\n </mat-menu>\n </div>\n </div>\n\n <div class=\"table-body-wrapper\">\n <div class=\"table-body-scroll\" #tableBodyScroll>\n <table mat-table matSort [dataSource]=\"tableDataSource\" class=\"jquery-table\"\n [trackBy]=\"trackRowFn\"\n [matSortActive]=\"_matSortActive\" [matSortDirection]=\"_matSortDirection\">\n <ng-container *ngFor=\"let column of visibleColumns\" [matColumnDef]=\"column.key\">\n <th\n mat-header-cell\n *matHeaderCellDef\n [mat-sort-header]=\"column.sortable !== false ? column.key : ''\"\n [disabled]=\"column.sortable === false\"\n [style.width]=\"column.width || null\"\n [style.min-width]=\"column.width || null\"\n [class.drag-enabled]=\"isColumnDragDropEnabled\"\n [class.drop-target]=\"isHeaderDropTarget(column.key)\"\n (dragover)=\"onHeaderDragOver(column.key, $event)\"\n (drop)=\"onHeaderDrop(column.key, $event)\"\n >\n <div class=\"head-cell\">\n <mat-icon *ngIf=\"column.icon\" class=\"head-icon\" fontSet=\"material-icons-outlined\">{{ column.icon }}</mat-icon>\n <span class=\"head-text\" [title]=\"column.header\">{{ column.header }}</span>\n <span\n *ngIf=\"isColumnDragDropEnabled\"\n class=\"drag-handle\"\n draggable=\"true\"\n (dragstart)=\"onHeaderDragStart(column.key, $event)\"\n (dragend)=\"onHeaderDragEnd()\"\n (click)=\"$event.stopPropagation()\"\n >\n <mat-icon>drag_indicator</mat-icon>\n </span>\n </div>\n </th>\n <td mat-cell *matCellDef=\"let row\"\n [style.width]=\"column.width || null\"\n [style.min-width]=\"column.width || null\">\n <ng-container *ngIf=\"getCellTemplate(column.key) as customTpl; else defaultCellTpl\">\n <ng-container *ngTemplateOutlet=\"customTpl; context: { $implicit: row.__raw, index: row.__index }\"></ng-container>\n </ng-container>\n <ng-template #defaultCellTpl>\n <ng-container [ngSwitch]=\"column.lazy ? getLazyRenderType(row[column.key]) : 'value'\">\n <span *ngSwitchCase=\"'loading'\" class=\"lazy-skeleton\"></span>\n <button *ngSwitchCase=\"'error'\" type=\"button\" class=\"lazy-error-btn\" (click)=\"retryLazyColumn(column)\" title=\"Erreur de chargement \u2014 cliquer pour r\u00E9essayer\">\n <span class=\"lazy-error-icon\">\u26A0</span>\n <span class=\"lazy-error-label\">R\u00E9essayer</span>\n </button>\n <span *ngSwitchDefault class=\"cell-text\" [class.cell-empty]=\"isCellEmpty(row[column.key])\" [title]=\"asTitle(row[column.key])\">{{ isCellEmpty(row[column.key]) ? 'N/A' : row[column.key] }}</span>\n </ng-container>\n </ng-template>\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"renderedColumns; sticky: true\"></tr>\n <tr\n mat-row\n *matRowDef=\"let row; columns: renderedColumns; when: !isGroupHeader\"\n [attr.tabindex]=\"hasRowClickHandler ? 0 : null\"\n [ngClass]=\"getRowClass(row)\"\n [class.no-pointer]=\"!hasRowClickHandler\"\n (click)=\"onRowClick(row, $event)\"\n (keydown.enter)=\"onRowClick(row, null)\"\n (keydown.space)=\"onRowClick(row, null)\"\n ></tr>\n <ng-container matColumnDef=\"__group_row\">\n <td mat-cell *matCellDef=\"let row\" [attr.colspan]=\"renderedColumns.length\" class=\"group-header-cell\">\n <div class=\"group-header-label\">\n <span class=\"group-chevron\" [class.collapsed]=\"isGroupCollapsed(row.__groupKey)\" *ngIf=\"row.__groupKey !== '__lazy_loading__' && row.__groupKey !== '__too_many_groups__'\">\u203A</span>\n <span *ngIf=\"row.__groupKey !== '__lazy_loading__' && row.__groupKey !== '__too_many_groups__'\" class=\"group-key-label\">{{ row.__groupKey }}</span>\n <span *ngIf=\"row.__groupKey === '__lazy_loading__'\" class=\"lazy-skeleton group-lazy-key-skeleton\"></span>\n <span *ngIf=\"row.__groupKey === '__too_many_groups__'\" class=\"group-too-many-label\">\u26A0 Trop de groupes ({{ row.__groupCount }}) \u2014 affinez les donn\u00E9es ou choisissez une autre colonne</span>\n <span class=\"group-count-badge\">{{ row.__groupCount }}</span>\n <div class=\"group-pagination\" *ngIf=\"!isGroupCollapsed(row.__groupKey) && row.__groupPageCount > 1\" (click)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"group-page-btn\" [disabled]=\"row.__groupPage === 0\" (click)=\"groupPageBack(row.__groupKey)\">\u2039</button>\n <span class=\"group-page-info\">{{ row.__groupPage + 1 }}\u00A0/\u00A0{{ row.__groupPageCount }}</span>\n <button type=\"button\" class=\"group-page-btn\" [disabled]=\"row.__groupPage >= row.__groupPageCount - 1\" (click)=\"groupPageForward(row.__groupKey, row.__groupCount)\">\u203A</button>\n </div>\n </div>\n </td>\n </ng-container>\n <tr mat-row *matRowDef=\"let row; columns: ['__group_row']; when: isGroupHeader\"\n class=\"group-header-row\"\n [class.group-header-loading]=\"row.__groupKey === '__lazy_loading__'\"\n [class.group-header-too-many]=\"row.__groupKey === '__too_many_groups__'\"\n [attr.data-group-key]=\"row.__groupKey\"\n (click)=\"row.__groupKey !== '__lazy_loading__' && row.__groupKey !== '__too_many_groups__' && toggleGroupCollapse(row.__groupKey)\"></tr>\n <tr class=\"mat-mdc-row no-data-row\" *matNoDataRow>\n <td class=\"mat-mdc-cell no-data-cell\" [attr.colspan]=\"renderedColumns.length\"></td>\n </tr>\n </table>\n </div><!-- /table-body-scroll -->\n <div class=\"table-body-overlay\" *ngIf=\"effectiveIsLoading || isEmpty\">\n <span class=\"table-body-overlay-text\">{{ effectiveIsLoading ? loadingStateLabel : emptyStateLabel }}</span>\n </div>\n </div><!-- /table-body-wrapper -->\n\n <div class=\"bottom-bar\" *ngIf=\"showPagination\">\n <mat-paginator\n [length]=\"totalRows\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"pageSizeOptions\"\n ></mat-paginator>\n </div>\n\n <div class=\"group-bottom-bar\" *ngIf=\"activeGroupByKey\">\n <div class=\"group-bottom-bar-container\">\n <div class=\"group-bottom-bar-left\">\n <button type=\"button\" class=\"group-reset-btn\" (click)=\"onGroupByChange(null)\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" class=\"group-reset-icon\">\n <path d=\"M3 8a5 5 0 1 0 5-5 5 5 0 0 0-3.54 1.46L3 3\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M3 3v2.5H5.5\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n Retirer le groupe\n </button>\n <div class=\"group-bar-separator\"></div>\n <div class=\"group-sort-controls\">\n <span class=\"group-sort-label\">Trier :</span>\n <button type=\"button\" class=\"group-sort-btn\" [class.active]=\"groupSortOrder === 'asc'\" (click)=\"onGroupSortToggle('asc')\" title=\"A \u2192 Z\">\n <mat-icon>arrow_upward</mat-icon>\n </button>\n <button type=\"button\" class=\"group-sort-btn\" [class.active]=\"groupSortOrder === 'desc'\" (click)=\"onGroupSortToggle('desc')\" title=\"Z \u2192 A\">\n <mat-icon>arrow_downward</mat-icon>\n </button>\n </div>\n </div>\n <div class=\"mat-mdc-paginator-page-size\">\n <div class=\"mat-mdc-paginator-page-size-label\">Lignes par groupe :</div>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"mat-mdc-paginator-page-size-select\">\n <mat-select\n [(ngModel)]=\"groupPageSize\"\n (ngModelChange)=\"onGroupPageSizeChange($event)\"\n panelClass=\"mat-mdc-paginator-select-page-size group-page-size-panel\"\n [panelWidth]=\"'auto'\"\n >\n <mat-select-trigger>{{ groupPageSize === 0 ? 'Toutes' : groupPageSize }}</mat-select-trigger>\n <mat-option *ngFor=\"let opt of pageSizeOptionsGroupBy\" [value]=\"opt\">{{ opt }}</mat-option>\n <mat-option [value]=\"0\">Toutes</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </div>\n </div>\n </div>\n</section>\n", styles: [":host{display:block;max-height:100%;min-height:inherit;--jqt-title-color: #1f2937;--jqt-text-color: #111827;--jqt-text-secondary: #6b7280;--jqt-text-muted: #9ca3af;--jqt-surface-color: #ffffff;--jqt-surface-alt-color: #f9fafb;--jqt-row-hover-bg: #f3f4f6;--jqt-border-color: #e5e7eb;--jqt-group-bg: #eef2f7;--jqt-group-bg-hover: #e4ecf4;--jqt-group-accent: #94a3b8;--jqt-group-border: #d1d9e4;--jqt-primary-color: #1d4ed8;--jqt-danger-color: #dc2626;--jqt-danger-hover-bg: #fef2f2;--jqt-danger-border: #fca5a5;--jqt-header-bg: #f5f5f5;--jqt-header-text-color: #374151;--jqt-cell-bg: #ffffff;--jqt-footer-bg: #f5f5f5;--jqt-footer-text-color: #374151;--jqt-footer-icon-color: #374151}.jquery-table-wrapper{display:flex;flex-direction:column;width:100%;height:100%;min-height:inherit;padding:2px 2px 0;box-sizing:border-box}.table-header{display:flex;align-items:center;justify-content:flex-start;gap:8px;margin-bottom:12px}.table-header h3{margin:0;font-size:20px;font-weight:600;color:var(--jqt-title-color)}.table-layout{display:grid;grid-template-columns:minmax(155px,190px) minmax(0,1fr);gap:12px;align-items:stretch;min-height:inherit;height:100%}.table-layout.no-slice{grid-template-columns:minmax(0,1fr)}.table-layout.slice-collapsed{grid-template-columns:40px minmax(0,1fr)}.table-shell{display:flex;flex-direction:column;flex:1 1 0;min-height:0;min-width:0;width:100%;max-width:100%}.table-body-wrapper{position:relative;flex:1 1 0;min-height:0;display:flex;flex-direction:column}.table-body-scroll{flex:1 1 0;overflow-y:auto;min-height:0;border-top:1px solid var(--jqt-border-color);border-right:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);border-radius:8px 8px 0 0;scrollbar-width:thin;scrollbar-color:var(--jqt-border-color) transparent}.table-body-scroll::-webkit-scrollbar{width:6px}.table-body-scroll::-webkit-scrollbar-thumb{background:var(--jqt-border-color);border-radius:4px}.table-body-overlay{position:absolute;top:var(--actual-header-height, 56px);left:0;right:0;bottom:0;z-index:3;display:flex;align-items:center;justify-content:center;background:var(--jqt-surface-color);border-right:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);pointer-events:none}.table-body-overlay-text{font-size:14px;color:var(--jqt-text-secondary)}.table-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 0 8px}.table-toolbar-left{flex:1 1 auto;min-width:0}.table-toolbar-right{flex:0 0 auto}.search-input-wrapper{position:relative;display:inline-flex;align-items:center;width:100%;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 8px;box-sizing:border-box;gap:6px}.search-input-wrapper:focus-within{border-color:var(--jqt-text-muted)}.search-input-wrapper .search-icon{flex-shrink:0;width:14px;height:14px;color:var(--jqt-text-muted)}.search-input{flex:1;border:none;outline:none;font-size:13px;color:var(--jqt-text-color);background:transparent;padding:7px 0;min-width:0}.search-input::placeholder{color:var(--jqt-text-muted)}.search-count{flex-shrink:0;font-size:12px;font-weight:600;color:var(--jqt-text-secondary);background:var(--jqt-row-hover-bg);border-radius:10px;padding:1px 7px;min-width:20px;text-align:center;line-height:18px}.search-clear-btn{background:none;border:none;cursor:pointer;color:var(--jqt-text-muted);font-size:16px;padding:0;line-height:1;flex-shrink:0}.search-clear-btn:hover{color:var(--jqt-text-color)}.view-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.view-btn:hover{border-color:var(--jqt-text-muted)}.view-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.group-header-row{cursor:pointer}:host ::ng-deep .jquery-table{border-spacing:0}:host ::ng-deep .group-header-row td{padding:0!important;background:var(--jqt-group-bg);border-bottom:1px solid var(--jqt-group-border);border-top:1px solid var(--jqt-group-border);border-left:3px solid var(--jqt-group-accent);position:sticky;top:var(--actual-header-height, 56px);z-index:2;box-shadow:-4px 0 0 0 var(--jqt-group-bg)}:host ::ng-deep .group-header-row:hover td{background:var(--jqt-group-bg-hover);box-shadow:-4px 0 0 0 var(--jqt-group-bg-hover)}:host ::ng-deep .group-header-loading td,:host ::ng-deep .group-header-loading:hover td,:host ::ng-deep .group-header-too-many td,:host ::ng-deep .group-header-too-many:hover td{cursor:default;background:var(--jqt-surface-alt-color)}.group-too-many-label{font-size:12px;font-weight:500;color:var(--jqt-warn-color, #b45309);font-style:italic}.group-header-cell{padding:0!important}.group-header-label{display:flex;align-items:center;gap:6px;padding:6px 16px;font-size:12px;font-weight:700;color:var(--jqt-text-color);text-transform:uppercase;letter-spacing:.04em}.group-key-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-count-badge{flex-shrink:0;font-size:11px;line-height:1;font-weight:600;color:#fff;background:var(--jqt-text-secondary, #6b7280);border-radius:10px;padding:3px 7px;min-width:20px;text-align:center;text-transform:none;letter-spacing:0}.group-pagination{display:inline-flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.group-page-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:1px solid var(--jqt-border-color);border-radius:5px;background:var(--jqt-surface-color);cursor:pointer;font-size:14px;color:var(--jqt-text-color);line-height:1;padding:0 0 3px;text-transform:none;letter-spacing:0}.group-page-btn:hover:not(:disabled){border-color:var(--jqt-text-muted);background:var(--jqt-surface-alt-color)}.group-page-btn:disabled{opacity:.35;cursor:default}.group-page-info{font-size:11px;color:var(--jqt-text-secondary);min-width:36px;text-align:center;font-weight:500;text-transform:none;letter-spacing:0}.group-chevron{flex-shrink:0;font-size:14px;font-weight:700;padding-bottom:3px;transition:transform .15s ease;transform:rotate(90deg)}.group-chevron.collapsed{transform:rotate(0)}.no-data-row{cursor:default!important}.no-data-cell{padding:0!important;height:0!important;border:none!important}.jquery-table{width:100%;table-layout:fixed}:host ::ng-deep .mat-mdc-table{width:100%;background:transparent}:host ::ng-deep .mat-mdc-header-cell,:host ::ng-deep .mat-mdc-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host ::ng-deep .mat-mdc-header-row{background:var(--jqt-header-bg)}:host ::ng-deep .mat-mdc-header-cell{background:var(--jqt-header-bg);color:var(--jqt-header-text-color)}:host ::ng-deep .mat-mdc-row,:host ::ng-deep .mat-mdc-cell{background:var(--jqt-cell-bg)}:host ::ng-deep .mat-mdc-row{cursor:pointer;border-bottom-color:var(--jqt-border-color)}:host ::ng-deep .mat-mdc-row.no-pointer{cursor:auto}:host ::ng-deep .mat-mdc-row.no-data-row{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drag-enabled{-webkit-user-select:none;user-select:none}:host ::ng-deep .mat-mdc-header-cell.drag-enabled:active{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drop-target{background:var(--jqt-row-hover-bg)}.head-cell{display:flex;align-items:center;justify-content:space-between;gap:6px;min-width:0}.drag-handle{display:inline-flex;align-items:center;cursor:grab;color:var(--jqt-border-color);flex-shrink:0;padding:0 2px;border-radius:4px;line-height:1}.drag-handle:active{cursor:grabbing}.drag-handle:hover{color:var(--jqt-text-muted)}.drag-handle mat-icon{font-size:16px;width:16px;height:16px;line-height:1}.head-icon{font-size:14px;width:14px;height:14px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.head-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase;font-size:11px;font-weight:600;letter-spacing:.05em}.head-actions{display:inline-flex;align-items:center;gap:6px}.cell-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cell-empty{color:var(--jqt-empty-cell-color, #9ca3af);font-style:italic}.bottom-bar{display:flex;align-items:center;flex:0 0 auto;background:var(--jqt-footer-bg);border-top:1px solid var(--jqt-border-color);border-right:1px solid var(--jqt-border-color);border-bottom:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);border-radius:0 0 8px 8px}:host ::ng-deep .bottom-bar mat-paginator{flex:1 1 auto;background:transparent;color:var(--jqt-footer-text-color)}:host ::ng-deep .bottom-bar .mat-mdc-paginator-outer-container{display:flex}:host ::ng-deep .bottom-bar .mat-mdc-paginator-container{display:flex;align-items:center;flex-wrap:nowrap;min-height:0;height:var(--actual-header-height, 56px);padding:0 8px;box-sizing:border-box}:host ::ng-deep .bottom-bar .mat-mdc-paginator-range-label,:host ::ng-deep .bottom-bar .mat-mdc-paginator-page-size-label{font-size:12px;color:var(--jqt-footer-text-color)}:host ::ng-deep .bottom-bar .mat-mdc-icon-button{color:var(--jqt-footer-icon-color);width:28px;height:28px;padding:0;line-height:1;display:inline-flex;align-items:center;justify-content:center;--mdc-icon-button-state-layer-size: 28px}:host ::ng-deep .bottom-bar .mat-mdc-icon-button .mat-mdc-button-persistent-ripple,:host ::ng-deep .bottom-bar .mat-mdc-icon-button .mdc-icon-button__state-layer{width:28px;height:28px;top:50%;left:50%;transform:translate(-50%,-50%)}:host ::ng-deep .bottom-bar .mat-mdc-icon-button .mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}:host ::ng-deep .bottom-bar .mat-mdc-icon-button:disabled{opacity:.35}:host ::ng-deep .bottom-bar .mat-mdc-paginator-page-size-select{width:72px;font-size:12px}:host ::ng-deep .bottom-bar .mat-mdc-text-field-wrapper{padding:0 8px}:host ::ng-deep .bottom-bar .mdc-text-field--outlined{height:28px}:host ::ng-deep .bottom-bar .mat-mdc-form-field-infix{padding-top:0;padding-bottom:0;min-height:0;height:28px;display:flex;align-items:center}:host ::ng-deep .bottom-bar .mat-mdc-select-value-text{font-size:12px;color:var(--jqt-text-color)}:host ::ng-deep .bottom-bar .mdc-notched-outline__leading,:host ::ng-deep .bottom-bar .mdc-notched-outline__notch,:host ::ng-deep .bottom-bar .mdc-notched-outline__trailing{border-color:var(--jqt-border-color)}.group-bottom-bar{display:flex;align-items:center;flex:0 0 auto;background:var(--jqt-footer-bg);border-top:1px solid var(--jqt-border-color);border-right:1px solid var(--jqt-border-color);border-bottom:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);border-radius:0 0 8px 8px}.group-bottom-bar-container{display:flex;align-items:center;justify-content:space-between;flex-wrap:nowrap;height:var(--actual-header-height, 56px);padding:0 16px;gap:16px;width:100%;box-sizing:border-box}.group-bar-separator{width:1px;height:20px;background:#9ca3af;flex-shrink:0;margin:0 4px}.group-bottom-bar-left{display:flex;align-items:center;gap:8px;flex-shrink:0}.group-sort-controls{display:flex;align-items:center;gap:2px}.group-sort-label{font-size:12px;color:var(--jqt-footer-text-color);white-space:nowrap;margin-right:2px}.group-sort-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--jqt-footer-icon-color);cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}.group-sort-btn mat-icon{font-size:16px;width:16px;height:16px;line-height:16px}.group-sort-btn:hover{background:#0000000a}.group-sort-btn.active{color:var(--jqt-primary-color, #1976d2);border-color:var(--jqt-primary-color, #1976d2);background:#1976d214}:host ::ng-deep .group-bottom-bar .mat-mdc-paginator-page-size{display:flex;align-items:center;gap:8px}:host ::ng-deep .group-bottom-bar .mat-mdc-paginator-page-size-label{font-size:12px;color:var(--jqt-footer-text-color);white-space:nowrap}:host ::ng-deep .group-bottom-bar .mat-mdc-paginator-page-size-select{width:72px;font-size:12px}:host ::ng-deep .group-bottom-bar .mat-mdc-text-field-wrapper{padding:0 8px}:host ::ng-deep .group-bottom-bar .mdc-text-field--outlined{height:28px}:host ::ng-deep .group-bottom-bar .mat-mdc-form-field-infix{padding-top:0;padding-bottom:0;min-height:0;height:28px;display:flex;align-items:center}:host ::ng-deep .group-bottom-bar .mat-mdc-select-value-text{font-size:12px;color:var(--jqt-text-color)}:host ::ng-deep .group-bottom-bar .mdc-notched-outline__leading,:host ::ng-deep .group-bottom-bar .mdc-notched-outline__notch,:host ::ng-deep .group-bottom-bar .mdc-notched-outline__trailing{border-color:var(--jqt-border-color)}:host ::ng-deep .group-bottom-bar .mat-mdc-select-arrow{color:var(--jqt-footer-text-color)}.group-reset-btn{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);color:var(--jqt-text-secondary);font-size:12px;font-weight:500;font-family:inherit;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease;white-space:nowrap;flex-shrink:0}.group-reset-btn:hover{background:var(--jqt-danger-hover-bg);color:var(--jqt-danger-color);border-color:var(--jqt-danger-border)}.group-reset-icon{width:14px;height:14px;flex-shrink:0}::ng-deep .mat-mdc-paginator-select-page-size .mat-pseudo-checkbox{display:none!important}::ng-deep .group-page-size-panel{min-width:90px!important}::ng-deep .group-page-size-panel .mat-mdc-option{justify-content:center;min-height:40px}::ng-deep .group-page-size-panel .mdc-list-item__primary-text{text-align:center;width:100%}@media (max-width: 960px){.table-layout{grid-template-columns:1fr}}::ng-deep .view-menu-panel .mat-mdc-menu-panel{min-width:220px!important;overflow:visible!important}::ng-deep .view-menu-panel .mat-mdc-menu-content{padding:4px 0!important}::ng-deep .view-submenu-panel .mat-mdc-menu-panel{min-width:200px!important}::ng-deep .view-submenu-panel .mat-mdc-menu-content{padding:0!important}::ng-deep .view-menu-panel .view-menu-sub-trigger.mat-mdc-menu-item{min-height:42px;padding:0 14px;display:flex;align-items:center;box-sizing:border-box}::ng-deep .view-menu-panel .view-menu-sub-trigger .mdc-list-item__primary-text{display:flex;align-items:center;width:100%;gap:0;overflow:hidden}::ng-deep .view-menu-sub-label{font-size:13px;font-weight:500;color:var(--jqt-text-color);flex:0 0 auto;white-space:nowrap;padding-right:8px}::ng-deep .view-menu-sub-value{font-size:12px;color:var(--jqt-text-secondary);flex:1 1 auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:right}::ng-deep .view-submenu{padding:4px 6px;min-width:180px;box-sizing:border-box}::ng-deep .view-submenu .view-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px;border:none;background:transparent;border-radius:6px;font-size:13px;color:var(--jqt-text-color);cursor:pointer;text-align:left;box-sizing:border-box;white-space:nowrap}::ng-deep .view-submenu .view-menu-item:hover{background:var(--jqt-row-hover-bg)}::ng-deep .view-submenu .view-menu-item.active{font-weight:600}::ng-deep .view-submenu .view-menu-item:disabled{opacity:.4;cursor:default}::ng-deep .view-menu-col-icon{flex-shrink:0;font-size:14px!important;width:14px!important;height:14px!important;color:var(--jqt-text-muted)}::ng-deep .view-menu-check{flex-shrink:0;width:16px;text-align:center;font-size:12px;color:var(--jqt-text-color)}::ng-deep .view-menu-separator{border:none;border-top:1px solid var(--jqt-border-color);margin:4px 0}.lazy-skeleton{display:block;height:12px;width:70%;border-radius:4px;background:linear-gradient(90deg,var(--jqt-border-color) 25%,var(--jqt-surface-alt-color) 50%,var(--jqt-border-color) 75%);background-size:200% 100%;animation:lazy-shimmer 1.4s ease-in-out infinite}.group-lazy-key-skeleton{flex:1 1 auto;min-width:80px;height:13px;border-radius:4px}@keyframes lazy-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.lazy-error-btn{display:inline-flex;align-items:center;gap:4px;background:none;border:1px solid #fca5a5;border-radius:4px;padding:2px 6px;cursor:pointer;color:#ef4444;font-size:12px;opacity:.85;transition:opacity .15s}.lazy-error-btn:hover{opacity:1;background:#fef2f2}.lazy-error-icon{font-size:12px}.lazy-error-label{font-size:11px;font-weight:500}::ng-deep .lazy-menu-spinner{display:inline-block;margin-left:auto;font-size:13px;color:var(--jqt-text-secondary);animation:lazy-spin 1s linear infinite}::ng-deep .lazy-menu-error{display:inline-block;margin-left:auto;font-size:13px;color:#ef4444}@keyframes lazy-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatTableModule }, { kind: "component", type: i3.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i3.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i3.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i3.MatColumnDef, selector: "[matColumnDef]", inputs: ["sticky", "matColumnDef"] }, { kind: "directive", type: i3.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i3.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i3.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i3.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i3.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i3.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "directive", type: i3.MatNoDataRow, selector: "ng-template[matNoDataRow]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatPaginatorModule }, { kind: "component", type: i4.MatPaginator, selector: "mat-paginator", inputs: ["disabled"], exportAs: ["matPaginator"] }, { kind: "ngmodule", type: MatSortModule }, { kind: "directive", type: i5.MatSort, selector: "[matSort]", inputs: ["matSortDisabled", "matSortActive", "matSortStart", "matSortDirection", "matSortDisableClear"], outputs: ["matSortChange"], exportAs: ["matSort"] }, { kind: "component", type: i5.MatSortHeader, selector: "[mat-sort-header]", inputs: ["disabled", "mat-sort-header", "arrowPosition", "start", "sortActionDescription", "disableClear"], exportAs: ["matSortHeader"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i6.MatMenu, selector: "mat-menu", exportAs: ["matMenu"] }, { kind: "component", type: i6.MatMenuItem, selector: "[mat-menu-item]", inputs: ["disabled", "disableRipple", "role"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i6.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i8.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "component", type: i9.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex", "panelWidth", "hideSingleSelectionIndicator"], exportAs: ["matSelect"] }, { kind: "directive", type: i9.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "component", type: i10.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { kind: "component", type: SlicePanelComponent, selector: "slice-panel", inputs: ["sliceConfigs", "columns", "data", "lazyData", "lazyStatus", "showToggle", "showCounts", "collapsedByDefault", "alwaysShow"], outputs: ["filterChange", "activeKeysChange", "dynamicSliceKeysChange", "collapsedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
2517
|
+
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: TableComponent, isStandalone: true, selector: "jquery-table", inputs: { config: "config", dataSource: "dataSource", data: "data", displayedColumns: "displayedColumns", columnsConfig: "columnsConfig", view: "view", columnLabels: "columnLabels", isLoading: "isLoading" }, outputs: { addRequested: "addRequested", columnAdded: "columnAdded", columnRemoved: "columnRemoved", categorySelected: "categorySelected", rowSelected: "rowSelected", sortChange: "sortChange", pageChange: "pageChange", searchChange: "searchChange", groupByChange: "groupByChange", columnsChange: "columnsChange" }, providers: [{ provide: MatPaginatorIntl, useFactory: getFrenchPaginatorIntl }], queries: [{ propertyName: "_cellDefs", predicate: JqtCellDefDirective }], viewQueries: [{ propertyName: "paginator", first: true, predicate: MatPaginator, descendants: true }, { propertyName: "sort", first: true, predicate: MatSort, descendants: true }, { propertyName: "tableBodyScrollRef", first: true, predicate: ["tableBodyScroll"], descendants: true }, { propertyName: "slicePanelRef", first: true, predicate: SlicePanelComponent, descendants: true }, { propertyName: "viewMenuTrigger", first: true, predicate: ["viewMenuTrigger"], descendants: true }], usesOnChanges: true, ngImport: i0, template: "<section class=\"jquery-table-wrapper\">\n <header class=\"table-header\" *ngIf=\"showTitle\">\n <h3>{{ title }}</h3>\n </header>\n\n <div\n class=\"table-layout\"\n [class.no-slice]=\"!_showSlicePanel || _slicePanelCollapsed\"\n >\n <slice-panel\n [style.display]=\"!_showSlicePanel || _slicePanelCollapsed ? 'none' : null\"\n [sliceConfigs]=\"_sliceConfigs\"\n [columns]=\"_sliceColumns\"\n [data]=\"_sliceData\"\n [lazyData]=\"_lazyColumnData\"\n [lazyStatus]=\"_lazyColumnStatus\"\n [showToggle]=\"_sliceShowToggle\"\n [collapsedByDefault]=\"_slicePanelCollapsed\"\n (filterChange)=\"onSliceFilterChange($event)\"\n (dynamicSliceKeysChange)=\"onDynamicSliceKeysChange($event)\"\n (collapsedChange)=\"onSlicePanelCollapsedChange($event)\"\n ></slice-panel>\n\n <div class=\"table-shell\">\n <div class=\"table-toolbar\" *ngIf=\"showToolbar\">\n <div class=\"table-toolbar-left\">\n <button\n *ngIf=\"showSliceExpandBtn\"\n type=\"button\"\n class=\"slice-expand-btn\"\n [title]=\"i18n.expandFilterTitle\"\n (click)=\"expandSlicePanel()\"\n ><mat-icon class=\"slice-expand-mat-icon\">menu</mat-icon></button>\n <div *ngIf=\"showSearchBar\" class=\"search-input-wrapper\">\n <svg class=\"search-icon\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <circle cx=\"8.5\" cy=\"8.5\" r=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M13 13L17 17\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n <input\n class=\"search-input\"\n type=\"text\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\"\n [placeholder]=\"i18n.searchPlaceholder\"\n autocomplete=\"off\"\n />\n <button *ngIf=\"searchQuery\" type=\"button\" class=\"search-clear-btn\" (click)=\"searchQuery = ''; onSearchChange()\">\u00D7</button>\n <span class=\"search-count\">{{ filteredRowCount }}</span>\n </div>\n </div>\n <div *ngIf=\"showViewButton\" class=\"table-toolbar-right\">\n <button *ngIf=\"showViewButton\" type=\"button\" class=\"view-btn\" [matMenuTriggerFor]=\"viewMenu\" #viewMenuTrigger=\"matMenuTrigger\" [attr.aria-label]=\"'View'\">\n <mat-icon class=\"view-btn-icon\">settings</mat-icon>\n </button>\n\n <mat-menu #viewMenu=\"matMenu\" [panelClass]=\"'view-menu-panel'\">\n <button *ngIf=\"showFields\" mat-menu-item [matMenuTriggerFor]=\"fieldsSubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">{{ i18n.viewMenuFields }}</span>\n <span class=\"view-menu-sub-value\">{{ visibleColumns.length }}/{{ totalColumnCount }}</span>\n </button>\n <button *ngIf=\"showGroupBySection\" mat-menu-item [matMenuTriggerFor]=\"groupBySubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">{{ i18n.viewMenuGroupBy }}</span>\n <span class=\"view-menu-sub-value\">{{ activeGroupByLabel }}</span>\n </button>\n <button *ngIf=\"showAddSliceButton\" mat-menu-item [matMenuTriggerFor]=\"sliceBySubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">{{ i18n.viewMenuSliceBy }}</span>\n <span class=\"view-menu-sub-value\">{{ activeSliceByLabel }}</span>\n </button>\n <mat-divider *ngIf=\"showExportButton\" class=\"view-menu-export-divider\"></mat-divider>\n <button *ngIf=\"showExportButton\" mat-menu-item type=\"button\" class=\"view-menu-export-item\"\n (click)=\"onExport(); viewMenuTrigger.closeMenu()\">\n <mat-icon class=\"view-menu-export-icon\">download</mat-icon>\n <span class=\"view-menu-sub-label\">{{ i18n.exportButtonLabel }}</span>\n </button>\n <mat-divider *ngIf=\"showPreferencesMenu\" class=\"view-menu-export-divider\"></mat-divider>\n <button *ngIf=\"showPreferencesMenu\" mat-menu-item [matMenuTriggerFor]=\"preferencesSubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">{{ i18n.preferencesMenuLabel }}</span>\n </button>\n </mat-menu>\n\n <mat-menu #preferencesSubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" (mousedown)=\"enterEditMode(); viewMenuTrigger.closeMenu()\">\n <mat-icon class=\"view-menu-col-icon\">edit</mat-icon>\n {{ i18n.preferencesEdit }}\n </button>\n <button type=\"button\" class=\"view-menu-item\" (mousedown)=\"savePreferences(); viewMenuTrigger.closeMenu()\">\n <mat-icon class=\"view-menu-col-icon\">save</mat-icon>\n {{ i18n.preferencesSave }}\n </button>\n <button type=\"button\" class=\"view-menu-item\" [disabled]=\"!hasSavedPreferences\" (mousedown)=\"clearPreferences(); viewMenuTrigger.closeMenu()\">\n <mat-icon class=\"view-menu-col-icon\">delete_outline</mat-icon>\n {{ i18n.preferencesClear }}\n </button>\n </div>\n </mat-menu>\n\n <mat-menu #fieldsSubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let column of menuBaseColumns\"\n [disabled]=\"!column.optional && !allowColumnRemoval\"\n (mousedown)=\"onColumnVisibilityChange(column, !isColumnVisible(column.key))\">\n <span class=\"view-menu-check\">{{ isColumnVisible(column.key) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"column.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ column.icon }}</mat-icon>\n {{ colLabel(column) }}\n </button>\n <ng-container *ngIf=\"menuOptionalColumns.length > 0\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let column of menuOptionalColumns\"\n [disabled]=\"(isColumnVisible(column.key) && visibleColumns.length === 1) || getLazyColumnStatus(column.key) === 'loading'\"\n (mousedown)=\"onColumnVisibilityChange(column, !isColumnVisible(column.key))\">\n <span class=\"view-menu-check\">{{ isColumnVisible(column.key) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"column.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ column.icon }}</mat-icon>\n {{ colLabel(column) }}\n <span *ngIf=\"column.lazy && getLazyColumnStatus(column.key) === 'loading'\" class=\"lazy-menu-spinner\">\u21BB</span>\n <span *ngIf=\"column.lazy && getLazyColumnStatus(column.key) === 'error'\" class=\"lazy-menu-error\" [title]=\"i18n.lazyErrorMenuTitle\">\u26A0</span>\n </button>\n </ng-container>\n </div>\n </mat-menu>\n\n <mat-menu #groupBySubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let col of groupByColumns\"\n [class.active]=\"activeGroupByKey === col.key\"\n (mousedown)=\"onGroupByChange(activeGroupByKey === col.key ? null : col.key)\">\n <span class=\"view-menu-check\">{{ activeGroupByKey === col.key ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"col.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ col.icon }}</mat-icon>\n {{ colLabel(col) }}\n </button>\n </div>\n </mat-menu>\n\n <mat-menu #sliceBySubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let item of sliceByMenuItems\"\n [class.active]=\"isSliceMenuItemActive(item)\"\n (mousedown)=\"onSliceMenuItemClick(item)\">\n <span class=\"view-menu-check\">{{ isSliceMenuItemActive(item) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"item.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ item.icon }}</mat-icon>\n {{ item.title }}\n </button>\n </div>\n </mat-menu>\n </div>\n </div>\n\n <div class=\"table-body-wrapper\">\n\n <!-- Banni\u00E8re message pr\u00E9f\u00E9rences -->\n <div *ngIf=\"_preferencesMessage\" class=\"preferences-message\">\n <mat-icon class=\"preferences-message-icon\">check_circle</mat-icon>\n <span>{{ _preferencesMessage }}</span>\n </div>\n\n <!-- Bouton de confirmation du mode \u00E9dition -->\n <button\n *ngIf=\"_editMode\"\n type=\"button\"\n class=\"edit-confirm-btn\"\n [title]=\"i18n.preferencesConfirmEdit\"\n (click)=\"confirmEditMode()\"\n >\n <mat-icon>check_circle</mat-icon>\n </button>\n <div class=\"table-body-scroll\" #tableBodyScroll>\n <table mat-table matSort [dataSource]=\"tableDataSource\" class=\"jquery-table\"\n [style.min-width]=\"tableMinWidth\"\n [trackBy]=\"trackRowFn\"\n [matSortActive]=\"_matSortActive\" [matSortDirection]=\"_matSortDirection\">\n <ng-container *ngFor=\"let column of visibleColumns\" [matColumnDef]=\"column.key\">\n <th\n mat-header-cell\n *matHeaderCellDef\n [mat-sort-header]=\"(!_editMode && column.sortable !== false) ? column.key : ''\"\n [disabled]=\"_editMode || column.sortable === false\"\n [style.width]=\"getColumnWidth(column.key)\"\n [class.drag-enabled]=\"isColumnDragDropEnabled || _editMode\"\n [class.drop-target]=\"isHeaderDropTarget(column.key)\"\n [class.edit-mode-header]=\"_editMode\"\n (dragover)=\"onHeaderDragOver(column.key, $event)\"\n (drop)=\"onHeaderDrop(column.key, $event)\"\n >\n <div class=\"head-cell\">\n <mat-icon *ngIf=\"column.icon\" class=\"head-icon\" fontSet=\"material-icons-outlined\">{{ column.icon }}</mat-icon>\n <span class=\"head-text\" [title]=\"column.header\">{{ column.header }}</span>\n <span\n *ngIf=\"isColumnDragDropEnabled || _editMode\"\n class=\"drag-handle\"\n draggable=\"true\"\n (dragstart)=\"onHeaderDragStart(column.key, $event)\"\n (dragend)=\"onHeaderDragEnd()\"\n (click)=\"$event.stopPropagation()\"\n >\n <mat-icon>drag_indicator</mat-icon>\n </span>\n </div>\n <!-- Poign\u00E9e de resize (visible uniquement en mode \u00E9dition) -->\n <span\n *ngIf=\"_editMode\"\n class=\"col-resize-handle\"\n draggable=\"false\"\n (mousedown)=\"onResizeStart(column.key, $event)\"\n ></span>\n </th>\n <td mat-cell *matCellDef=\"let row\"\n [style.width]=\"getColumnWidth(column.key)\"\n [class.edit-mode-cell]=\"_editMode\">\n <ng-container *ngIf=\"!_editMode; else editModeCellTpl\">\n <ng-container *ngIf=\"getCellTemplate(column.key) as customTpl; else defaultCellTpl\">\n <ng-container *ngTemplateOutlet=\"customTpl; context: { $implicit: row[_ROW_RAW_KEY], index: row[_ROW_INDEX_KEY] }\"></ng-container>\n </ng-container>\n <ng-template #defaultCellTpl>\n <ng-container [ngSwitch]=\"column.lazy ? getLazyRenderType(row[column.key]) : 'value'\">\n <span *ngSwitchCase=\"'loading'\" class=\"lazy-skeleton\"></span>\n <button *ngSwitchCase=\"'error'\" type=\"button\" class=\"lazy-error-btn\" (click)=\"retryLazyColumn(column)\" [title]=\"i18n.lazyErrorTitle\">\n <span class=\"lazy-error-icon\">\u26A0</span>\n <span class=\"lazy-error-label\">{{ i18n.lazyRetry }}</span>\n </button>\n <span *ngSwitchDefault class=\"cell-text\" [class.cell-empty]=\"isCellEmpty(row[column.key])\" [title]=\"asTitle(row[column.key])\">{{ isCellEmpty(row[column.key]) ? i18n.cellEmpty : row[column.key] }}</span>\n </ng-container>\n </ng-template>\n </ng-container>\n <ng-template #editModeCellTpl>\n <span class=\"cell-text cell-edit-mode\" [title]=\"asTitle(row[column.key])\">{{ row[column.key] }}</span>\n </ng-template>\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"renderedColumns; sticky: true\"></tr>\n <tr\n mat-row\n *matRowDef=\"let row; columns: renderedColumns; when: !isGroupHeader\"\n [attr.tabindex]=\"hasRowClickHandler && !_editMode ? 0 : null\"\n [ngClass]=\"getRowClass(row)\"\n [class.no-pointer]=\"!hasRowClickHandler || _editMode\"\n [class.edit-mode-row]=\"_editMode\"\n (click)=\"!_editMode && onRowClick(row, $event)\"\n (keydown.enter)=\"!_editMode && onRowClick(row, null)\"\n (keydown.space)=\"!_editMode && onRowClick(row, null)\"\n ></tr>\n <ng-container [matColumnDef]=\"_GROUP_ROW_COLUMN\">\n <td mat-cell *matCellDef=\"let row\" [attr.colspan]=\"renderedColumns.length\" class=\"group-header-cell\">\n <div class=\"group-header-label\">\n <span class=\"group-chevron\" [class.collapsed]=\"isGroupCollapsed(row[_ROW_GROUP_KEY])\" *ngIf=\"isNormalGroupRow(row)\">\u203A</span>\n <span *ngIf=\"isNormalGroupRow(row)\" class=\"group-key-label\">{{ row[_ROW_GROUP_KEY] }}</span>\n <span *ngIf=\"isGroupRowLoading(row)\" class=\"lazy-skeleton group-lazy-key-skeleton\"></span>\n <span *ngIf=\"isGroupRowTooMany(row)\" class=\"group-too-many-label\">{{ i18n.groupTooMany(row[_ROW_GROUP_COUNT]) }}</span>\n <span class=\"group-count-badge\">{{ row[_ROW_GROUP_COUNT] }}</span>\n <div class=\"group-pagination\" *ngIf=\"!isGroupCollapsed(row[_ROW_GROUP_KEY]) && row[_ROW_GROUP_PAGE_COUNT] > 1\" (click)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"group-page-btn\" [disabled]=\"row[_ROW_GROUP_PAGE] === 0\" (click)=\"groupPageBack(row[_ROW_GROUP_KEY])\">\u2039</button>\n <span class=\"group-page-info\">{{ row[_ROW_GROUP_PAGE] + 1 }} / {{ row[_ROW_GROUP_PAGE_COUNT] }}</span>\n <button type=\"button\" class=\"group-page-btn\" [disabled]=\"row[_ROW_GROUP_PAGE] >= row[_ROW_GROUP_PAGE_COUNT] - 1\" (click)=\"groupPageForward(row[_ROW_GROUP_KEY], row[_ROW_GROUP_COUNT])\">\u203A</button>\n </div>\n </div>\n </td>\n </ng-container>\n <tr mat-row *matRowDef=\"let row; columns: [_GROUP_ROW_COLUMN]; when: isGroupHeader\"\n class=\"group-header-row\"\n [class.group-header-loading]=\"isGroupRowLoading(row)\"\n [class.group-header-too-many]=\"isGroupRowTooMany(row)\"\n [attr.data-group-key]=\"row[_ROW_GROUP_KEY]\"\n (click)=\"onGroupHeaderClick(row)\"></tr>\n <tr class=\"mat-mdc-row no-data-row\" *matNoDataRow>\n <td class=\"mat-mdc-cell no-data-cell\" [attr.colspan]=\"renderedColumns.length\"></td>\n </tr>\n </table>\n </div><!-- /table-body-scroll -->\n <div class=\"table-body-overlay\" *ngIf=\"effectiveIsLoading || isEmpty\">\n <span class=\"table-body-overlay-text\">{{ effectiveIsLoading ? loadingStateLabel : emptyStateLabel }}</span>\n </div>\n </div><!-- /table-body-wrapper -->\n\n <div class=\"bottom-bar\" *ngIf=\"showPagination\">\n <mat-paginator\n [length]=\"totalRows\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"pageSizeOptions\"\n ></mat-paginator>\n </div>\n\n <div class=\"group-bottom-bar\" *ngIf=\"activeGroupByKey\">\n <div class=\"group-bottom-bar-container\">\n <div class=\"group-bottom-bar-left\">\n <button type=\"button\" class=\"group-reset-btn\" (click)=\"onGroupByChange(null)\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" class=\"group-reset-icon\">\n <path d=\"M3 8a5 5 0 1 0 5-5 5 5 0 0 0-3.54 1.46L3 3\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M3 3v2.5H5.5\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n {{ i18n.groupRemove }}\n </button>\n <div class=\"group-bar-separator\"></div>\n <div class=\"group-sort-controls\">\n <span class=\"group-sort-label\">{{ i18n.groupSortLabel }}</span>\n <button type=\"button\" class=\"group-sort-btn\" [class.active]=\"groupSortOrder === 'asc'\" (click)=\"onGroupSortToggle('asc')\" [title]=\"i18n.groupSortAsc\">\n <mat-icon>arrow_upward</mat-icon>\n </button>\n <button type=\"button\" class=\"group-sort-btn\" [class.active]=\"groupSortOrder === 'desc'\" (click)=\"onGroupSortToggle('desc')\" [title]=\"i18n.groupSortDesc\">\n <mat-icon>arrow_downward</mat-icon>\n </button>\n </div>\n </div>\n <div class=\"mat-mdc-paginator-page-size\">\n <div class=\"mat-mdc-paginator-page-size-label\">{{ i18n.groupRowsPerGroup }}</div>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"mat-mdc-paginator-page-size-select\">\n <mat-select\n [(ngModel)]=\"groupPageSize\"\n (ngModelChange)=\"onGroupPageSizeChange($event)\"\n panelClass=\"mat-mdc-paginator-select-page-size group-page-size-panel\"\n [panelWidth]=\"'auto'\"\n >\n <mat-select-trigger>{{ groupPageSize === 0 ? i18n.groupShowAll : groupPageSize }}</mat-select-trigger>\n <mat-option *ngFor=\"let opt of pageSizeOptionsGroupBy\" [value]=\"opt\">{{ opt }}</mat-option>\n <mat-option [value]=\"0\">{{ i18n.groupShowAll }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </div>\n </div>\n </div>\n</section>\n", styles: [":host{display:block;max-height:100%;min-height:inherit;--jqt-title-color: #1f2937;--jqt-text-color: #111827;--jqt-text-secondary: #6b7280;--jqt-text-muted: #9ca3af;--jqt-surface-color: #ffffff;--jqt-surface-alt-color: #f9fafb;--jqt-row-hover-bg: #f3f4f6;--jqt-border-color: #e5e7eb;--jqt-group-bg: #eef2f7;--jqt-group-bg-hover: #e4ecf4;--jqt-group-accent: #94a3b8;--jqt-group-border: #d1d9e4;--jqt-primary-color: #1d4ed8;--jqt-danger-color: #dc2626;--jqt-danger-hover-bg: #fef2f2;--jqt-danger-border: #fca5a5;--jqt-header-bg: #f5f5f5;--jqt-header-text-color: #374151;--jqt-cell-bg: #ffffff;--jqt-footer-bg: #f5f5f5;--jqt-footer-text-color: #374151;--jqt-footer-icon-color: #374151}.jquery-table-wrapper{display:flex;flex-direction:column;width:100%;height:100%;min-height:inherit;padding:2px 2px 0;box-sizing:border-box}.table-header{display:flex;align-items:center;justify-content:flex-start;gap:8px;margin-bottom:12px}.table-header h3{margin:0;font-size:20px;font-weight:600;color:var(--jqt-title-color)}.table-layout{display:grid;grid-template-columns:minmax(155px,190px) minmax(0,1fr);gap:12px;align-items:stretch;min-height:inherit;height:100%}.table-layout.no-slice{grid-template-columns:minmax(0,1fr)}.table-layout.slice-collapsed{grid-template-columns:40px minmax(0,1fr)}.table-shell{display:flex;flex-direction:column;flex:1 1 0;min-height:0;min-width:0;width:100%;max-width:100%}.table-body-wrapper{position:relative;flex:1 1 0;min-height:0;display:flex;flex-direction:column}.table-body-scroll{flex:1 1 0;overflow-x:auto;overflow-y:auto;min-height:0;border-top:1px solid var(--jqt-border-color);border-right:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);border-radius:8px 8px 0 0;scrollbar-width:thin;scrollbar-color:var(--jqt-border-color) transparent}.table-body-scroll::-webkit-scrollbar{width:6px}.table-body-scroll::-webkit-scrollbar-thumb{background:var(--jqt-border-color);border-radius:4px}.table-body-overlay{position:absolute;top:var(--actual-header-height, 56px);left:0;right:0;bottom:0;z-index:3;display:flex;align-items:center;justify-content:center;background:var(--jqt-surface-color);border-right:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);pointer-events:none}.table-body-overlay-text{font-size:14px;color:var(--jqt-text-secondary)}.table-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 0 8px}.table-toolbar-left{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:6px}.table-toolbar-right{flex:0 0 auto;display:flex;align-items:center;gap:6px}.search-input-wrapper{position:relative;display:inline-flex;align-items:center;flex:1 1 auto;min-width:0;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 8px;box-sizing:border-box;gap:6px}.search-input-wrapper:focus-within{border-color:var(--jqt-text-muted)}.search-input-wrapper .search-icon{flex-shrink:0;width:14px;height:14px;color:var(--jqt-text-muted)}.search-input{flex:1;border:none;outline:none;font-size:13px;color:var(--jqt-text-color);background:transparent;padding:7px 0;min-width:0}.search-input::placeholder{color:var(--jqt-text-muted)}.search-count{flex-shrink:0;font-size:12px;font-weight:600;color:var(--jqt-text-secondary);background:var(--jqt-row-hover-bg);border-radius:10px;padding:1px 7px;min-width:20px;text-align:center;line-height:18px}.search-clear-btn{background:none;border:none;cursor:pointer;color:var(--jqt-text-muted);font-size:16px;padding:0;line-height:1;flex-shrink:0}.search-clear-btn:hover{color:var(--jqt-text-color)}.slice-expand-btn{flex-shrink:0;width:34px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--jqt-text-color);padding:0;line-height:1}.slice-expand-btn:hover{border-color:var(--jqt-text-muted)}.slice-expand-mat-icon{font-size:18px;width:18px;height:18px;line-height:1}.view-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.view-btn:hover{border-color:var(--jqt-text-muted)}.view-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.export-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.export-btn:hover{border-color:var(--jqt-text-muted)}.export-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.view-menu-export-divider{margin:4px 0!important}.view-menu-export-item{display:flex;align-items:center;gap:6px;width:100%;padding:0 16px;height:40px;border:none;background:transparent;cursor:pointer;font-size:13px;font-weight:500;color:var(--jqt-text-color);text-align:left;box-sizing:border-box}.view-menu-export-item:hover{background:#0000000a}.view-menu-export-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.edit-confirm-btn{position:absolute;top:10px;right:10px;z-index:200;display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:50%;background:#16a34a;cursor:pointer;box-shadow:0 2px 8px #00000040;transition:background .15s}.edit-confirm-btn mat-icon{color:#fff;font-size:22px;width:22px;height:22px}.edit-confirm-btn:hover{background:#15803d}.preferences-message{display:flex;align-items:center;gap:8px;margin:0 0 8px;padding:8px 12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;font-size:12px;color:#15803d}.preferences-message .preferences-message-icon{font-size:16px;width:16px;height:16px}:host ::ng-deep .edit-mode-header{background:#3b82f60a!important;cursor:grab!important;-webkit-user-select:none;user-select:none;overflow:visible!important}.col-resize-handle{position:absolute;top:0;right:0;width:6px;height:100%;cursor:col-resize!important;background:#3b82f640;z-index:20}.col-resize-handle:hover{background:#3b82f6b3}.col-resize-handle:active{background:#3b82f6e6}:host ::ng-deep th.mat-mdc-header-cell{position:relative;overflow:visible!important}:host ::ng-deep .edit-mode-row{pointer-events:none;-webkit-user-select:none;user-select:none;opacity:.8}.cell-edit-mode{pointer-events:none;-webkit-user-select:none;user-select:none;color:var(--jqt-text-muted)}.group-header-row{cursor:pointer}:host ::ng-deep .jquery-table{border-spacing:0}:host ::ng-deep .group-header-row td{padding:0!important;background:var(--jqt-group-bg);border-bottom:1px solid var(--jqt-group-border);border-top:1px solid var(--jqt-group-border);border-left:3px solid var(--jqt-group-accent);position:sticky;top:var(--actual-header-height, 56px);z-index:2;box-shadow:-4px 0 0 0 var(--jqt-group-bg)}:host ::ng-deep .group-header-row:hover td{background:var(--jqt-group-bg-hover);box-shadow:-4px 0 0 0 var(--jqt-group-bg-hover)}:host ::ng-deep .group-header-loading td,:host ::ng-deep .group-header-loading:hover td,:host ::ng-deep .group-header-too-many td,:host ::ng-deep .group-header-too-many:hover td{cursor:default;background:var(--jqt-surface-alt-color)}.group-too-many-label{font-size:12px;font-weight:500;color:var(--jqt-warn-color, #b45309);font-style:italic}.group-header-cell{padding:0!important}.group-header-label{display:flex;align-items:center;gap:6px;padding:6px 16px;font-size:12px;font-weight:700;color:var(--jqt-text-color);text-transform:uppercase;letter-spacing:.04em}.group-key-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-count-badge{flex-shrink:0;font-size:11px;line-height:1;font-weight:600;color:#fff;background:var(--jqt-text-secondary, #6b7280);border-radius:10px;padding:3px 7px;min-width:20px;text-align:center;text-transform:none;letter-spacing:0}.group-pagination{display:inline-flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.group-page-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:1px solid var(--jqt-border-color);border-radius:5px;background:var(--jqt-surface-color);cursor:pointer;font-size:14px;color:var(--jqt-text-color);line-height:1;padding:0 0 3px;text-transform:none;letter-spacing:0}.group-page-btn:hover:not(:disabled){border-color:var(--jqt-text-muted);background:var(--jqt-surface-alt-color)}.group-page-btn:disabled{opacity:.35;cursor:default}.group-page-info{font-size:11px;color:var(--jqt-text-secondary);min-width:36px;text-align:center;font-weight:500;text-transform:none;letter-spacing:0}.group-chevron{flex-shrink:0;font-size:14px;font-weight:700;padding-bottom:3px;transition:transform .15s ease;transform:rotate(90deg)}.group-chevron.collapsed{transform:rotate(0)}.no-data-row{cursor:default!important}.no-data-cell{padding:0!important;height:0!important;border:none!important}.jquery-table{width:100%;table-layout:fixed}:host ::ng-deep .mat-mdc-table{width:100%;background:transparent}:host ::ng-deep .mat-mdc-header-cell,:host ::ng-deep .mat-mdc-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host ::ng-deep .mat-mdc-header-row{background:var(--jqt-header-bg)}:host ::ng-deep .mat-mdc-header-cell{background:var(--jqt-header-bg);color:var(--jqt-header-text-color)}:host ::ng-deep .mat-mdc-row,:host ::ng-deep .mat-mdc-cell{background:var(--jqt-cell-bg)}:host ::ng-deep .mat-mdc-row{cursor:pointer;border-bottom-color:var(--jqt-border-color)}:host ::ng-deep .mat-mdc-row.no-pointer{cursor:auto}:host ::ng-deep .mat-mdc-row.no-data-row{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drag-enabled{-webkit-user-select:none;user-select:none}:host ::ng-deep .mat-mdc-header-cell.drag-enabled:active{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drop-target{background:var(--jqt-row-hover-bg)}.head-cell{display:flex;align-items:center;justify-content:space-between;gap:6px;min-width:0}.drag-handle{display:inline-flex;align-items:center;cursor:grab;color:var(--jqt-border-color);flex-shrink:0;padding:0 2px;border-radius:4px;line-height:1}.drag-handle:active{cursor:grabbing}.drag-handle:hover{color:var(--jqt-text-muted)}.drag-handle mat-icon{font-size:16px;width:16px;height:16px;line-height:1}.head-icon{font-size:14px;width:14px;height:14px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.head-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase;font-size:11px;font-weight:600;letter-spacing:.05em}.head-actions{display:inline-flex;align-items:center;gap:6px}.cell-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cell-empty{color:var(--jqt-empty-cell-color, #9ca3af);font-style:italic}.bottom-bar{display:flex;align-items:center;flex:0 0 auto;background:var(--jqt-footer-bg);border-top:1px solid var(--jqt-border-color);border-right:1px solid var(--jqt-border-color);border-bottom:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);border-radius:0 0 8px 8px}:host ::ng-deep .bottom-bar mat-paginator{flex:1 1 auto;background:transparent;color:var(--jqt-footer-text-color)}:host ::ng-deep .bottom-bar .mat-mdc-paginator-outer-container{display:flex}:host ::ng-deep .bottom-bar .mat-mdc-paginator-container{display:flex;align-items:center;flex-wrap:nowrap;min-height:0;height:var(--actual-header-height, 56px);padding:0 8px;box-sizing:border-box}:host ::ng-deep .bottom-bar .mat-mdc-paginator-range-label,:host ::ng-deep .bottom-bar .mat-mdc-paginator-page-size-label{font-size:12px;color:var(--jqt-footer-text-color)}:host ::ng-deep .bottom-bar .mat-mdc-icon-button{color:var(--jqt-footer-icon-color);width:28px;height:28px;padding:0;line-height:1;display:inline-flex;align-items:center;justify-content:center;--mdc-icon-button-state-layer-size: 28px}:host ::ng-deep .bottom-bar .mat-mdc-icon-button .mat-mdc-button-persistent-ripple,:host ::ng-deep .bottom-bar .mat-mdc-icon-button .mdc-icon-button__state-layer{width:28px;height:28px;top:50%;left:50%;transform:translate(-50%,-50%)}:host ::ng-deep .bottom-bar .mat-mdc-icon-button .mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}:host ::ng-deep .bottom-bar .mat-mdc-icon-button:disabled{opacity:.35}:host ::ng-deep .bottom-bar .mat-mdc-paginator-page-size-select{width:72px;font-size:12px}:host ::ng-deep .bottom-bar .mat-mdc-text-field-wrapper{padding:0 8px}:host ::ng-deep .bottom-bar .mdc-text-field--outlined{height:28px}:host ::ng-deep .bottom-bar .mat-mdc-form-field-infix{padding-top:0;padding-bottom:0;min-height:0;height:28px;display:flex;align-items:center}:host ::ng-deep .bottom-bar .mat-mdc-select-value-text{font-size:12px;color:var(--jqt-text-color)}:host ::ng-deep .bottom-bar .mdc-notched-outline__leading,:host ::ng-deep .bottom-bar .mdc-notched-outline__notch,:host ::ng-deep .bottom-bar .mdc-notched-outline__trailing{border-color:var(--jqt-border-color)}.group-bottom-bar{display:flex;align-items:center;flex:0 0 auto;background:var(--jqt-footer-bg);border-top:1px solid var(--jqt-border-color);border-right:1px solid var(--jqt-border-color);border-bottom:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);border-radius:0 0 8px 8px}.group-bottom-bar-container{display:flex;align-items:center;justify-content:space-between;flex-wrap:nowrap;height:var(--actual-header-height, 56px);padding:0 16px;gap:16px;width:100%;box-sizing:border-box}.group-bar-separator{width:1px;height:20px;background:#9ca3af;flex-shrink:0;margin:0 4px}.group-bottom-bar-left{display:flex;align-items:center;gap:8px;flex-shrink:0}.group-sort-controls{display:flex;align-items:center;gap:2px}.group-sort-label{font-size:12px;color:var(--jqt-footer-text-color);white-space:nowrap;margin-right:2px}.group-sort-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--jqt-footer-icon-color);cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}.group-sort-btn mat-icon{font-size:16px;width:16px;height:16px;line-height:16px}.group-sort-btn:hover{background:#0000000a}.group-sort-btn.active{color:var(--jqt-primary-color, #1976d2);border-color:var(--jqt-primary-color, #1976d2);background:#1976d214}:host ::ng-deep .group-bottom-bar .mat-mdc-paginator-page-size{display:flex;align-items:center;gap:8px}:host ::ng-deep .group-bottom-bar .mat-mdc-paginator-page-size-label{font-size:12px;color:var(--jqt-footer-text-color);white-space:nowrap}:host ::ng-deep .group-bottom-bar .mat-mdc-paginator-page-size-select{width:72px;font-size:12px}:host ::ng-deep .group-bottom-bar .mat-mdc-text-field-wrapper{padding:0 8px}:host ::ng-deep .group-bottom-bar .mdc-text-field--outlined{height:28px}:host ::ng-deep .group-bottom-bar .mat-mdc-form-field-infix{padding-top:0;padding-bottom:0;min-height:0;height:28px;display:flex;align-items:center}:host ::ng-deep .group-bottom-bar .mat-mdc-select-value-text{font-size:12px;color:var(--jqt-text-color)}:host ::ng-deep .group-bottom-bar .mdc-notched-outline__leading,:host ::ng-deep .group-bottom-bar .mdc-notched-outline__notch,:host ::ng-deep .group-bottom-bar .mdc-notched-outline__trailing{border-color:var(--jqt-border-color)}:host ::ng-deep .group-bottom-bar .mat-mdc-select-arrow{color:var(--jqt-footer-text-color)}.group-reset-btn{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);color:var(--jqt-text-secondary);font-size:12px;font-weight:500;font-family:inherit;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease;white-space:nowrap;flex-shrink:0}.group-reset-btn:hover{background:var(--jqt-danger-hover-bg);color:var(--jqt-danger-color);border-color:var(--jqt-danger-border)}.group-reset-icon{width:14px;height:14px;flex-shrink:0}::ng-deep .mat-mdc-paginator-select-page-size .mat-pseudo-checkbox{display:none!important}::ng-deep .group-page-size-panel{min-width:90px!important}::ng-deep .group-page-size-panel .mat-mdc-option{justify-content:center;min-height:40px}::ng-deep .group-page-size-panel .mdc-list-item__primary-text{text-align:center;width:100%}@media (max-width: 960px){.table-layout{grid-template-columns:1fr}}::ng-deep .view-menu-panel .mat-mdc-menu-panel{min-width:220px!important;overflow:visible!important}::ng-deep .view-menu-panel .mat-mdc-menu-content{padding:4px 0!important}::ng-deep .view-submenu-panel .mat-mdc-menu-panel{min-width:200px!important}::ng-deep .view-submenu-panel .mat-mdc-menu-content{padding:0!important}::ng-deep .view-menu-panel .view-menu-sub-trigger.mat-mdc-menu-item{min-height:42px;padding:0 14px;display:flex;align-items:center;box-sizing:border-box}::ng-deep .view-menu-panel .view-menu-sub-trigger .mdc-list-item__primary-text{display:flex;align-items:center;width:100%;gap:0;overflow:hidden}::ng-deep .view-menu-sub-label{font-size:13px;font-weight:500;color:var(--jqt-text-color);flex:0 0 auto;white-space:nowrap;padding-right:8px}::ng-deep .view-menu-sub-value{font-size:12px;color:var(--jqt-text-secondary);flex:1 1 auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:right}::ng-deep .view-submenu{padding:4px 6px;min-width:180px;box-sizing:border-box}::ng-deep .view-submenu .view-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px;border:none;background:transparent;border-radius:6px;font-size:13px;color:var(--jqt-text-color);cursor:pointer;text-align:left;box-sizing:border-box;white-space:nowrap}::ng-deep .view-submenu .view-menu-item:hover{background:var(--jqt-row-hover-bg)}::ng-deep .view-submenu .view-menu-item.active{font-weight:600}::ng-deep .view-submenu .view-menu-item:disabled{opacity:.4;cursor:default}::ng-deep .view-menu-col-icon{flex-shrink:0;font-size:14px!important;width:14px!important;height:14px!important;color:var(--jqt-text-muted)}::ng-deep .view-menu-check{flex-shrink:0;width:16px;text-align:center;font-size:12px;color:var(--jqt-text-color)}::ng-deep .view-menu-separator{border:none;border-top:1px solid var(--jqt-border-color);margin:4px 0}.lazy-skeleton{display:block;height:12px;width:70%;border-radius:4px;background:linear-gradient(90deg,var(--jqt-border-color) 25%,var(--jqt-surface-alt-color) 50%,var(--jqt-border-color) 75%);background-size:200% 100%;animation:lazy-shimmer 1.4s ease-in-out infinite}.group-lazy-key-skeleton{flex:1 1 auto;min-width:80px;height:13px;border-radius:4px}@keyframes lazy-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.lazy-error-btn{display:inline-flex;align-items:center;gap:4px;background:none;border:1px solid #fca5a5;border-radius:4px;padding:2px 6px;cursor:pointer;color:#ef4444;font-size:12px;opacity:.85;transition:opacity .15s}.lazy-error-btn:hover{opacity:1;background:#fef2f2}.lazy-error-icon{font-size:12px}.lazy-error-label{font-size:11px;font-weight:500}::ng-deep .lazy-menu-spinner{display:inline-block;margin-left:auto;font-size:13px;color:var(--jqt-text-secondary);animation:lazy-spin 1s linear infinite}::ng-deep .lazy-menu-error{display:inline-block;margin-left:auto;font-size:13px;color:#ef4444}@keyframes lazy-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i1.NgClass, selector: "[ngClass]", inputs: ["class", "ngClass"] }, { kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i1.NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }, { kind: "directive", type: i1.NgSwitch, selector: "[ngSwitch]", inputs: ["ngSwitch"] }, { kind: "directive", type: i1.NgSwitchCase, selector: "[ngSwitchCase]", inputs: ["ngSwitchCase"] }, { kind: "directive", type: i1.NgSwitchDefault, selector: "[ngSwitchDefault]" }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2$1.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$1.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2$1.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "ngmodule", type: MatTableModule }, { kind: "component", type: i3.MatTable, selector: "mat-table, table[mat-table]", exportAs: ["matTable"] }, { kind: "directive", type: i3.MatHeaderCellDef, selector: "[matHeaderCellDef]" }, { kind: "directive", type: i3.MatHeaderRowDef, selector: "[matHeaderRowDef]", inputs: ["matHeaderRowDef", "matHeaderRowDefSticky"] }, { kind: "directive", type: i3.MatColumnDef, selector: "[matColumnDef]", inputs: ["sticky", "matColumnDef"] }, { kind: "directive", type: i3.MatCellDef, selector: "[matCellDef]" }, { kind: "directive", type: i3.MatRowDef, selector: "[matRowDef]", inputs: ["matRowDefColumns", "matRowDefWhen"] }, { kind: "directive", type: i3.MatHeaderCell, selector: "mat-header-cell, th[mat-header-cell]" }, { kind: "directive", type: i3.MatCell, selector: "mat-cell, td[mat-cell]" }, { kind: "component", type: i3.MatHeaderRow, selector: "mat-header-row, tr[mat-header-row]", exportAs: ["matHeaderRow"] }, { kind: "component", type: i3.MatRow, selector: "mat-row, tr[mat-row]", exportAs: ["matRow"] }, { kind: "directive", type: i3.MatNoDataRow, selector: "ng-template[matNoDataRow]" }, { kind: "ngmodule", type: MatButtonModule }, { kind: "ngmodule", type: MatDividerModule }, { kind: "component", type: i4.MatDivider, selector: "mat-divider", inputs: ["vertical", "inset"] }, { kind: "ngmodule", type: MatPaginatorModule }, { kind: "component", type: i5.MatPaginator, selector: "mat-paginator", inputs: ["disabled"], exportAs: ["matPaginator"] }, { kind: "ngmodule", type: MatSortModule }, { kind: "directive", type: i6.MatSort, selector: "[matSort]", inputs: ["matSortDisabled", "matSortActive", "matSortStart", "matSortDirection", "matSortDisableClear"], outputs: ["matSortChange"], exportAs: ["matSort"] }, { kind: "component", type: i6.MatSortHeader, selector: "[mat-sort-header]", inputs: ["disabled", "mat-sort-header", "arrowPosition", "start", "sortActionDescription", "disableClear"], exportAs: ["matSortHeader"] }, { kind: "ngmodule", type: MatMenuModule }, { kind: "component", type: i7.MatMenu, selector: "mat-menu", exportAs: ["matMenu"] }, { kind: "component", type: i7.MatMenuItem, selector: "[mat-menu-item]", inputs: ["disabled", "disableRipple", "role"], exportAs: ["matMenuItem"] }, { kind: "directive", type: i7.MatMenuTrigger, selector: "[mat-menu-trigger-for], [matMenuTriggerFor]", exportAs: ["matMenuTrigger"] }, { kind: "ngmodule", type: MatIconModule }, { kind: "component", type: i2.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i9.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "component", type: i10.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex", "panelWidth", "hideSingleSelectionIndicator"], exportAs: ["matSelect"] }, { kind: "directive", type: i10.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "component", type: i11.MatOption, selector: "mat-option", exportAs: ["matOption"] }, { kind: "component", type: SlicePanelComponent, selector: "slice-panel", inputs: ["sliceConfigs", "columns", "data", "lazyData", "lazyStatus", "showToggle", "showCounts", "collapsedByDefault", "alwaysShow"], outputs: ["filterChange", "activeKeysChange", "dynamicSliceKeysChange", "collapsedChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
|
|
1561
2518
|
}
|
|
1562
2519
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TableComponent, decorators: [{
|
|
1563
2520
|
type: Component,
|
|
1564
|
-
args: [{ standalone: true, selector: 'jquery-table', imports: [CommonModule, FormsModule, MatTableModule, MatButtonModule, MatPaginatorModule, MatSortModule, MatMenuModule, MatIconModule, MatSelectModule, SlicePanelComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [{ provide: MatPaginatorIntl, useFactory: getFrenchPaginatorIntl }], template: "<section class=\"jquery-table-wrapper\">\n <header class=\"table-header\" *ngIf=\"showTitle\">\n <h3>{{ title }}</h3>\n </header>\n\n <div\n class=\"table-layout\"\n [class.slice-collapsed]=\"showSlicePanel && (slicePanelRef?.isCollapsed ?? false)\"\n [class.no-slice]=\"!showSlicePanel\"\n >\n <slice-panel\n [style.display]=\"showSlicePanel ? null : 'none'\"\n [sliceConfigs]=\"_sliceConfigs\"\n [columns]=\"_sliceColumns\"\n [data]=\"_sliceData\"\n [lazyData]=\"_lazyColumnData\"\n [lazyStatus]=\"_lazyColumnStatus\"\n [showToggle]=\"_sliceShowToggle\"\n (filterChange)=\"onSliceFilterChange($event)\"\n (dynamicSliceKeysChange)=\"onDynamicSliceKeysChange($event)\"\n ></slice-panel>\n\n <div class=\"table-shell\">\n <div class=\"table-toolbar\" *ngIf=\"showToolbar\">\n <div class=\"table-toolbar-left\">\n <div *ngIf=\"showSearchBar\" class=\"search-input-wrapper\">\n <svg class=\"search-icon\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <circle cx=\"8.5\" cy=\"8.5\" r=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M13 13L17 17\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n <input\n class=\"search-input\"\n type=\"text\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\"\n placeholder=\"Rechercher...\"\n autocomplete=\"off\"\n />\n <button *ngIf=\"searchQuery\" type=\"button\" class=\"search-clear-btn\" (click)=\"searchQuery = ''; onSearchChange()\">\u00D7</button>\n <span class=\"search-count\">{{ filteredRowCount }}</span>\n </div>\n </div>\n <div *ngIf=\"showViewButton\" class=\"table-toolbar-right\">\n <button type=\"button\" class=\"view-btn\" [matMenuTriggerFor]=\"viewMenu\" #viewMenuTrigger=\"matMenuTrigger\">\n <mat-icon class=\"view-btn-icon\">settings</mat-icon>\n View\n </button>\n\n <mat-menu #viewMenu=\"matMenu\" [panelClass]=\"'view-menu-panel'\">\n <button *ngIf=\"showFields\" mat-menu-item [matMenuTriggerFor]=\"fieldsSubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">Champs</span>\n <span class=\"view-menu-sub-value\">{{ visibleColumns.length }}/{{ totalColumnCount }}</span>\n </button>\n <button *ngIf=\"showGroupBySection\" mat-menu-item [matMenuTriggerFor]=\"groupBySubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">Group by</span>\n <span class=\"view-menu-sub-value\">{{ activeGroupByLabel }}</span>\n </button>\n <button *ngIf=\"showAddSliceButton\" mat-menu-item [matMenuTriggerFor]=\"sliceBySubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">Slice by</span>\n <span class=\"view-menu-sub-value\">{{ activeSliceByLabel }}</span>\n </button>\n </mat-menu>\n\n <mat-menu #fieldsSubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let column of menuBaseColumns\"\n [disabled]=\"!column.optional && !allowColumnRemoval\"\n (click)=\"onColumnVisibilityChange(column, !isColumnVisible(column.key))\">\n <span class=\"view-menu-check\">{{ isColumnVisible(column.key) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"column.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ column.icon }}</mat-icon>\n {{ colLabel(column) }}\n </button>\n <ng-container *ngIf=\"menuOptionalColumns.length > 0\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let column of menuOptionalColumns\"\n [disabled]=\"(isColumnVisible(column.key) && visibleColumns.length === 1) || getLazyColumnStatus(column.key) === 'loading'\"\n (click)=\"onColumnVisibilityChange(column, !isColumnVisible(column.key))\">\n <span class=\"view-menu-check\">{{ isColumnVisible(column.key) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"column.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ column.icon }}</mat-icon>\n {{ colLabel(column) }}\n <span *ngIf=\"column.lazy && getLazyColumnStatus(column.key) === 'loading'\" class=\"lazy-menu-spinner\">\u21BB</span>\n <span *ngIf=\"column.lazy && getLazyColumnStatus(column.key) === 'error'\" class=\"lazy-menu-error\" title=\"Erreur de chargement\">\u26A0</span>\n </button>\n </ng-container>\n </div>\n </mat-menu>\n\n <mat-menu #groupBySubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let col of groupByColumns\"\n [class.active]=\"activeGroupByKey === col.key\"\n (click)=\"onGroupByChange(activeGroupByKey === col.key ? null : col.key)\">\n <span class=\"view-menu-check\">{{ activeGroupByKey === col.key ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"col.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ col.icon }}</mat-icon>\n {{ colLabel(col) }}\n </button>\n </div>\n </mat-menu>\n\n <mat-menu #sliceBySubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let s of staticSlicesForMenu\"\n (click)=\"toggleStaticSlice(s.key)\">\n <span class=\"view-menu-check\">{{ isStaticSliceVisible(s.key) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"s.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ s.icon }}</mat-icon>\n {{ s.title }}\n </button>\n <ng-container *ngIf=\"allDynamicSliceColumns.length > 0\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let col of allDynamicSliceColumns\"\n [class.active]=\"isDynamicSliceActive(col.key)\"\n (click)=\"toggleDynamicSlice(col)\">\n <span class=\"view-menu-check\">{{ isDynamicSliceActive(col.key) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"col.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ col.icon }}</mat-icon>\n {{ colLabel(col) }}\n </button>\n </ng-container>\n </div>\n </mat-menu>\n </div>\n </div>\n\n <div class=\"table-body-wrapper\">\n <div class=\"table-body-scroll\" #tableBodyScroll>\n <table mat-table matSort [dataSource]=\"tableDataSource\" class=\"jquery-table\"\n [trackBy]=\"trackRowFn\"\n [matSortActive]=\"_matSortActive\" [matSortDirection]=\"_matSortDirection\">\n <ng-container *ngFor=\"let column of visibleColumns\" [matColumnDef]=\"column.key\">\n <th\n mat-header-cell\n *matHeaderCellDef\n [mat-sort-header]=\"column.sortable !== false ? column.key : ''\"\n [disabled]=\"column.sortable === false\"\n [style.width]=\"column.width || null\"\n [style.min-width]=\"column.width || null\"\n [class.drag-enabled]=\"isColumnDragDropEnabled\"\n [class.drop-target]=\"isHeaderDropTarget(column.key)\"\n (dragover)=\"onHeaderDragOver(column.key, $event)\"\n (drop)=\"onHeaderDrop(column.key, $event)\"\n >\n <div class=\"head-cell\">\n <mat-icon *ngIf=\"column.icon\" class=\"head-icon\" fontSet=\"material-icons-outlined\">{{ column.icon }}</mat-icon>\n <span class=\"head-text\" [title]=\"column.header\">{{ column.header }}</span>\n <span\n *ngIf=\"isColumnDragDropEnabled\"\n class=\"drag-handle\"\n draggable=\"true\"\n (dragstart)=\"onHeaderDragStart(column.key, $event)\"\n (dragend)=\"onHeaderDragEnd()\"\n (click)=\"$event.stopPropagation()\"\n >\n <mat-icon>drag_indicator</mat-icon>\n </span>\n </div>\n </th>\n <td mat-cell *matCellDef=\"let row\"\n [style.width]=\"column.width || null\"\n [style.min-width]=\"column.width || null\">\n <ng-container *ngIf=\"getCellTemplate(column.key) as customTpl; else defaultCellTpl\">\n <ng-container *ngTemplateOutlet=\"customTpl; context: { $implicit: row.__raw, index: row.__index }\"></ng-container>\n </ng-container>\n <ng-template #defaultCellTpl>\n <ng-container [ngSwitch]=\"column.lazy ? getLazyRenderType(row[column.key]) : 'value'\">\n <span *ngSwitchCase=\"'loading'\" class=\"lazy-skeleton\"></span>\n <button *ngSwitchCase=\"'error'\" type=\"button\" class=\"lazy-error-btn\" (click)=\"retryLazyColumn(column)\" title=\"Erreur de chargement \u2014 cliquer pour r\u00E9essayer\">\n <span class=\"lazy-error-icon\">\u26A0</span>\n <span class=\"lazy-error-label\">R\u00E9essayer</span>\n </button>\n <span *ngSwitchDefault class=\"cell-text\" [class.cell-empty]=\"isCellEmpty(row[column.key])\" [title]=\"asTitle(row[column.key])\">{{ isCellEmpty(row[column.key]) ? 'N/A' : row[column.key] }}</span>\n </ng-container>\n </ng-template>\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"renderedColumns; sticky: true\"></tr>\n <tr\n mat-row\n *matRowDef=\"let row; columns: renderedColumns; when: !isGroupHeader\"\n [attr.tabindex]=\"hasRowClickHandler ? 0 : null\"\n [ngClass]=\"getRowClass(row)\"\n [class.no-pointer]=\"!hasRowClickHandler\"\n (click)=\"onRowClick(row, $event)\"\n (keydown.enter)=\"onRowClick(row, null)\"\n (keydown.space)=\"onRowClick(row, null)\"\n ></tr>\n <ng-container matColumnDef=\"__group_row\">\n <td mat-cell *matCellDef=\"let row\" [attr.colspan]=\"renderedColumns.length\" class=\"group-header-cell\">\n <div class=\"group-header-label\">\n <span class=\"group-chevron\" [class.collapsed]=\"isGroupCollapsed(row.__groupKey)\" *ngIf=\"row.__groupKey !== '__lazy_loading__' && row.__groupKey !== '__too_many_groups__'\">\u203A</span>\n <span *ngIf=\"row.__groupKey !== '__lazy_loading__' && row.__groupKey !== '__too_many_groups__'\" class=\"group-key-label\">{{ row.__groupKey }}</span>\n <span *ngIf=\"row.__groupKey === '__lazy_loading__'\" class=\"lazy-skeleton group-lazy-key-skeleton\"></span>\n <span *ngIf=\"row.__groupKey === '__too_many_groups__'\" class=\"group-too-many-label\">\u26A0 Trop de groupes ({{ row.__groupCount }}) \u2014 affinez les donn\u00E9es ou choisissez une autre colonne</span>\n <span class=\"group-count-badge\">{{ row.__groupCount }}</span>\n <div class=\"group-pagination\" *ngIf=\"!isGroupCollapsed(row.__groupKey) && row.__groupPageCount > 1\" (click)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"group-page-btn\" [disabled]=\"row.__groupPage === 0\" (click)=\"groupPageBack(row.__groupKey)\">\u2039</button>\n <span class=\"group-page-info\">{{ row.__groupPage + 1 }}\u00A0/\u00A0{{ row.__groupPageCount }}</span>\n <button type=\"button\" class=\"group-page-btn\" [disabled]=\"row.__groupPage >= row.__groupPageCount - 1\" (click)=\"groupPageForward(row.__groupKey, row.__groupCount)\">\u203A</button>\n </div>\n </div>\n </td>\n </ng-container>\n <tr mat-row *matRowDef=\"let row; columns: ['__group_row']; when: isGroupHeader\"\n class=\"group-header-row\"\n [class.group-header-loading]=\"row.__groupKey === '__lazy_loading__'\"\n [class.group-header-too-many]=\"row.__groupKey === '__too_many_groups__'\"\n [attr.data-group-key]=\"row.__groupKey\"\n (click)=\"row.__groupKey !== '__lazy_loading__' && row.__groupKey !== '__too_many_groups__' && toggleGroupCollapse(row.__groupKey)\"></tr>\n <tr class=\"mat-mdc-row no-data-row\" *matNoDataRow>\n <td class=\"mat-mdc-cell no-data-cell\" [attr.colspan]=\"renderedColumns.length\"></td>\n </tr>\n </table>\n </div><!-- /table-body-scroll -->\n <div class=\"table-body-overlay\" *ngIf=\"effectiveIsLoading || isEmpty\">\n <span class=\"table-body-overlay-text\">{{ effectiveIsLoading ? loadingStateLabel : emptyStateLabel }}</span>\n </div>\n </div><!-- /table-body-wrapper -->\n\n <div class=\"bottom-bar\" *ngIf=\"showPagination\">\n <mat-paginator\n [length]=\"totalRows\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"pageSizeOptions\"\n ></mat-paginator>\n </div>\n\n <div class=\"group-bottom-bar\" *ngIf=\"activeGroupByKey\">\n <div class=\"group-bottom-bar-container\">\n <div class=\"group-bottom-bar-left\">\n <button type=\"button\" class=\"group-reset-btn\" (click)=\"onGroupByChange(null)\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" class=\"group-reset-icon\">\n <path d=\"M3 8a5 5 0 1 0 5-5 5 5 0 0 0-3.54 1.46L3 3\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M3 3v2.5H5.5\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n Retirer le groupe\n </button>\n <div class=\"group-bar-separator\"></div>\n <div class=\"group-sort-controls\">\n <span class=\"group-sort-label\">Trier :</span>\n <button type=\"button\" class=\"group-sort-btn\" [class.active]=\"groupSortOrder === 'asc'\" (click)=\"onGroupSortToggle('asc')\" title=\"A \u2192 Z\">\n <mat-icon>arrow_upward</mat-icon>\n </button>\n <button type=\"button\" class=\"group-sort-btn\" [class.active]=\"groupSortOrder === 'desc'\" (click)=\"onGroupSortToggle('desc')\" title=\"Z \u2192 A\">\n <mat-icon>arrow_downward</mat-icon>\n </button>\n </div>\n </div>\n <div class=\"mat-mdc-paginator-page-size\">\n <div class=\"mat-mdc-paginator-page-size-label\">Lignes par groupe :</div>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"mat-mdc-paginator-page-size-select\">\n <mat-select\n [(ngModel)]=\"groupPageSize\"\n (ngModelChange)=\"onGroupPageSizeChange($event)\"\n panelClass=\"mat-mdc-paginator-select-page-size group-page-size-panel\"\n [panelWidth]=\"'auto'\"\n >\n <mat-select-trigger>{{ groupPageSize === 0 ? 'Toutes' : groupPageSize }}</mat-select-trigger>\n <mat-option *ngFor=\"let opt of pageSizeOptionsGroupBy\" [value]=\"opt\">{{ opt }}</mat-option>\n <mat-option [value]=\"0\">Toutes</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </div>\n </div>\n </div>\n</section>\n", styles: [":host{display:block;max-height:100%;min-height:inherit;--jqt-title-color: #1f2937;--jqt-text-color: #111827;--jqt-text-secondary: #6b7280;--jqt-text-muted: #9ca3af;--jqt-surface-color: #ffffff;--jqt-surface-alt-color: #f9fafb;--jqt-row-hover-bg: #f3f4f6;--jqt-border-color: #e5e7eb;--jqt-group-bg: #eef2f7;--jqt-group-bg-hover: #e4ecf4;--jqt-group-accent: #94a3b8;--jqt-group-border: #d1d9e4;--jqt-primary-color: #1d4ed8;--jqt-danger-color: #dc2626;--jqt-danger-hover-bg: #fef2f2;--jqt-danger-border: #fca5a5;--jqt-header-bg: #f5f5f5;--jqt-header-text-color: #374151;--jqt-cell-bg: #ffffff;--jqt-footer-bg: #f5f5f5;--jqt-footer-text-color: #374151;--jqt-footer-icon-color: #374151}.jquery-table-wrapper{display:flex;flex-direction:column;width:100%;height:100%;min-height:inherit;padding:2px 2px 0;box-sizing:border-box}.table-header{display:flex;align-items:center;justify-content:flex-start;gap:8px;margin-bottom:12px}.table-header h3{margin:0;font-size:20px;font-weight:600;color:var(--jqt-title-color)}.table-layout{display:grid;grid-template-columns:minmax(155px,190px) minmax(0,1fr);gap:12px;align-items:stretch;min-height:inherit;height:100%}.table-layout.no-slice{grid-template-columns:minmax(0,1fr)}.table-layout.slice-collapsed{grid-template-columns:40px minmax(0,1fr)}.table-shell{display:flex;flex-direction:column;flex:1 1 0;min-height:0;min-width:0;width:100%;max-width:100%}.table-body-wrapper{position:relative;flex:1 1 0;min-height:0;display:flex;flex-direction:column}.table-body-scroll{flex:1 1 0;overflow-y:auto;min-height:0;border-top:1px solid var(--jqt-border-color);border-right:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);border-radius:8px 8px 0 0;scrollbar-width:thin;scrollbar-color:var(--jqt-border-color) transparent}.table-body-scroll::-webkit-scrollbar{width:6px}.table-body-scroll::-webkit-scrollbar-thumb{background:var(--jqt-border-color);border-radius:4px}.table-body-overlay{position:absolute;top:var(--actual-header-height, 56px);left:0;right:0;bottom:0;z-index:3;display:flex;align-items:center;justify-content:center;background:var(--jqt-surface-color);border-right:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);pointer-events:none}.table-body-overlay-text{font-size:14px;color:var(--jqt-text-secondary)}.table-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 0 8px}.table-toolbar-left{flex:1 1 auto;min-width:0}.table-toolbar-right{flex:0 0 auto}.search-input-wrapper{position:relative;display:inline-flex;align-items:center;width:100%;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 8px;box-sizing:border-box;gap:6px}.search-input-wrapper:focus-within{border-color:var(--jqt-text-muted)}.search-input-wrapper .search-icon{flex-shrink:0;width:14px;height:14px;color:var(--jqt-text-muted)}.search-input{flex:1;border:none;outline:none;font-size:13px;color:var(--jqt-text-color);background:transparent;padding:7px 0;min-width:0}.search-input::placeholder{color:var(--jqt-text-muted)}.search-count{flex-shrink:0;font-size:12px;font-weight:600;color:var(--jqt-text-secondary);background:var(--jqt-row-hover-bg);border-radius:10px;padding:1px 7px;min-width:20px;text-align:center;line-height:18px}.search-clear-btn{background:none;border:none;cursor:pointer;color:var(--jqt-text-muted);font-size:16px;padding:0;line-height:1;flex-shrink:0}.search-clear-btn:hover{color:var(--jqt-text-color)}.view-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.view-btn:hover{border-color:var(--jqt-text-muted)}.view-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.group-header-row{cursor:pointer}:host ::ng-deep .jquery-table{border-spacing:0}:host ::ng-deep .group-header-row td{padding:0!important;background:var(--jqt-group-bg);border-bottom:1px solid var(--jqt-group-border);border-top:1px solid var(--jqt-group-border);border-left:3px solid var(--jqt-group-accent);position:sticky;top:var(--actual-header-height, 56px);z-index:2;box-shadow:-4px 0 0 0 var(--jqt-group-bg)}:host ::ng-deep .group-header-row:hover td{background:var(--jqt-group-bg-hover);box-shadow:-4px 0 0 0 var(--jqt-group-bg-hover)}:host ::ng-deep .group-header-loading td,:host ::ng-deep .group-header-loading:hover td,:host ::ng-deep .group-header-too-many td,:host ::ng-deep .group-header-too-many:hover td{cursor:default;background:var(--jqt-surface-alt-color)}.group-too-many-label{font-size:12px;font-weight:500;color:var(--jqt-warn-color, #b45309);font-style:italic}.group-header-cell{padding:0!important}.group-header-label{display:flex;align-items:center;gap:6px;padding:6px 16px;font-size:12px;font-weight:700;color:var(--jqt-text-color);text-transform:uppercase;letter-spacing:.04em}.group-key-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-count-badge{flex-shrink:0;font-size:11px;line-height:1;font-weight:600;color:#fff;background:var(--jqt-text-secondary, #6b7280);border-radius:10px;padding:3px 7px;min-width:20px;text-align:center;text-transform:none;letter-spacing:0}.group-pagination{display:inline-flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.group-page-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:1px solid var(--jqt-border-color);border-radius:5px;background:var(--jqt-surface-color);cursor:pointer;font-size:14px;color:var(--jqt-text-color);line-height:1;padding:0 0 3px;text-transform:none;letter-spacing:0}.group-page-btn:hover:not(:disabled){border-color:var(--jqt-text-muted);background:var(--jqt-surface-alt-color)}.group-page-btn:disabled{opacity:.35;cursor:default}.group-page-info{font-size:11px;color:var(--jqt-text-secondary);min-width:36px;text-align:center;font-weight:500;text-transform:none;letter-spacing:0}.group-chevron{flex-shrink:0;font-size:14px;font-weight:700;padding-bottom:3px;transition:transform .15s ease;transform:rotate(90deg)}.group-chevron.collapsed{transform:rotate(0)}.no-data-row{cursor:default!important}.no-data-cell{padding:0!important;height:0!important;border:none!important}.jquery-table{width:100%;table-layout:fixed}:host ::ng-deep .mat-mdc-table{width:100%;background:transparent}:host ::ng-deep .mat-mdc-header-cell,:host ::ng-deep .mat-mdc-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host ::ng-deep .mat-mdc-header-row{background:var(--jqt-header-bg)}:host ::ng-deep .mat-mdc-header-cell{background:var(--jqt-header-bg);color:var(--jqt-header-text-color)}:host ::ng-deep .mat-mdc-row,:host ::ng-deep .mat-mdc-cell{background:var(--jqt-cell-bg)}:host ::ng-deep .mat-mdc-row{cursor:pointer;border-bottom-color:var(--jqt-border-color)}:host ::ng-deep .mat-mdc-row.no-pointer{cursor:auto}:host ::ng-deep .mat-mdc-row.no-data-row{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drag-enabled{-webkit-user-select:none;user-select:none}:host ::ng-deep .mat-mdc-header-cell.drag-enabled:active{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drop-target{background:var(--jqt-row-hover-bg)}.head-cell{display:flex;align-items:center;justify-content:space-between;gap:6px;min-width:0}.drag-handle{display:inline-flex;align-items:center;cursor:grab;color:var(--jqt-border-color);flex-shrink:0;padding:0 2px;border-radius:4px;line-height:1}.drag-handle:active{cursor:grabbing}.drag-handle:hover{color:var(--jqt-text-muted)}.drag-handle mat-icon{font-size:16px;width:16px;height:16px;line-height:1}.head-icon{font-size:14px;width:14px;height:14px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.head-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase;font-size:11px;font-weight:600;letter-spacing:.05em}.head-actions{display:inline-flex;align-items:center;gap:6px}.cell-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cell-empty{color:var(--jqt-empty-cell-color, #9ca3af);font-style:italic}.bottom-bar{display:flex;align-items:center;flex:0 0 auto;background:var(--jqt-footer-bg);border-top:1px solid var(--jqt-border-color);border-right:1px solid var(--jqt-border-color);border-bottom:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);border-radius:0 0 8px 8px}:host ::ng-deep .bottom-bar mat-paginator{flex:1 1 auto;background:transparent;color:var(--jqt-footer-text-color)}:host ::ng-deep .bottom-bar .mat-mdc-paginator-outer-container{display:flex}:host ::ng-deep .bottom-bar .mat-mdc-paginator-container{display:flex;align-items:center;flex-wrap:nowrap;min-height:0;height:var(--actual-header-height, 56px);padding:0 8px;box-sizing:border-box}:host ::ng-deep .bottom-bar .mat-mdc-paginator-range-label,:host ::ng-deep .bottom-bar .mat-mdc-paginator-page-size-label{font-size:12px;color:var(--jqt-footer-text-color)}:host ::ng-deep .bottom-bar .mat-mdc-icon-button{color:var(--jqt-footer-icon-color);width:28px;height:28px;padding:0;line-height:1;display:inline-flex;align-items:center;justify-content:center;--mdc-icon-button-state-layer-size: 28px}:host ::ng-deep .bottom-bar .mat-mdc-icon-button .mat-mdc-button-persistent-ripple,:host ::ng-deep .bottom-bar .mat-mdc-icon-button .mdc-icon-button__state-layer{width:28px;height:28px;top:50%;left:50%;transform:translate(-50%,-50%)}:host ::ng-deep .bottom-bar .mat-mdc-icon-button .mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}:host ::ng-deep .bottom-bar .mat-mdc-icon-button:disabled{opacity:.35}:host ::ng-deep .bottom-bar .mat-mdc-paginator-page-size-select{width:72px;font-size:12px}:host ::ng-deep .bottom-bar .mat-mdc-text-field-wrapper{padding:0 8px}:host ::ng-deep .bottom-bar .mdc-text-field--outlined{height:28px}:host ::ng-deep .bottom-bar .mat-mdc-form-field-infix{padding-top:0;padding-bottom:0;min-height:0;height:28px;display:flex;align-items:center}:host ::ng-deep .bottom-bar .mat-mdc-select-value-text{font-size:12px;color:var(--jqt-text-color)}:host ::ng-deep .bottom-bar .mdc-notched-outline__leading,:host ::ng-deep .bottom-bar .mdc-notched-outline__notch,:host ::ng-deep .bottom-bar .mdc-notched-outline__trailing{border-color:var(--jqt-border-color)}.group-bottom-bar{display:flex;align-items:center;flex:0 0 auto;background:var(--jqt-footer-bg);border-top:1px solid var(--jqt-border-color);border-right:1px solid var(--jqt-border-color);border-bottom:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);border-radius:0 0 8px 8px}.group-bottom-bar-container{display:flex;align-items:center;justify-content:space-between;flex-wrap:nowrap;height:var(--actual-header-height, 56px);padding:0 16px;gap:16px;width:100%;box-sizing:border-box}.group-bar-separator{width:1px;height:20px;background:#9ca3af;flex-shrink:0;margin:0 4px}.group-bottom-bar-left{display:flex;align-items:center;gap:8px;flex-shrink:0}.group-sort-controls{display:flex;align-items:center;gap:2px}.group-sort-label{font-size:12px;color:var(--jqt-footer-text-color);white-space:nowrap;margin-right:2px}.group-sort-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--jqt-footer-icon-color);cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}.group-sort-btn mat-icon{font-size:16px;width:16px;height:16px;line-height:16px}.group-sort-btn:hover{background:#0000000a}.group-sort-btn.active{color:var(--jqt-primary-color, #1976d2);border-color:var(--jqt-primary-color, #1976d2);background:#1976d214}:host ::ng-deep .group-bottom-bar .mat-mdc-paginator-page-size{display:flex;align-items:center;gap:8px}:host ::ng-deep .group-bottom-bar .mat-mdc-paginator-page-size-label{font-size:12px;color:var(--jqt-footer-text-color);white-space:nowrap}:host ::ng-deep .group-bottom-bar .mat-mdc-paginator-page-size-select{width:72px;font-size:12px}:host ::ng-deep .group-bottom-bar .mat-mdc-text-field-wrapper{padding:0 8px}:host ::ng-deep .group-bottom-bar .mdc-text-field--outlined{height:28px}:host ::ng-deep .group-bottom-bar .mat-mdc-form-field-infix{padding-top:0;padding-bottom:0;min-height:0;height:28px;display:flex;align-items:center}:host ::ng-deep .group-bottom-bar .mat-mdc-select-value-text{font-size:12px;color:var(--jqt-text-color)}:host ::ng-deep .group-bottom-bar .mdc-notched-outline__leading,:host ::ng-deep .group-bottom-bar .mdc-notched-outline__notch,:host ::ng-deep .group-bottom-bar .mdc-notched-outline__trailing{border-color:var(--jqt-border-color)}:host ::ng-deep .group-bottom-bar .mat-mdc-select-arrow{color:var(--jqt-footer-text-color)}.group-reset-btn{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);color:var(--jqt-text-secondary);font-size:12px;font-weight:500;font-family:inherit;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease;white-space:nowrap;flex-shrink:0}.group-reset-btn:hover{background:var(--jqt-danger-hover-bg);color:var(--jqt-danger-color);border-color:var(--jqt-danger-border)}.group-reset-icon{width:14px;height:14px;flex-shrink:0}::ng-deep .mat-mdc-paginator-select-page-size .mat-pseudo-checkbox{display:none!important}::ng-deep .group-page-size-panel{min-width:90px!important}::ng-deep .group-page-size-panel .mat-mdc-option{justify-content:center;min-height:40px}::ng-deep .group-page-size-panel .mdc-list-item__primary-text{text-align:center;width:100%}@media (max-width: 960px){.table-layout{grid-template-columns:1fr}}::ng-deep .view-menu-panel .mat-mdc-menu-panel{min-width:220px!important;overflow:visible!important}::ng-deep .view-menu-panel .mat-mdc-menu-content{padding:4px 0!important}::ng-deep .view-submenu-panel .mat-mdc-menu-panel{min-width:200px!important}::ng-deep .view-submenu-panel .mat-mdc-menu-content{padding:0!important}::ng-deep .view-menu-panel .view-menu-sub-trigger.mat-mdc-menu-item{min-height:42px;padding:0 14px;display:flex;align-items:center;box-sizing:border-box}::ng-deep .view-menu-panel .view-menu-sub-trigger .mdc-list-item__primary-text{display:flex;align-items:center;width:100%;gap:0;overflow:hidden}::ng-deep .view-menu-sub-label{font-size:13px;font-weight:500;color:var(--jqt-text-color);flex:0 0 auto;white-space:nowrap;padding-right:8px}::ng-deep .view-menu-sub-value{font-size:12px;color:var(--jqt-text-secondary);flex:1 1 auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:right}::ng-deep .view-submenu{padding:4px 6px;min-width:180px;box-sizing:border-box}::ng-deep .view-submenu .view-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px;border:none;background:transparent;border-radius:6px;font-size:13px;color:var(--jqt-text-color);cursor:pointer;text-align:left;box-sizing:border-box;white-space:nowrap}::ng-deep .view-submenu .view-menu-item:hover{background:var(--jqt-row-hover-bg)}::ng-deep .view-submenu .view-menu-item.active{font-weight:600}::ng-deep .view-submenu .view-menu-item:disabled{opacity:.4;cursor:default}::ng-deep .view-menu-col-icon{flex-shrink:0;font-size:14px!important;width:14px!important;height:14px!important;color:var(--jqt-text-muted)}::ng-deep .view-menu-check{flex-shrink:0;width:16px;text-align:center;font-size:12px;color:var(--jqt-text-color)}::ng-deep .view-menu-separator{border:none;border-top:1px solid var(--jqt-border-color);margin:4px 0}.lazy-skeleton{display:block;height:12px;width:70%;border-radius:4px;background:linear-gradient(90deg,var(--jqt-border-color) 25%,var(--jqt-surface-alt-color) 50%,var(--jqt-border-color) 75%);background-size:200% 100%;animation:lazy-shimmer 1.4s ease-in-out infinite}.group-lazy-key-skeleton{flex:1 1 auto;min-width:80px;height:13px;border-radius:4px}@keyframes lazy-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.lazy-error-btn{display:inline-flex;align-items:center;gap:4px;background:none;border:1px solid #fca5a5;border-radius:4px;padding:2px 6px;cursor:pointer;color:#ef4444;font-size:12px;opacity:.85;transition:opacity .15s}.lazy-error-btn:hover{opacity:1;background:#fef2f2}.lazy-error-icon{font-size:12px}.lazy-error-label{font-size:11px;font-weight:500}::ng-deep .lazy-menu-spinner{display:inline-block;margin-left:auto;font-size:13px;color:var(--jqt-text-secondary);animation:lazy-spin 1s linear infinite}::ng-deep .lazy-menu-error{display:inline-block;margin-left:auto;font-size:13px;color:#ef4444}@keyframes lazy-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"] }]
|
|
2521
|
+
args: [{ standalone: true, selector: 'jquery-table', imports: [CommonModule, FormsModule, MatTableModule, MatButtonModule, MatDividerModule, MatPaginatorModule, MatSortModule, MatMenuModule, MatIconModule, MatSelectModule, SlicePanelComponent], changeDetection: ChangeDetectionStrategy.OnPush, providers: [{ provide: MatPaginatorIntl, useFactory: getFrenchPaginatorIntl }], template: "<section class=\"jquery-table-wrapper\">\n <header class=\"table-header\" *ngIf=\"showTitle\">\n <h3>{{ title }}</h3>\n </header>\n\n <div\n class=\"table-layout\"\n [class.no-slice]=\"!_showSlicePanel || _slicePanelCollapsed\"\n >\n <slice-panel\n [style.display]=\"!_showSlicePanel || _slicePanelCollapsed ? 'none' : null\"\n [sliceConfigs]=\"_sliceConfigs\"\n [columns]=\"_sliceColumns\"\n [data]=\"_sliceData\"\n [lazyData]=\"_lazyColumnData\"\n [lazyStatus]=\"_lazyColumnStatus\"\n [showToggle]=\"_sliceShowToggle\"\n [collapsedByDefault]=\"_slicePanelCollapsed\"\n (filterChange)=\"onSliceFilterChange($event)\"\n (dynamicSliceKeysChange)=\"onDynamicSliceKeysChange($event)\"\n (collapsedChange)=\"onSlicePanelCollapsedChange($event)\"\n ></slice-panel>\n\n <div class=\"table-shell\">\n <div class=\"table-toolbar\" *ngIf=\"showToolbar\">\n <div class=\"table-toolbar-left\">\n <button\n *ngIf=\"showSliceExpandBtn\"\n type=\"button\"\n class=\"slice-expand-btn\"\n [title]=\"i18n.expandFilterTitle\"\n (click)=\"expandSlicePanel()\"\n ><mat-icon class=\"slice-expand-mat-icon\">menu</mat-icon></button>\n <div *ngIf=\"showSearchBar\" class=\"search-input-wrapper\">\n <svg class=\"search-icon\" viewBox=\"0 0 20 20\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\">\n <circle cx=\"8.5\" cy=\"8.5\" r=\"5.5\" stroke=\"currentColor\" stroke-width=\"1.5\"/>\n <path d=\"M13 13L17 17\" stroke=\"currentColor\" stroke-width=\"1.5\" stroke-linecap=\"round\"/>\n </svg>\n <input\n class=\"search-input\"\n type=\"text\"\n [(ngModel)]=\"searchQuery\"\n (ngModelChange)=\"onSearchChange()\"\n [placeholder]=\"i18n.searchPlaceholder\"\n autocomplete=\"off\"\n />\n <button *ngIf=\"searchQuery\" type=\"button\" class=\"search-clear-btn\" (click)=\"searchQuery = ''; onSearchChange()\">\u00D7</button>\n <span class=\"search-count\">{{ filteredRowCount }}</span>\n </div>\n </div>\n <div *ngIf=\"showViewButton\" class=\"table-toolbar-right\">\n <button *ngIf=\"showViewButton\" type=\"button\" class=\"view-btn\" [matMenuTriggerFor]=\"viewMenu\" #viewMenuTrigger=\"matMenuTrigger\" [attr.aria-label]=\"'View'\">\n <mat-icon class=\"view-btn-icon\">settings</mat-icon>\n </button>\n\n <mat-menu #viewMenu=\"matMenu\" [panelClass]=\"'view-menu-panel'\">\n <button *ngIf=\"showFields\" mat-menu-item [matMenuTriggerFor]=\"fieldsSubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">{{ i18n.viewMenuFields }}</span>\n <span class=\"view-menu-sub-value\">{{ visibleColumns.length }}/{{ totalColumnCount }}</span>\n </button>\n <button *ngIf=\"showGroupBySection\" mat-menu-item [matMenuTriggerFor]=\"groupBySubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">{{ i18n.viewMenuGroupBy }}</span>\n <span class=\"view-menu-sub-value\">{{ activeGroupByLabel }}</span>\n </button>\n <button *ngIf=\"showAddSliceButton\" mat-menu-item [matMenuTriggerFor]=\"sliceBySubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">{{ i18n.viewMenuSliceBy }}</span>\n <span class=\"view-menu-sub-value\">{{ activeSliceByLabel }}</span>\n </button>\n <mat-divider *ngIf=\"showExportButton\" class=\"view-menu-export-divider\"></mat-divider>\n <button *ngIf=\"showExportButton\" mat-menu-item type=\"button\" class=\"view-menu-export-item\"\n (click)=\"onExport(); viewMenuTrigger.closeMenu()\">\n <mat-icon class=\"view-menu-export-icon\">download</mat-icon>\n <span class=\"view-menu-sub-label\">{{ i18n.exportButtonLabel }}</span>\n </button>\n <mat-divider *ngIf=\"showPreferencesMenu\" class=\"view-menu-export-divider\"></mat-divider>\n <button *ngIf=\"showPreferencesMenu\" mat-menu-item [matMenuTriggerFor]=\"preferencesSubMenu\" class=\"view-menu-sub-trigger\">\n <span class=\"view-menu-sub-label\">{{ i18n.preferencesMenuLabel }}</span>\n </button>\n </mat-menu>\n\n <mat-menu #preferencesSubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" (mousedown)=\"enterEditMode(); viewMenuTrigger.closeMenu()\">\n <mat-icon class=\"view-menu-col-icon\">edit</mat-icon>\n {{ i18n.preferencesEdit }}\n </button>\n <button type=\"button\" class=\"view-menu-item\" (mousedown)=\"savePreferences(); viewMenuTrigger.closeMenu()\">\n <mat-icon class=\"view-menu-col-icon\">save</mat-icon>\n {{ i18n.preferencesSave }}\n </button>\n <button type=\"button\" class=\"view-menu-item\" [disabled]=\"!hasSavedPreferences\" (mousedown)=\"clearPreferences(); viewMenuTrigger.closeMenu()\">\n <mat-icon class=\"view-menu-col-icon\">delete_outline</mat-icon>\n {{ i18n.preferencesClear }}\n </button>\n </div>\n </mat-menu>\n\n <mat-menu #fieldsSubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let column of menuBaseColumns\"\n [disabled]=\"!column.optional && !allowColumnRemoval\"\n (mousedown)=\"onColumnVisibilityChange(column, !isColumnVisible(column.key))\">\n <span class=\"view-menu-check\">{{ isColumnVisible(column.key) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"column.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ column.icon }}</mat-icon>\n {{ colLabel(column) }}\n </button>\n <ng-container *ngIf=\"menuOptionalColumns.length > 0\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let column of menuOptionalColumns\"\n [disabled]=\"(isColumnVisible(column.key) && visibleColumns.length === 1) || getLazyColumnStatus(column.key) === 'loading'\"\n (mousedown)=\"onColumnVisibilityChange(column, !isColumnVisible(column.key))\">\n <span class=\"view-menu-check\">{{ isColumnVisible(column.key) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"column.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ column.icon }}</mat-icon>\n {{ colLabel(column) }}\n <span *ngIf=\"column.lazy && getLazyColumnStatus(column.key) === 'loading'\" class=\"lazy-menu-spinner\">\u21BB</span>\n <span *ngIf=\"column.lazy && getLazyColumnStatus(column.key) === 'error'\" class=\"lazy-menu-error\" [title]=\"i18n.lazyErrorMenuTitle\">\u26A0</span>\n </button>\n </ng-container>\n </div>\n </mat-menu>\n\n <mat-menu #groupBySubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let col of groupByColumns\"\n [class.active]=\"activeGroupByKey === col.key\"\n (mousedown)=\"onGroupByChange(activeGroupByKey === col.key ? null : col.key)\">\n <span class=\"view-menu-check\">{{ activeGroupByKey === col.key ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"col.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ col.icon }}</mat-icon>\n {{ colLabel(col) }}\n </button>\n </div>\n </mat-menu>\n\n <mat-menu #sliceBySubMenu=\"matMenu\" [panelClass]=\"'view-submenu-panel'\">\n <div class=\"view-submenu\" (mousedown)=\"$event.stopPropagation()\" (click)=\"$event.stopPropagation()\" (keydown)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"view-menu-item\" *ngFor=\"let item of sliceByMenuItems\"\n [class.active]=\"isSliceMenuItemActive(item)\"\n (mousedown)=\"onSliceMenuItemClick(item)\">\n <span class=\"view-menu-check\">{{ isSliceMenuItemActive(item) ? '\u2713' : '' }}</span>\n <mat-icon *ngIf=\"item.icon\" class=\"view-menu-col-icon\" fontSet=\"material-icons-outlined\">{{ item.icon }}</mat-icon>\n {{ item.title }}\n </button>\n </div>\n </mat-menu>\n </div>\n </div>\n\n <div class=\"table-body-wrapper\">\n\n <!-- Banni\u00E8re message pr\u00E9f\u00E9rences -->\n <div *ngIf=\"_preferencesMessage\" class=\"preferences-message\">\n <mat-icon class=\"preferences-message-icon\">check_circle</mat-icon>\n <span>{{ _preferencesMessage }}</span>\n </div>\n\n <!-- Bouton de confirmation du mode \u00E9dition -->\n <button\n *ngIf=\"_editMode\"\n type=\"button\"\n class=\"edit-confirm-btn\"\n [title]=\"i18n.preferencesConfirmEdit\"\n (click)=\"confirmEditMode()\"\n >\n <mat-icon>check_circle</mat-icon>\n </button>\n <div class=\"table-body-scroll\" #tableBodyScroll>\n <table mat-table matSort [dataSource]=\"tableDataSource\" class=\"jquery-table\"\n [style.min-width]=\"tableMinWidth\"\n [trackBy]=\"trackRowFn\"\n [matSortActive]=\"_matSortActive\" [matSortDirection]=\"_matSortDirection\">\n <ng-container *ngFor=\"let column of visibleColumns\" [matColumnDef]=\"column.key\">\n <th\n mat-header-cell\n *matHeaderCellDef\n [mat-sort-header]=\"(!_editMode && column.sortable !== false) ? column.key : ''\"\n [disabled]=\"_editMode || column.sortable === false\"\n [style.width]=\"getColumnWidth(column.key)\"\n [class.drag-enabled]=\"isColumnDragDropEnabled || _editMode\"\n [class.drop-target]=\"isHeaderDropTarget(column.key)\"\n [class.edit-mode-header]=\"_editMode\"\n (dragover)=\"onHeaderDragOver(column.key, $event)\"\n (drop)=\"onHeaderDrop(column.key, $event)\"\n >\n <div class=\"head-cell\">\n <mat-icon *ngIf=\"column.icon\" class=\"head-icon\" fontSet=\"material-icons-outlined\">{{ column.icon }}</mat-icon>\n <span class=\"head-text\" [title]=\"column.header\">{{ column.header }}</span>\n <span\n *ngIf=\"isColumnDragDropEnabled || _editMode\"\n class=\"drag-handle\"\n draggable=\"true\"\n (dragstart)=\"onHeaderDragStart(column.key, $event)\"\n (dragend)=\"onHeaderDragEnd()\"\n (click)=\"$event.stopPropagation()\"\n >\n <mat-icon>drag_indicator</mat-icon>\n </span>\n </div>\n <!-- Poign\u00E9e de resize (visible uniquement en mode \u00E9dition) -->\n <span\n *ngIf=\"_editMode\"\n class=\"col-resize-handle\"\n draggable=\"false\"\n (mousedown)=\"onResizeStart(column.key, $event)\"\n ></span>\n </th>\n <td mat-cell *matCellDef=\"let row\"\n [style.width]=\"getColumnWidth(column.key)\"\n [class.edit-mode-cell]=\"_editMode\">\n <ng-container *ngIf=\"!_editMode; else editModeCellTpl\">\n <ng-container *ngIf=\"getCellTemplate(column.key) as customTpl; else defaultCellTpl\">\n <ng-container *ngTemplateOutlet=\"customTpl; context: { $implicit: row[_ROW_RAW_KEY], index: row[_ROW_INDEX_KEY] }\"></ng-container>\n </ng-container>\n <ng-template #defaultCellTpl>\n <ng-container [ngSwitch]=\"column.lazy ? getLazyRenderType(row[column.key]) : 'value'\">\n <span *ngSwitchCase=\"'loading'\" class=\"lazy-skeleton\"></span>\n <button *ngSwitchCase=\"'error'\" type=\"button\" class=\"lazy-error-btn\" (click)=\"retryLazyColumn(column)\" [title]=\"i18n.lazyErrorTitle\">\n <span class=\"lazy-error-icon\">\u26A0</span>\n <span class=\"lazy-error-label\">{{ i18n.lazyRetry }}</span>\n </button>\n <span *ngSwitchDefault class=\"cell-text\" [class.cell-empty]=\"isCellEmpty(row[column.key])\" [title]=\"asTitle(row[column.key])\">{{ isCellEmpty(row[column.key]) ? i18n.cellEmpty : row[column.key] }}</span>\n </ng-container>\n </ng-template>\n </ng-container>\n <ng-template #editModeCellTpl>\n <span class=\"cell-text cell-edit-mode\" [title]=\"asTitle(row[column.key])\">{{ row[column.key] }}</span>\n </ng-template>\n </td>\n </ng-container>\n\n <tr mat-header-row *matHeaderRowDef=\"renderedColumns; sticky: true\"></tr>\n <tr\n mat-row\n *matRowDef=\"let row; columns: renderedColumns; when: !isGroupHeader\"\n [attr.tabindex]=\"hasRowClickHandler && !_editMode ? 0 : null\"\n [ngClass]=\"getRowClass(row)\"\n [class.no-pointer]=\"!hasRowClickHandler || _editMode\"\n [class.edit-mode-row]=\"_editMode\"\n (click)=\"!_editMode && onRowClick(row, $event)\"\n (keydown.enter)=\"!_editMode && onRowClick(row, null)\"\n (keydown.space)=\"!_editMode && onRowClick(row, null)\"\n ></tr>\n <ng-container [matColumnDef]=\"_GROUP_ROW_COLUMN\">\n <td mat-cell *matCellDef=\"let row\" [attr.colspan]=\"renderedColumns.length\" class=\"group-header-cell\">\n <div class=\"group-header-label\">\n <span class=\"group-chevron\" [class.collapsed]=\"isGroupCollapsed(row[_ROW_GROUP_KEY])\" *ngIf=\"isNormalGroupRow(row)\">\u203A</span>\n <span *ngIf=\"isNormalGroupRow(row)\" class=\"group-key-label\">{{ row[_ROW_GROUP_KEY] }}</span>\n <span *ngIf=\"isGroupRowLoading(row)\" class=\"lazy-skeleton group-lazy-key-skeleton\"></span>\n <span *ngIf=\"isGroupRowTooMany(row)\" class=\"group-too-many-label\">{{ i18n.groupTooMany(row[_ROW_GROUP_COUNT]) }}</span>\n <span class=\"group-count-badge\">{{ row[_ROW_GROUP_COUNT] }}</span>\n <div class=\"group-pagination\" *ngIf=\"!isGroupCollapsed(row[_ROW_GROUP_KEY]) && row[_ROW_GROUP_PAGE_COUNT] > 1\" (click)=\"$event.stopPropagation()\">\n <button type=\"button\" class=\"group-page-btn\" [disabled]=\"row[_ROW_GROUP_PAGE] === 0\" (click)=\"groupPageBack(row[_ROW_GROUP_KEY])\">\u2039</button>\n <span class=\"group-page-info\">{{ row[_ROW_GROUP_PAGE] + 1 }} / {{ row[_ROW_GROUP_PAGE_COUNT] }}</span>\n <button type=\"button\" class=\"group-page-btn\" [disabled]=\"row[_ROW_GROUP_PAGE] >= row[_ROW_GROUP_PAGE_COUNT] - 1\" (click)=\"groupPageForward(row[_ROW_GROUP_KEY], row[_ROW_GROUP_COUNT])\">\u203A</button>\n </div>\n </div>\n </td>\n </ng-container>\n <tr mat-row *matRowDef=\"let row; columns: [_GROUP_ROW_COLUMN]; when: isGroupHeader\"\n class=\"group-header-row\"\n [class.group-header-loading]=\"isGroupRowLoading(row)\"\n [class.group-header-too-many]=\"isGroupRowTooMany(row)\"\n [attr.data-group-key]=\"row[_ROW_GROUP_KEY]\"\n (click)=\"onGroupHeaderClick(row)\"></tr>\n <tr class=\"mat-mdc-row no-data-row\" *matNoDataRow>\n <td class=\"mat-mdc-cell no-data-cell\" [attr.colspan]=\"renderedColumns.length\"></td>\n </tr>\n </table>\n </div><!-- /table-body-scroll -->\n <div class=\"table-body-overlay\" *ngIf=\"effectiveIsLoading || isEmpty\">\n <span class=\"table-body-overlay-text\">{{ effectiveIsLoading ? loadingStateLabel : emptyStateLabel }}</span>\n </div>\n </div><!-- /table-body-wrapper -->\n\n <div class=\"bottom-bar\" *ngIf=\"showPagination\">\n <mat-paginator\n [length]=\"totalRows\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"pageSizeOptions\"\n ></mat-paginator>\n </div>\n\n <div class=\"group-bottom-bar\" *ngIf=\"activeGroupByKey\">\n <div class=\"group-bottom-bar-container\">\n <div class=\"group-bottom-bar-left\">\n <button type=\"button\" class=\"group-reset-btn\" (click)=\"onGroupByChange(null)\">\n <svg viewBox=\"0 0 16 16\" fill=\"none\" xmlns=\"http://www.w3.org/2000/svg\" class=\"group-reset-icon\">\n <path d=\"M3 8a5 5 0 1 0 5-5 5 5 0 0 0-3.54 1.46L3 3\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n <path d=\"M3 3v2.5H5.5\" stroke=\"currentColor\" stroke-width=\"1.4\" stroke-linecap=\"round\" stroke-linejoin=\"round\"/>\n </svg>\n {{ i18n.groupRemove }}\n </button>\n <div class=\"group-bar-separator\"></div>\n <div class=\"group-sort-controls\">\n <span class=\"group-sort-label\">{{ i18n.groupSortLabel }}</span>\n <button type=\"button\" class=\"group-sort-btn\" [class.active]=\"groupSortOrder === 'asc'\" (click)=\"onGroupSortToggle('asc')\" [title]=\"i18n.groupSortAsc\">\n <mat-icon>arrow_upward</mat-icon>\n </button>\n <button type=\"button\" class=\"group-sort-btn\" [class.active]=\"groupSortOrder === 'desc'\" (click)=\"onGroupSortToggle('desc')\" [title]=\"i18n.groupSortDesc\">\n <mat-icon>arrow_downward</mat-icon>\n </button>\n </div>\n </div>\n <div class=\"mat-mdc-paginator-page-size\">\n <div class=\"mat-mdc-paginator-page-size-label\">{{ i18n.groupRowsPerGroup }}</div>\n <mat-form-field appearance=\"outline\" subscriptSizing=\"dynamic\" class=\"mat-mdc-paginator-page-size-select\">\n <mat-select\n [(ngModel)]=\"groupPageSize\"\n (ngModelChange)=\"onGroupPageSizeChange($event)\"\n panelClass=\"mat-mdc-paginator-select-page-size group-page-size-panel\"\n [panelWidth]=\"'auto'\"\n >\n <mat-select-trigger>{{ groupPageSize === 0 ? i18n.groupShowAll : groupPageSize }}</mat-select-trigger>\n <mat-option *ngFor=\"let opt of pageSizeOptionsGroupBy\" [value]=\"opt\">{{ opt }}</mat-option>\n <mat-option [value]=\"0\">{{ i18n.groupShowAll }}</mat-option>\n </mat-select>\n </mat-form-field>\n </div>\n </div>\n </div>\n </div>\n </div>\n</section>\n", styles: [":host{display:block;max-height:100%;min-height:inherit;--jqt-title-color: #1f2937;--jqt-text-color: #111827;--jqt-text-secondary: #6b7280;--jqt-text-muted: #9ca3af;--jqt-surface-color: #ffffff;--jqt-surface-alt-color: #f9fafb;--jqt-row-hover-bg: #f3f4f6;--jqt-border-color: #e5e7eb;--jqt-group-bg: #eef2f7;--jqt-group-bg-hover: #e4ecf4;--jqt-group-accent: #94a3b8;--jqt-group-border: #d1d9e4;--jqt-primary-color: #1d4ed8;--jqt-danger-color: #dc2626;--jqt-danger-hover-bg: #fef2f2;--jqt-danger-border: #fca5a5;--jqt-header-bg: #f5f5f5;--jqt-header-text-color: #374151;--jqt-cell-bg: #ffffff;--jqt-footer-bg: #f5f5f5;--jqt-footer-text-color: #374151;--jqt-footer-icon-color: #374151}.jquery-table-wrapper{display:flex;flex-direction:column;width:100%;height:100%;min-height:inherit;padding:2px 2px 0;box-sizing:border-box}.table-header{display:flex;align-items:center;justify-content:flex-start;gap:8px;margin-bottom:12px}.table-header h3{margin:0;font-size:20px;font-weight:600;color:var(--jqt-title-color)}.table-layout{display:grid;grid-template-columns:minmax(155px,190px) minmax(0,1fr);gap:12px;align-items:stretch;min-height:inherit;height:100%}.table-layout.no-slice{grid-template-columns:minmax(0,1fr)}.table-layout.slice-collapsed{grid-template-columns:40px minmax(0,1fr)}.table-shell{display:flex;flex-direction:column;flex:1 1 0;min-height:0;min-width:0;width:100%;max-width:100%}.table-body-wrapper{position:relative;flex:1 1 0;min-height:0;display:flex;flex-direction:column}.table-body-scroll{flex:1 1 0;overflow-x:auto;overflow-y:auto;min-height:0;border-top:1px solid var(--jqt-border-color);border-right:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);border-radius:8px 8px 0 0;scrollbar-width:thin;scrollbar-color:var(--jqt-border-color) transparent}.table-body-scroll::-webkit-scrollbar{width:6px}.table-body-scroll::-webkit-scrollbar-thumb{background:var(--jqt-border-color);border-radius:4px}.table-body-overlay{position:absolute;top:var(--actual-header-height, 56px);left:0;right:0;bottom:0;z-index:3;display:flex;align-items:center;justify-content:center;background:var(--jqt-surface-color);border-right:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);pointer-events:none}.table-body-overlay-text{font-size:14px;color:var(--jqt-text-secondary)}.table-toolbar{display:flex;align-items:center;justify-content:space-between;gap:8px;padding:0 0 8px}.table-toolbar-left{flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:6px}.table-toolbar-right{flex:0 0 auto;display:flex;align-items:center;gap:6px}.search-input-wrapper{position:relative;display:inline-flex;align-items:center;flex:1 1 auto;min-width:0;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 8px;box-sizing:border-box;gap:6px}.search-input-wrapper:focus-within{border-color:var(--jqt-text-muted)}.search-input-wrapper .search-icon{flex-shrink:0;width:14px;height:14px;color:var(--jqt-text-muted)}.search-input{flex:1;border:none;outline:none;font-size:13px;color:var(--jqt-text-color);background:transparent;padding:7px 0;min-width:0}.search-input::placeholder{color:var(--jqt-text-muted)}.search-count{flex-shrink:0;font-size:12px;font-weight:600;color:var(--jqt-text-secondary);background:var(--jqt-row-hover-bg);border-radius:10px;padding:1px 7px;min-width:20px;text-align:center;line-height:18px}.search-clear-btn{background:none;border:none;cursor:pointer;color:var(--jqt-text-muted);font-size:16px;padding:0;line-height:1;flex-shrink:0}.search-clear-btn:hover{color:var(--jqt-text-color)}.slice-expand-btn{flex-shrink:0;width:34px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--jqt-text-color);padding:0;line-height:1}.slice-expand-btn:hover{border-color:var(--jqt-text-muted)}.slice-expand-mat-icon{font-size:18px;width:18px;height:18px;line-height:1}.view-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.view-btn:hover{border-color:var(--jqt-text-muted)}.view-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.export-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.export-btn:hover{border-color:var(--jqt-text-muted)}.export-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.view-menu-export-divider{margin:4px 0!important}.view-menu-export-item{display:flex;align-items:center;gap:6px;width:100%;padding:0 16px;height:40px;border:none;background:transparent;cursor:pointer;font-size:13px;font-weight:500;color:var(--jqt-text-color);text-align:left;box-sizing:border-box}.view-menu-export-item:hover{background:#0000000a}.view-menu-export-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.edit-confirm-btn{position:absolute;top:10px;right:10px;z-index:200;display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:50%;background:#16a34a;cursor:pointer;box-shadow:0 2px 8px #00000040;transition:background .15s}.edit-confirm-btn mat-icon{color:#fff;font-size:22px;width:22px;height:22px}.edit-confirm-btn:hover{background:#15803d}.preferences-message{display:flex;align-items:center;gap:8px;margin:0 0 8px;padding:8px 12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;font-size:12px;color:#15803d}.preferences-message .preferences-message-icon{font-size:16px;width:16px;height:16px}:host ::ng-deep .edit-mode-header{background:#3b82f60a!important;cursor:grab!important;-webkit-user-select:none;user-select:none;overflow:visible!important}.col-resize-handle{position:absolute;top:0;right:0;width:6px;height:100%;cursor:col-resize!important;background:#3b82f640;z-index:20}.col-resize-handle:hover{background:#3b82f6b3}.col-resize-handle:active{background:#3b82f6e6}:host ::ng-deep th.mat-mdc-header-cell{position:relative;overflow:visible!important}:host ::ng-deep .edit-mode-row{pointer-events:none;-webkit-user-select:none;user-select:none;opacity:.8}.cell-edit-mode{pointer-events:none;-webkit-user-select:none;user-select:none;color:var(--jqt-text-muted)}.group-header-row{cursor:pointer}:host ::ng-deep .jquery-table{border-spacing:0}:host ::ng-deep .group-header-row td{padding:0!important;background:var(--jqt-group-bg);border-bottom:1px solid var(--jqt-group-border);border-top:1px solid var(--jqt-group-border);border-left:3px solid var(--jqt-group-accent);position:sticky;top:var(--actual-header-height, 56px);z-index:2;box-shadow:-4px 0 0 0 var(--jqt-group-bg)}:host ::ng-deep .group-header-row:hover td{background:var(--jqt-group-bg-hover);box-shadow:-4px 0 0 0 var(--jqt-group-bg-hover)}:host ::ng-deep .group-header-loading td,:host ::ng-deep .group-header-loading:hover td,:host ::ng-deep .group-header-too-many td,:host ::ng-deep .group-header-too-many:hover td{cursor:default;background:var(--jqt-surface-alt-color)}.group-too-many-label{font-size:12px;font-weight:500;color:var(--jqt-warn-color, #b45309);font-style:italic}.group-header-cell{padding:0!important}.group-header-label{display:flex;align-items:center;gap:6px;padding:6px 16px;font-size:12px;font-weight:700;color:var(--jqt-text-color);text-transform:uppercase;letter-spacing:.04em}.group-key-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-count-badge{flex-shrink:0;font-size:11px;line-height:1;font-weight:600;color:#fff;background:var(--jqt-text-secondary, #6b7280);border-radius:10px;padding:3px 7px;min-width:20px;text-align:center;text-transform:none;letter-spacing:0}.group-pagination{display:inline-flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.group-page-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:1px solid var(--jqt-border-color);border-radius:5px;background:var(--jqt-surface-color);cursor:pointer;font-size:14px;color:var(--jqt-text-color);line-height:1;padding:0 0 3px;text-transform:none;letter-spacing:0}.group-page-btn:hover:not(:disabled){border-color:var(--jqt-text-muted);background:var(--jqt-surface-alt-color)}.group-page-btn:disabled{opacity:.35;cursor:default}.group-page-info{font-size:11px;color:var(--jqt-text-secondary);min-width:36px;text-align:center;font-weight:500;text-transform:none;letter-spacing:0}.group-chevron{flex-shrink:0;font-size:14px;font-weight:700;padding-bottom:3px;transition:transform .15s ease;transform:rotate(90deg)}.group-chevron.collapsed{transform:rotate(0)}.no-data-row{cursor:default!important}.no-data-cell{padding:0!important;height:0!important;border:none!important}.jquery-table{width:100%;table-layout:fixed}:host ::ng-deep .mat-mdc-table{width:100%;background:transparent}:host ::ng-deep .mat-mdc-header-cell,:host ::ng-deep .mat-mdc-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host ::ng-deep .mat-mdc-header-row{background:var(--jqt-header-bg)}:host ::ng-deep .mat-mdc-header-cell{background:var(--jqt-header-bg);color:var(--jqt-header-text-color)}:host ::ng-deep .mat-mdc-row,:host ::ng-deep .mat-mdc-cell{background:var(--jqt-cell-bg)}:host ::ng-deep .mat-mdc-row{cursor:pointer;border-bottom-color:var(--jqt-border-color)}:host ::ng-deep .mat-mdc-row.no-pointer{cursor:auto}:host ::ng-deep .mat-mdc-row.no-data-row{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drag-enabled{-webkit-user-select:none;user-select:none}:host ::ng-deep .mat-mdc-header-cell.drag-enabled:active{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drop-target{background:var(--jqt-row-hover-bg)}.head-cell{display:flex;align-items:center;justify-content:space-between;gap:6px;min-width:0}.drag-handle{display:inline-flex;align-items:center;cursor:grab;color:var(--jqt-border-color);flex-shrink:0;padding:0 2px;border-radius:4px;line-height:1}.drag-handle:active{cursor:grabbing}.drag-handle:hover{color:var(--jqt-text-muted)}.drag-handle mat-icon{font-size:16px;width:16px;height:16px;line-height:1}.head-icon{font-size:14px;width:14px;height:14px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.head-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase;font-size:11px;font-weight:600;letter-spacing:.05em}.head-actions{display:inline-flex;align-items:center;gap:6px}.cell-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cell-empty{color:var(--jqt-empty-cell-color, #9ca3af);font-style:italic}.bottom-bar{display:flex;align-items:center;flex:0 0 auto;background:var(--jqt-footer-bg);border-top:1px solid var(--jqt-border-color);border-right:1px solid var(--jqt-border-color);border-bottom:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);border-radius:0 0 8px 8px}:host ::ng-deep .bottom-bar mat-paginator{flex:1 1 auto;background:transparent;color:var(--jqt-footer-text-color)}:host ::ng-deep .bottom-bar .mat-mdc-paginator-outer-container{display:flex}:host ::ng-deep .bottom-bar .mat-mdc-paginator-container{display:flex;align-items:center;flex-wrap:nowrap;min-height:0;height:var(--actual-header-height, 56px);padding:0 8px;box-sizing:border-box}:host ::ng-deep .bottom-bar .mat-mdc-paginator-range-label,:host ::ng-deep .bottom-bar .mat-mdc-paginator-page-size-label{font-size:12px;color:var(--jqt-footer-text-color)}:host ::ng-deep .bottom-bar .mat-mdc-icon-button{color:var(--jqt-footer-icon-color);width:28px;height:28px;padding:0;line-height:1;display:inline-flex;align-items:center;justify-content:center;--mdc-icon-button-state-layer-size: 28px}:host ::ng-deep .bottom-bar .mat-mdc-icon-button .mat-mdc-button-persistent-ripple,:host ::ng-deep .bottom-bar .mat-mdc-icon-button .mdc-icon-button__state-layer{width:28px;height:28px;top:50%;left:50%;transform:translate(-50%,-50%)}:host ::ng-deep .bottom-bar .mat-mdc-icon-button .mat-icon{font-size:18px;width:18px;height:18px;line-height:18px}:host ::ng-deep .bottom-bar .mat-mdc-icon-button:disabled{opacity:.35}:host ::ng-deep .bottom-bar .mat-mdc-paginator-page-size-select{width:72px;font-size:12px}:host ::ng-deep .bottom-bar .mat-mdc-text-field-wrapper{padding:0 8px}:host ::ng-deep .bottom-bar .mdc-text-field--outlined{height:28px}:host ::ng-deep .bottom-bar .mat-mdc-form-field-infix{padding-top:0;padding-bottom:0;min-height:0;height:28px;display:flex;align-items:center}:host ::ng-deep .bottom-bar .mat-mdc-select-value-text{font-size:12px;color:var(--jqt-text-color)}:host ::ng-deep .bottom-bar .mdc-notched-outline__leading,:host ::ng-deep .bottom-bar .mdc-notched-outline__notch,:host ::ng-deep .bottom-bar .mdc-notched-outline__trailing{border-color:var(--jqt-border-color)}.group-bottom-bar{display:flex;align-items:center;flex:0 0 auto;background:var(--jqt-footer-bg);border-top:1px solid var(--jqt-border-color);border-right:1px solid var(--jqt-border-color);border-bottom:1px solid var(--jqt-border-color);border-left:1px solid var(--jqt-border-color);border-radius:0 0 8px 8px}.group-bottom-bar-container{display:flex;align-items:center;justify-content:space-between;flex-wrap:nowrap;height:var(--actual-header-height, 56px);padding:0 16px;gap:16px;width:100%;box-sizing:border-box}.group-bar-separator{width:1px;height:20px;background:#9ca3af;flex-shrink:0;margin:0 4px}.group-bottom-bar-left{display:flex;align-items:center;gap:8px;flex-shrink:0}.group-sort-controls{display:flex;align-items:center;gap:2px}.group-sort-label{font-size:12px;color:var(--jqt-footer-text-color);white-space:nowrap;margin-right:2px}.group-sort-btn{display:inline-flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;border:1px solid transparent;border-radius:6px;background:transparent;color:var(--jqt-footer-icon-color);cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease}.group-sort-btn mat-icon{font-size:16px;width:16px;height:16px;line-height:16px}.group-sort-btn:hover{background:#0000000a}.group-sort-btn.active{color:var(--jqt-primary-color, #1976d2);border-color:var(--jqt-primary-color, #1976d2);background:#1976d214}:host ::ng-deep .group-bottom-bar .mat-mdc-paginator-page-size{display:flex;align-items:center;gap:8px}:host ::ng-deep .group-bottom-bar .mat-mdc-paginator-page-size-label{font-size:12px;color:var(--jqt-footer-text-color);white-space:nowrap}:host ::ng-deep .group-bottom-bar .mat-mdc-paginator-page-size-select{width:72px;font-size:12px}:host ::ng-deep .group-bottom-bar .mat-mdc-text-field-wrapper{padding:0 8px}:host ::ng-deep .group-bottom-bar .mdc-text-field--outlined{height:28px}:host ::ng-deep .group-bottom-bar .mat-mdc-form-field-infix{padding-top:0;padding-bottom:0;min-height:0;height:28px;display:flex;align-items:center}:host ::ng-deep .group-bottom-bar .mat-mdc-select-value-text{font-size:12px;color:var(--jqt-text-color)}:host ::ng-deep .group-bottom-bar .mdc-notched-outline__leading,:host ::ng-deep .group-bottom-bar .mdc-notched-outline__notch,:host ::ng-deep .group-bottom-bar .mdc-notched-outline__trailing{border-color:var(--jqt-border-color)}:host ::ng-deep .group-bottom-bar .mat-mdc-select-arrow{color:var(--jqt-footer-text-color)}.group-reset-btn{display:inline-flex;align-items:center;gap:6px;height:30px;padding:0 10px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);color:var(--jqt-text-secondary);font-size:12px;font-weight:500;font-family:inherit;cursor:pointer;transition:background .15s ease,color .15s ease,border-color .15s ease;white-space:nowrap;flex-shrink:0}.group-reset-btn:hover{background:var(--jqt-danger-hover-bg);color:var(--jqt-danger-color);border-color:var(--jqt-danger-border)}.group-reset-icon{width:14px;height:14px;flex-shrink:0}::ng-deep .mat-mdc-paginator-select-page-size .mat-pseudo-checkbox{display:none!important}::ng-deep .group-page-size-panel{min-width:90px!important}::ng-deep .group-page-size-panel .mat-mdc-option{justify-content:center;min-height:40px}::ng-deep .group-page-size-panel .mdc-list-item__primary-text{text-align:center;width:100%}@media (max-width: 960px){.table-layout{grid-template-columns:1fr}}::ng-deep .view-menu-panel .mat-mdc-menu-panel{min-width:220px!important;overflow:visible!important}::ng-deep .view-menu-panel .mat-mdc-menu-content{padding:4px 0!important}::ng-deep .view-submenu-panel .mat-mdc-menu-panel{min-width:200px!important}::ng-deep .view-submenu-panel .mat-mdc-menu-content{padding:0!important}::ng-deep .view-menu-panel .view-menu-sub-trigger.mat-mdc-menu-item{min-height:42px;padding:0 14px;display:flex;align-items:center;box-sizing:border-box}::ng-deep .view-menu-panel .view-menu-sub-trigger .mdc-list-item__primary-text{display:flex;align-items:center;width:100%;gap:0;overflow:hidden}::ng-deep .view-menu-sub-label{font-size:13px;font-weight:500;color:var(--jqt-text-color);flex:0 0 auto;white-space:nowrap;padding-right:8px}::ng-deep .view-menu-sub-value{font-size:12px;color:var(--jqt-text-secondary);flex:1 1 auto;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:right}::ng-deep .view-submenu{padding:4px 6px;min-width:180px;box-sizing:border-box}::ng-deep .view-submenu .view-menu-item{display:flex;align-items:center;gap:8px;width:100%;padding:7px 8px;border:none;background:transparent;border-radius:6px;font-size:13px;color:var(--jqt-text-color);cursor:pointer;text-align:left;box-sizing:border-box;white-space:nowrap}::ng-deep .view-submenu .view-menu-item:hover{background:var(--jqt-row-hover-bg)}::ng-deep .view-submenu .view-menu-item.active{font-weight:600}::ng-deep .view-submenu .view-menu-item:disabled{opacity:.4;cursor:default}::ng-deep .view-menu-col-icon{flex-shrink:0;font-size:14px!important;width:14px!important;height:14px!important;color:var(--jqt-text-muted)}::ng-deep .view-menu-check{flex-shrink:0;width:16px;text-align:center;font-size:12px;color:var(--jqt-text-color)}::ng-deep .view-menu-separator{border:none;border-top:1px solid var(--jqt-border-color);margin:4px 0}.lazy-skeleton{display:block;height:12px;width:70%;border-radius:4px;background:linear-gradient(90deg,var(--jqt-border-color) 25%,var(--jqt-surface-alt-color) 50%,var(--jqt-border-color) 75%);background-size:200% 100%;animation:lazy-shimmer 1.4s ease-in-out infinite}.group-lazy-key-skeleton{flex:1 1 auto;min-width:80px;height:13px;border-radius:4px}@keyframes lazy-shimmer{0%{background-position:200% 0}to{background-position:-200% 0}}.lazy-error-btn{display:inline-flex;align-items:center;gap:4px;background:none;border:1px solid #fca5a5;border-radius:4px;padding:2px 6px;cursor:pointer;color:#ef4444;font-size:12px;opacity:.85;transition:opacity .15s}.lazy-error-btn:hover{opacity:1;background:#fef2f2}.lazy-error-icon{font-size:12px}.lazy-error-label{font-size:11px;font-weight:500}::ng-deep .lazy-menu-spinner{display:inline-block;margin-left:auto;font-size:13px;color:var(--jqt-text-secondary);animation:lazy-spin 1s linear infinite}::ng-deep .lazy-menu-error{display:inline-block;margin-left:auto;font-size:13px;color:#ef4444}@keyframes lazy-spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}\n"] }]
|
|
1565
2522
|
}], propDecorators: { config: [{
|
|
1566
2523
|
type: Input
|
|
1567
2524
|
}], dataSource: [{
|
|
@@ -1572,6 +2529,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
|
|
|
1572
2529
|
type: Input
|
|
1573
2530
|
}], columnsConfig: [{
|
|
1574
2531
|
type: Input
|
|
2532
|
+
}], view: [{
|
|
2533
|
+
type: Input
|
|
1575
2534
|
}], columnLabels: [{
|
|
1576
2535
|
type: Input
|
|
1577
2536
|
}], isLoading: [{
|
|
@@ -1586,6 +2545,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
|
|
|
1586
2545
|
type: Output
|
|
1587
2546
|
}], rowSelected: [{
|
|
1588
2547
|
type: Output
|
|
2548
|
+
}], sortChange: [{
|
|
2549
|
+
type: Output
|
|
2550
|
+
}], pageChange: [{
|
|
2551
|
+
type: Output
|
|
2552
|
+
}], searchChange: [{
|
|
2553
|
+
type: Output
|
|
2554
|
+
}], groupByChange: [{
|
|
2555
|
+
type: Output
|
|
2556
|
+
}], columnsChange: [{
|
|
2557
|
+
type: Output
|
|
1589
2558
|
}], paginator: [{
|
|
1590
2559
|
type: ViewChild,
|
|
1591
2560
|
args: [MatPaginator]
|
|
@@ -1614,5 +2583,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
|
|
|
1614
2583
|
* Generated bundle index. Do not edit.
|
|
1615
2584
|
*/
|
|
1616
2585
|
|
|
1617
|
-
export { JqtCellDefDirective, SlicePanelComponent, TableComponent };
|
|
2586
|
+
export { JQT_I18N, JQT_I18N_DEFAULTS, JqtCellDefDirective, SlicePanelComponent, TableComponent, col };
|
|
1618
2587
|
//# sourceMappingURL=oneteme-jquery-table.mjs.map
|