@oneteme/jquery-table 0.0.1 → 0.0.3
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 +732 -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 +304 -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 +1434 -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 +165 -27
- package/lib/component/table.constants.d.ts +30 -0
- package/lib/component/view/view.facade.d.ts +95 -0
- package/lib/jqt-i18n.token.d.ts +87 -0
- package/lib/jquery-table.model.d.ts +82 -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,617 @@ 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
|
+
userCustomized: false,
|
|
624
|
+
};
|
|
625
|
+
this.sliceBy = {
|
|
626
|
+
activeDynamicColumns: [],
|
|
627
|
+
availableForDynamic: [],
|
|
628
|
+
allDynamicColumns: [],
|
|
629
|
+
activeLabel: 'Aucun',
|
|
630
|
+
};
|
|
631
|
+
this._panelConfig = { columns: [], sliceConfigs: [] };
|
|
632
|
+
this._hiddenStaticKeys = new Set();
|
|
633
|
+
// ── Events
|
|
634
|
+
this._events$ = new Subject();
|
|
635
|
+
this.events$ = this._events$.asObservable();
|
|
636
|
+
}
|
|
637
|
+
// ── Computed flags
|
|
638
|
+
get enabled() {
|
|
639
|
+
return this._panelConfig.config?.enabled === true;
|
|
640
|
+
}
|
|
641
|
+
get showFields() {
|
|
642
|
+
return this.enabled;
|
|
643
|
+
}
|
|
644
|
+
get showGroupBySection() {
|
|
645
|
+
return this.enabled && this.groupByColumns.length > 0;
|
|
646
|
+
}
|
|
647
|
+
get showSliceBySection() {
|
|
648
|
+
return this.enabled && ((this._panelConfig.sliceConfigs?.length ?? 0) > 0 ||
|
|
649
|
+
this.sliceBy.allDynamicColumns.length > 0);
|
|
650
|
+
}
|
|
651
|
+
get allowColumnRemoval() {
|
|
652
|
+
return this._panelConfig.config?.enableColumnRemoval !== false;
|
|
653
|
+
}
|
|
654
|
+
get isColumnDragDropEnabled() {
|
|
655
|
+
return this._panelConfig.config?.enableColumnDragDrop === true;
|
|
656
|
+
}
|
|
657
|
+
get menuBaseColumns() {
|
|
658
|
+
return this._uniqueByKey((this._panelConfig.columns || []).filter(c => !c.optional));
|
|
659
|
+
}
|
|
660
|
+
get menuOptionalColumns() {
|
|
661
|
+
return this._uniqueByKey((this._panelConfig.columns || []).filter(c => c.optional));
|
|
662
|
+
}
|
|
663
|
+
get totalColumnCount() {
|
|
664
|
+
return new Set((this._panelConfig.columns || []).map(c => c.key)).size;
|
|
665
|
+
}
|
|
666
|
+
get groupByColumns() {
|
|
667
|
+
const seen = new Set();
|
|
668
|
+
return (this._panelConfig.columns || []).filter(c => {
|
|
669
|
+
if (c.groupable === false || seen.has(c.key))
|
|
670
|
+
return false;
|
|
671
|
+
if (!c.header && c.groupable !== true)
|
|
672
|
+
return false;
|
|
673
|
+
seen.add(c.key);
|
|
674
|
+
return true;
|
|
675
|
+
});
|
|
676
|
+
}
|
|
677
|
+
get activeGroupByLabel() {
|
|
678
|
+
if (!this.groupBy.activeKey)
|
|
679
|
+
return this._panelConfig.noneLabel ?? 'Aucun';
|
|
680
|
+
const col = this.groupByColumns.find(c => c.key === this.groupBy.activeKey);
|
|
681
|
+
return col?.header || this.groupBy.activeKey;
|
|
682
|
+
}
|
|
683
|
+
get staticSlicesForMenu() {
|
|
684
|
+
return (this._panelConfig.sliceConfigs ?? [])
|
|
685
|
+
.filter(s => !!s.columnKey)
|
|
686
|
+
.map(s => {
|
|
687
|
+
const colIcon = (this._panelConfig.columns ?? []).find(c => c.key === s.columnKey)?.icon;
|
|
688
|
+
return { key: s.columnKey, title: s.title || humanizeKey(s.columnKey), icon: s.icon ?? colIcon };
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
colLabel(col) {
|
|
692
|
+
return col.header || humanizeKey(col.key);
|
|
693
|
+
}
|
|
694
|
+
// ── Mise à jour de la config (appelée depuis refreshViewModel)
|
|
695
|
+
update(panelConfig) {
|
|
696
|
+
this._panelConfig = panelConfig;
|
|
697
|
+
this._refreshDefaultColumns();
|
|
698
|
+
this._refreshDynamicSliceMeta();
|
|
699
|
+
if (!this.groupBy.userCustomized) {
|
|
700
|
+
this.groupBy.activeKey = panelConfig.defaultGroupBy ?? null;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
// ── Actions Fields
|
|
704
|
+
setActiveColumns(columns) {
|
|
705
|
+
this.fields.activeColumns = columns;
|
|
706
|
+
this.fields.userCustomized = true;
|
|
707
|
+
}
|
|
708
|
+
setUserCustomized(value) {
|
|
709
|
+
this.fields.userCustomized = value;
|
|
710
|
+
}
|
|
711
|
+
/** Remet la vue dans son état initial : colonnes par défaut, groupBy par défaut, pas de dynamic slices. */
|
|
712
|
+
resetToDefaults() {
|
|
713
|
+
this.fields.userCustomized = false;
|
|
714
|
+
this.groupBy.userCustomized = false;
|
|
715
|
+
this._refreshDefaultColumns();
|
|
716
|
+
this.groupBy.activeKey = this._panelConfig.defaultGroupBy ?? null;
|
|
717
|
+
this.sliceBy.activeDynamicColumns = [];
|
|
718
|
+
this._refreshDynamicSliceMeta();
|
|
719
|
+
}
|
|
720
|
+
addColumn(column) {
|
|
721
|
+
const configCols = this._panelConfig.columns ?? [];
|
|
722
|
+
const configIndex = configCols.findIndex(c => c.key === column.key);
|
|
723
|
+
if (configIndex === -1) {
|
|
724
|
+
this.fields.activeColumns = [...this.fields.activeColumns, column];
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
const insertBefore = this.fields.activeColumns.findIndex(c => {
|
|
728
|
+
const ci = configCols.findIndex(cc => cc.key === c.key);
|
|
729
|
+
return ci !== -1 && ci > configIndex;
|
|
730
|
+
});
|
|
731
|
+
if (insertBefore === -1) {
|
|
732
|
+
this.fields.activeColumns = [...this.fields.activeColumns, column];
|
|
733
|
+
}
|
|
734
|
+
else {
|
|
735
|
+
const copy = [...this.fields.activeColumns];
|
|
736
|
+
copy.splice(insertBefore, 0, column);
|
|
737
|
+
this.fields.activeColumns = copy;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
this.fields.userCustomized = true;
|
|
741
|
+
this._events$.next({ type: 'fieldsChanged', fieldIds: this.fields.activeColumns.map(c => c.key) });
|
|
742
|
+
}
|
|
743
|
+
removeColumn(columnKey) {
|
|
744
|
+
const col = this.fields.activeColumns.find(c => c.key === columnKey);
|
|
745
|
+
if (!col)
|
|
746
|
+
return false;
|
|
747
|
+
if (!col.optional && !this.allowColumnRemoval)
|
|
748
|
+
return false;
|
|
749
|
+
if (this.fields.activeColumns.length <= 1)
|
|
750
|
+
return false;
|
|
751
|
+
this.fields.activeColumns = this.fields.activeColumns.filter(c => c.key !== columnKey);
|
|
752
|
+
this.fields.userCustomized = true;
|
|
753
|
+
this._events$.next({ type: 'fieldsChanged', fieldIds: this.fields.activeColumns.map(c => c.key) });
|
|
754
|
+
return true;
|
|
755
|
+
}
|
|
756
|
+
toggleColumnVisibility(column, visible) {
|
|
757
|
+
const isVisible = this.isColumnVisible(column.key);
|
|
758
|
+
if (visible && !isVisible) {
|
|
759
|
+
this.addColumn(column);
|
|
760
|
+
}
|
|
761
|
+
else if (!visible && isVisible) {
|
|
762
|
+
this.removeColumn(column.key);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
reorderColumns(fromKey, toKey) {
|
|
766
|
+
const prev = this.fields.activeColumns.findIndex(c => c.key === fromKey);
|
|
767
|
+
const curr = this.fields.activeColumns.findIndex(c => c.key === toKey);
|
|
768
|
+
if (prev === -1 || curr === -1 || prev === curr)
|
|
769
|
+
return false;
|
|
770
|
+
const reordered = [...this.fields.activeColumns];
|
|
771
|
+
const [moved] = reordered.splice(prev, 1);
|
|
772
|
+
reordered.splice(curr, 0, moved);
|
|
773
|
+
this.fields.activeColumns = reordered;
|
|
774
|
+
this.fields.userCustomized = true;
|
|
775
|
+
this._events$.next({ type: 'fieldsChanged', fieldIds: this.fields.activeColumns.map(c => c.key) });
|
|
776
|
+
return true;
|
|
777
|
+
}
|
|
778
|
+
isColumnVisible(key) {
|
|
779
|
+
return this.fields.activeColumns.some(c => c.key === key);
|
|
780
|
+
}
|
|
781
|
+
// ── Actions Group by
|
|
782
|
+
setGroupBy(key) {
|
|
783
|
+
this.groupBy.activeKey = key;
|
|
784
|
+
this.groupBy.userCustomized = true;
|
|
785
|
+
this._events$.next({ type: 'groupByChanged', key });
|
|
786
|
+
}
|
|
787
|
+
// ── Actions Slice by (dynamique)
|
|
788
|
+
/** Appelée quand le SlicePanelComponent émet dynamicSliceKeysChange */
|
|
789
|
+
onDynamicSliceKeysChange(keys) {
|
|
790
|
+
const allCols = this._panelConfig.columns ?? [];
|
|
791
|
+
const staticKeys = new Set((this._panelConfig.sliceConfigs ?? []).map(s => s.columnKey).filter((k) => !!k));
|
|
792
|
+
const dynamicKeySet = new Set(keys);
|
|
793
|
+
const existingKeys = new Set([...staticKeys, ...dynamicKeySet]);
|
|
794
|
+
this.sliceBy.activeDynamicColumns = keys
|
|
795
|
+
.map(k => allCols.find(c => c.key === k))
|
|
796
|
+
.filter((c) => !!c);
|
|
797
|
+
const seen = new Set();
|
|
798
|
+
this.sliceBy.availableForDynamic = allCols.filter(c => {
|
|
799
|
+
if (c.sliceable === false || existingKeys.has(c.key) || seen.has(c.key))
|
|
800
|
+
return false;
|
|
801
|
+
seen.add(c.key);
|
|
802
|
+
return true;
|
|
803
|
+
});
|
|
804
|
+
this._computeSliceLabel();
|
|
805
|
+
this._events$.next({ type: 'dynamicSlicesChanged', keys });
|
|
806
|
+
}
|
|
807
|
+
isDynamicSliceActive(key) {
|
|
808
|
+
return this.sliceBy.activeDynamicColumns.some(c => c.key === key);
|
|
809
|
+
}
|
|
810
|
+
// ── Contrat core
|
|
811
|
+
/** Retourne un snapshot ViewState (contrat core) de l'état courant. */
|
|
812
|
+
toViewState() {
|
|
813
|
+
return {
|
|
814
|
+
selectedFieldIds: this.fields.activeColumns.map(c => c.key),
|
|
815
|
+
groupByKey: this.groupBy.activeKey,
|
|
816
|
+
dynamicSliceKeys: this.sliceBy.activeDynamicColumns.map(c => c.key),
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
/** Convertit une TableColumnProvider en ViewField (contrat core). */
|
|
820
|
+
static toViewField(col) {
|
|
821
|
+
return {
|
|
822
|
+
id: col.key,
|
|
823
|
+
label: col.header,
|
|
824
|
+
icon: col.icon,
|
|
825
|
+
groupable: col.groupable,
|
|
826
|
+
sliceable: col.sliceable,
|
|
827
|
+
optional: col.optional,
|
|
828
|
+
};
|
|
829
|
+
}
|
|
830
|
+
// ── Destructor
|
|
831
|
+
destroy() {
|
|
832
|
+
this._events$.complete();
|
|
833
|
+
}
|
|
834
|
+
// ── Helpers privés
|
|
835
|
+
_refreshDefaultColumns() {
|
|
836
|
+
const defaultCols = (this._panelConfig.columns || []).filter(c => !c.optional);
|
|
837
|
+
if (!this.fields.userCustomized || this.fields.activeColumns.length === 0) {
|
|
838
|
+
this.fields.activeColumns = [...defaultCols];
|
|
839
|
+
}
|
|
840
|
+
else {
|
|
841
|
+
const allowedKeys = new Set((this._panelConfig.columns || []).map(c => c.key));
|
|
842
|
+
this.fields.activeColumns = this.fields.activeColumns.filter(c => allowedKeys.has(c.key));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
_refreshDynamicSliceMeta() {
|
|
846
|
+
const allCols = this._panelConfig.columns ?? [];
|
|
847
|
+
const staticKeys = new Set((this._panelConfig.sliceConfigs ?? []).map(s => s.columnKey).filter((k) => !!k));
|
|
848
|
+
const activeDynKeys = new Set(this.sliceBy.activeDynamicColumns.map(c => c.key));
|
|
849
|
+
const seen1 = new Set();
|
|
850
|
+
this.sliceBy.availableForDynamic = allCols.filter(c => {
|
|
851
|
+
if (c.sliceable === false || staticKeys.has(c.key) || activeDynKeys.has(c.key) || seen1.has(c.key))
|
|
852
|
+
return false;
|
|
853
|
+
if (!c.header && c.sliceable !== true)
|
|
854
|
+
return false;
|
|
855
|
+
seen1.add(c.key);
|
|
856
|
+
return true;
|
|
857
|
+
});
|
|
858
|
+
const seen2 = new Set();
|
|
859
|
+
this.sliceBy.allDynamicColumns = allCols.filter(c => {
|
|
860
|
+
if (c.sliceable === false || staticKeys.has(c.key) || seen2.has(c.key))
|
|
861
|
+
return false;
|
|
862
|
+
if (!c.header && c.sliceable !== true)
|
|
863
|
+
return false;
|
|
864
|
+
seen2.add(c.key);
|
|
865
|
+
return true;
|
|
866
|
+
});
|
|
867
|
+
this._computeSliceLabel();
|
|
868
|
+
}
|
|
869
|
+
/** Notifie la facade des slices statiques actuellement cachées (géré par table.component).
|
|
870
|
+
* Recalcule le label slice afin qu'il reflète uniquement les slices visibles. */
|
|
871
|
+
updateHiddenStaticKeys(keys) {
|
|
872
|
+
this._hiddenStaticKeys = keys;
|
|
873
|
+
this._computeSliceLabel();
|
|
874
|
+
}
|
|
875
|
+
_computeSliceLabel() {
|
|
876
|
+
const visibleStatics = (this._panelConfig.sliceConfigs ?? []).filter(s => !s.columnKey || !this._hiddenStaticKeys.has(s.columnKey));
|
|
877
|
+
const visibleStaticCount = visibleStatics.length;
|
|
878
|
+
const total = visibleStaticCount + this.sliceBy.activeDynamicColumns.length;
|
|
879
|
+
if (total === 0) {
|
|
880
|
+
this.sliceBy.activeLabel = this._panelConfig.noneLabel ?? 'Aucun';
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (total === 1) {
|
|
884
|
+
if (visibleStaticCount === 1) {
|
|
885
|
+
const first = visibleStatics[0];
|
|
886
|
+
this.sliceBy.activeLabel = first?.title || first?.columnKey || '1';
|
|
887
|
+
return;
|
|
888
|
+
}
|
|
889
|
+
this.sliceBy.activeLabel = this.sliceBy.activeDynamicColumns[0]?.header || '1';
|
|
890
|
+
return;
|
|
891
|
+
}
|
|
892
|
+
this.sliceBy.activeLabel = `${total} actifs`;
|
|
893
|
+
}
|
|
894
|
+
_uniqueByKey(cols) {
|
|
895
|
+
const seen = new Set();
|
|
896
|
+
return cols.filter(c => {
|
|
897
|
+
if (seen.has(c.key))
|
|
898
|
+
return false;
|
|
899
|
+
seen.add(c.key);
|
|
900
|
+
return true;
|
|
901
|
+
});
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
/**
|
|
906
|
+
* Constantes sentinel utilisées pour les identifiants de colonnes et les valeurs
|
|
907
|
+
* projetées dans le datasource interne de la table.
|
|
908
|
+
*
|
|
909
|
+
* Centraliser ici évite les typos silencieuses et facilite les tests unitaires.
|
|
910
|
+
*/
|
|
911
|
+
/** Identifiant de la colonne virtuelle rendue sur toute la largeur pour les en-têtes de groupe. */
|
|
912
|
+
const GROUP_ROW_COLUMN = '__group_row';
|
|
913
|
+
/** Marqueur posé sur les objets de ligne pour signaler une ligne d'en-tête de groupe. */
|
|
914
|
+
const GROUP_HEADER_MARKER = '__groupHeader';
|
|
915
|
+
/** Clé du groupe spéciale « chargement lazy en cours ». */
|
|
916
|
+
const GROUP_KEY_LAZY_LOADING = '__lazy_loading__';
|
|
917
|
+
/** Clé du groupe spéciale « trop de groupes ». */
|
|
918
|
+
const GROUP_KEY_TOO_MANY = '__too_many_groups__';
|
|
919
|
+
/** Propriété injectée dans chaque ligne projetée : référence vers la ligne brute d'origine. */
|
|
920
|
+
const ROW_RAW_KEY = '__raw';
|
|
921
|
+
/** Propriété injectée dans chaque ligne projetée : index global de la ligne dans le jeu de données filtré. */
|
|
922
|
+
const ROW_INDEX_KEY = '__index';
|
|
923
|
+
/** Propriété injectée dans chaque ligne projetée : clé du groupe auquel appartient la ligne d'en-tête. */
|
|
924
|
+
const ROW_GROUP_KEY = '__groupKey';
|
|
925
|
+
/** Propriété injectée dans chaque ligne projetée : nombre de lignes dans le groupe. */
|
|
926
|
+
const ROW_GROUP_COUNT = '__groupCount';
|
|
927
|
+
/** Propriété injectée dans chaque ligne projetée : page courante du groupe. */
|
|
928
|
+
const ROW_GROUP_PAGE = '__groupPage';
|
|
929
|
+
/** Propriété injectée dans chaque ligne projetée : nombre de pages du groupe. */
|
|
930
|
+
const ROW_GROUP_PAGE_COUNT = '__groupPageCount';
|
|
931
|
+
/** Valeur sentinelle posée dans une cellule lazy en cours de chargement. */
|
|
932
|
+
const LAZY_LOADING_VALUE = '__lazy_loading__';
|
|
933
|
+
/** Valeur sentinelle posée dans une cellule lazy en erreur. */
|
|
934
|
+
const LAZY_ERROR_VALUE = '__lazy_error__';
|
|
935
|
+
|
|
936
|
+
/**
|
|
937
|
+
* Gère le cycle de vie des colonnes à chargement différé (lazy) :
|
|
938
|
+
* statuts, données, annulation des requêtes en cours.
|
|
939
|
+
*
|
|
940
|
+
* Pas de dépendance Angular/DOM — peut être instancié manuellement dans le composant.
|
|
941
|
+
*/
|
|
942
|
+
class LazyColumnManager {
|
|
943
|
+
constructor() {
|
|
944
|
+
this._status = new Map();
|
|
945
|
+
this._data = new Map();
|
|
946
|
+
this._cancels = new Map();
|
|
947
|
+
}
|
|
948
|
+
get status() {
|
|
949
|
+
return this._status;
|
|
950
|
+
}
|
|
951
|
+
get data() {
|
|
952
|
+
return this._data;
|
|
953
|
+
}
|
|
954
|
+
getStatus(key) {
|
|
955
|
+
return this._status.get(key) ?? 'idle';
|
|
956
|
+
}
|
|
957
|
+
getRenderType(rowValue) {
|
|
958
|
+
if (rowValue === LAZY_LOADING_VALUE)
|
|
959
|
+
return 'loading';
|
|
960
|
+
if (rowValue === LAZY_ERROR_VALUE)
|
|
961
|
+
return 'error';
|
|
962
|
+
return 'value';
|
|
963
|
+
}
|
|
964
|
+
/** Annule tous les fetches en cours et réinitialise l'état (appeler lors d'un changement de données). */
|
|
965
|
+
cancelAll() {
|
|
966
|
+
this._cancels.forEach(s => { s.next(); s.complete(); });
|
|
967
|
+
this._cancels.clear();
|
|
968
|
+
this._status = new Map();
|
|
969
|
+
this._data = new Map();
|
|
970
|
+
}
|
|
971
|
+
/** Réinitialise une colonne et relance son fetch. */
|
|
972
|
+
retry(column, resolvedData, callbacks) {
|
|
973
|
+
const k = column.key;
|
|
974
|
+
const prev = this._cancels.get(k);
|
|
975
|
+
if (prev) {
|
|
976
|
+
prev.next();
|
|
977
|
+
prev.complete();
|
|
978
|
+
}
|
|
979
|
+
this._cancels.delete(k);
|
|
980
|
+
const newStatus = new Map(this._status);
|
|
981
|
+
newStatus.delete(k);
|
|
982
|
+
this._status = newStatus;
|
|
983
|
+
const newData = new Map(this._data);
|
|
984
|
+
newData.delete(k);
|
|
985
|
+
this._data = newData;
|
|
986
|
+
this.fetch(column, resolvedData, callbacks);
|
|
987
|
+
}
|
|
988
|
+
/** Lance le chargement différé d'une colonne. No-op si déjà en cours. */
|
|
989
|
+
fetch(column, resolvedData, callbacks) {
|
|
990
|
+
if (!column.lazy)
|
|
991
|
+
return;
|
|
992
|
+
const k = column.key;
|
|
993
|
+
if (this._status.get(k) === 'loading')
|
|
994
|
+
return;
|
|
995
|
+
const prev = this._cancels.get(k);
|
|
996
|
+
if (prev) {
|
|
997
|
+
prev.next();
|
|
998
|
+
prev.complete();
|
|
999
|
+
}
|
|
1000
|
+
const cancel$ = new Subject();
|
|
1001
|
+
this._cancels.set(k, cancel$);
|
|
1002
|
+
const newStatus = new Map(this._status);
|
|
1003
|
+
newStatus.set(k, 'loading');
|
|
1004
|
+
this._status = newStatus;
|
|
1005
|
+
callbacks.onRefresh();
|
|
1006
|
+
column.lazy.fetchFn().pipe(take(1), takeUntil(cancel$)).subscribe({
|
|
1007
|
+
next: (values) => {
|
|
1008
|
+
const rowMap = new Map();
|
|
1009
|
+
values.forEach((value, i) => {
|
|
1010
|
+
if (i < resolvedData.length)
|
|
1011
|
+
rowMap.set(resolvedData[i], value);
|
|
1012
|
+
});
|
|
1013
|
+
const newData = new Map(this._data);
|
|
1014
|
+
newData.set(k, rowMap);
|
|
1015
|
+
this._data = newData;
|
|
1016
|
+
const newStatus2 = new Map(this._status);
|
|
1017
|
+
newStatus2.set(k, 'loaded');
|
|
1018
|
+
this._status = newStatus2;
|
|
1019
|
+
callbacks.onRefresh();
|
|
1020
|
+
callbacks.onMarkForCheck();
|
|
1021
|
+
},
|
|
1022
|
+
error: () => {
|
|
1023
|
+
const newStatusErr = new Map(this._status);
|
|
1024
|
+
newStatusErr.set(k, 'error');
|
|
1025
|
+
this._status = newStatusErr;
|
|
1026
|
+
callbacks.onRefresh();
|
|
1027
|
+
callbacks.onMarkForCheck();
|
|
1028
|
+
},
|
|
1029
|
+
});
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
const STORAGE_PREFIX = 'jqt_prefs_';
|
|
1034
|
+
/**
|
|
1035
|
+
* Gère la persistance de la configuration d'un tableau dans le localStorage.
|
|
1036
|
+
* Identifie chaque tableau par un `tableId` unique fourni par l'utilisateur.
|
|
1037
|
+
*/
|
|
1038
|
+
class TablePreferencesManager {
|
|
1039
|
+
constructor(tableId) {
|
|
1040
|
+
this.key = STORAGE_PREFIX + tableId;
|
|
1041
|
+
}
|
|
1042
|
+
/** Retourne la configuration sauvegardée, ou `null` si absente / corrompue. */
|
|
1043
|
+
load() {
|
|
1044
|
+
try {
|
|
1045
|
+
const raw = localStorage.getItem(this.key);
|
|
1046
|
+
return raw ? JSON.parse(raw) : null;
|
|
1047
|
+
}
|
|
1048
|
+
catch {
|
|
1049
|
+
return null;
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
/** Persiste la configuration dans le localStorage. */
|
|
1053
|
+
save(config) {
|
|
1054
|
+
try {
|
|
1055
|
+
localStorage.setItem(this.key, JSON.stringify(config));
|
|
1056
|
+
}
|
|
1057
|
+
catch {
|
|
1058
|
+
// localStorage peut être indisponible (mode privé strict, quota dépassé…)
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
/** Supprime la configuration sauvegardée. */
|
|
1062
|
+
clear() {
|
|
1063
|
+
localStorage.removeItem(this.key);
|
|
1064
|
+
}
|
|
1065
|
+
/** Indique si une configuration est actuellement sauvegardée. */
|
|
1066
|
+
hasSaved() {
|
|
1067
|
+
return localStorage.getItem(this.key) !== null;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
/**
|
|
1072
|
+
* GroupByManager
|
|
1073
|
+
*
|
|
1074
|
+
* Encapsule tout l'état et la logique liés au mode Group By :
|
|
1075
|
+
* - état collapsed / expanded des groupes
|
|
1076
|
+
* - pagination interne par groupe
|
|
1077
|
+
* - tri des groupes (asc / desc)
|
|
1078
|
+
* - scroll vers un groupe
|
|
1079
|
+
*
|
|
1080
|
+
* Le composant TableComponent l'instancie et lui passe deux callbacks :
|
|
1081
|
+
* - `onNeedProjection` : demande une reprojection légère de la page courante
|
|
1082
|
+
* - `onNeedRender` : demande un rebuild complet (tri changé etc.)
|
|
1083
|
+
*/
|
|
1084
|
+
class GroupByManager {
|
|
1085
|
+
constructor(onNeedProjection, onNeedRender) {
|
|
1086
|
+
this.onNeedProjection = onNeedProjection;
|
|
1087
|
+
this.onNeedRender = onNeedRender;
|
|
1088
|
+
/** Groupes actuellement ouverts (mode default-collapsed) ou fermés (mode default-expanded). */
|
|
1089
|
+
this._collapsedGroups = new Set();
|
|
1090
|
+
/** Page courante par clé de groupe. */
|
|
1091
|
+
this._groupPages = new Map();
|
|
1092
|
+
/** Taille de page pour la pagination interne des groupes. */
|
|
1093
|
+
this.groupPageSize = 5;
|
|
1094
|
+
/** Ordre de tri des groupes ('asc' | 'desc'). */
|
|
1095
|
+
this.groupSortOrder = 'asc';
|
|
1096
|
+
/**
|
|
1097
|
+
* Quand `true`, tous les groupes sont collapsés par défaut.
|
|
1098
|
+
* `_collapsedGroups` contient alors le seul groupe ouvert.
|
|
1099
|
+
*/
|
|
1100
|
+
this._defaultGroupCollapsed = false;
|
|
1101
|
+
}
|
|
1102
|
+
// ── API publique
|
|
1103
|
+
/** Initialise le mode par défaut selon qu'un groupBy vient d'être activé. */
|
|
1104
|
+
setDefaultCollapsed(collapsed) {
|
|
1105
|
+
this._defaultGroupCollapsed = collapsed;
|
|
1106
|
+
}
|
|
1107
|
+
/** Réinitialise l'état (à appeler quand le groupBy change de colonne). */
|
|
1108
|
+
reset() {
|
|
1109
|
+
this._collapsedGroups.clear();
|
|
1110
|
+
this._groupPages.clear();
|
|
1111
|
+
}
|
|
1112
|
+
isGroupCollapsed(groupKey) {
|
|
1113
|
+
return this._defaultGroupCollapsed
|
|
1114
|
+
? !this._collapsedGroups.has(groupKey)
|
|
1115
|
+
: this._collapsedGroups.has(groupKey);
|
|
1116
|
+
}
|
|
1117
|
+
toggleGroupCollapse(groupKey, scrollTo) {
|
|
1118
|
+
if (this._collapsedGroups.has(groupKey)) {
|
|
1119
|
+
this._collapsedGroups.delete(groupKey);
|
|
1120
|
+
}
|
|
1121
|
+
else {
|
|
1122
|
+
this._collapsedGroups.clear();
|
|
1123
|
+
this._collapsedGroups.add(groupKey);
|
|
1124
|
+
scrollTo(groupKey);
|
|
1125
|
+
}
|
|
1126
|
+
this.onNeedProjection();
|
|
1127
|
+
}
|
|
1128
|
+
/** Retourne le groupe actuellement ouvert (seulement en mode default-collapsed avec exactement 1 ouvert). */
|
|
1129
|
+
getSingleOpenGroup() {
|
|
1130
|
+
return this._defaultGroupCollapsed && this._collapsedGroups.size === 1
|
|
1131
|
+
? [...this._collapsedGroups][0]
|
|
1132
|
+
: null;
|
|
1133
|
+
}
|
|
1134
|
+
// ── Pagination par groupe
|
|
1135
|
+
getPage(groupKey) {
|
|
1136
|
+
return this._groupPages.get(groupKey) ?? 0;
|
|
1137
|
+
}
|
|
1138
|
+
groupPageBack(groupKey) {
|
|
1139
|
+
const current = this._groupPages.get(groupKey) ?? 0;
|
|
1140
|
+
if (current > 0) {
|
|
1141
|
+
this._groupPages.set(groupKey, current - 1);
|
|
1142
|
+
this.onNeedProjection();
|
|
1143
|
+
}
|
|
1144
|
+
}
|
|
1145
|
+
groupPageForward(groupKey, totalCount) {
|
|
1146
|
+
const maxPage = Math.ceil(totalCount / this.groupPageSize) - 1;
|
|
1147
|
+
const current = this._groupPages.get(groupKey) ?? 0;
|
|
1148
|
+
if (current < maxPage) {
|
|
1149
|
+
this._groupPages.set(groupKey, current + 1);
|
|
1150
|
+
this.onNeedProjection();
|
|
1151
|
+
}
|
|
1152
|
+
}
|
|
1153
|
+
onGroupPageSizeChange(size) {
|
|
1154
|
+
this.groupPageSize = size;
|
|
1155
|
+
this._groupPages.clear();
|
|
1156
|
+
this.onNeedRender();
|
|
1157
|
+
}
|
|
1158
|
+
onGroupSortToggle(dir, scrollTo) {
|
|
1159
|
+
if (this.groupSortOrder === dir)
|
|
1160
|
+
return;
|
|
1161
|
+
this.groupSortOrder = dir;
|
|
1162
|
+
this._groupPages.clear();
|
|
1163
|
+
this.onNeedRender();
|
|
1164
|
+
const openGroup = this.getSingleOpenGroup();
|
|
1165
|
+
if (openGroup)
|
|
1166
|
+
scrollTo(openGroup);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
/**
|
|
1171
|
+
* ExportManager
|
|
1172
|
+
*
|
|
1173
|
+
* Encapsule l'export CSV : construction du fichier BOM/UTF-8, échappement et déclenchement du téléchargement.
|
|
1174
|
+
* Aucune dépendance Angular — indépendant du cycle de vie du composant.
|
|
1175
|
+
*/
|
|
1176
|
+
class ExportManager {
|
|
1177
|
+
constructor(getRows, getColumns, getConfig, resolveCellValue) {
|
|
1178
|
+
this.getRows = getRows;
|
|
1179
|
+
this.getColumns = getColumns;
|
|
1180
|
+
this.getConfig = getConfig;
|
|
1181
|
+
this.resolveCellValue = resolveCellValue;
|
|
1182
|
+
}
|
|
1183
|
+
export() {
|
|
1184
|
+
const cfg = this.getConfig();
|
|
1185
|
+
if (!cfg?.enabled)
|
|
1186
|
+
return;
|
|
1187
|
+
const rows = this.getRows();
|
|
1188
|
+
const columns = this.getColumns();
|
|
1189
|
+
const filename = (cfg.filename || 'export') + '.csv';
|
|
1190
|
+
const header = columns.map(c => this._csvEscape(c.header || c.key)).join(',');
|
|
1191
|
+
const lines = rows.map((row, idx) => {
|
|
1192
|
+
if (cfg.transform) {
|
|
1193
|
+
const t = cfg.transform(row);
|
|
1194
|
+
return columns.map(c => this._csvEscape(String(t[c.key] ?? ''))).join(',');
|
|
1195
|
+
}
|
|
1196
|
+
return columns.map(c => this._csvEscape(String(this.resolveCellValue(c, row, idx) ?? ''))).join(',');
|
|
1197
|
+
});
|
|
1198
|
+
// BOM UTF-8 pour une ouverture correcte sous Excel
|
|
1199
|
+
const blob = new Blob(['\uFEFF' + [header, ...lines].join('\n')], { type: 'text/csv;charset=utf-8' });
|
|
1200
|
+
const url = URL.createObjectURL(blob);
|
|
1201
|
+
const a = document.createElement('a');
|
|
1202
|
+
a.href = url;
|
|
1203
|
+
a.download = filename.endsWith('.csv') ? filename : filename + '.csv';
|
|
1204
|
+
a.click();
|
|
1205
|
+
URL.revokeObjectURL(url);
|
|
1206
|
+
}
|
|
1207
|
+
_csvEscape(value) {
|
|
1208
|
+
if (value.includes(',') || value.includes('"') || value.includes('\n')) {
|
|
1209
|
+
return '"' + value.replace(/"/g, '""') + '"';
|
|
1210
|
+
}
|
|
1211
|
+
return value;
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
|
|
542
1215
|
class TableComponent {
|
|
543
1216
|
constructor() {
|
|
544
1217
|
this.isLoading = false;
|
|
@@ -547,8 +1220,35 @@ class TableComponent {
|
|
|
547
1220
|
this.columnRemoved = new EventEmitter();
|
|
548
1221
|
this.categorySelected = new EventEmitter();
|
|
549
1222
|
this.rowSelected = new EventEmitter();
|
|
1223
|
+
/** Émis à chaque changement de tri (y compris le tri initial). */
|
|
1224
|
+
this.sortChange = new EventEmitter();
|
|
1225
|
+
/** Émis à chaque changement de page ou de taille de page. */
|
|
1226
|
+
this.pageChange = new EventEmitter();
|
|
1227
|
+
/** Émis à chaque changement de la recherche texte. */
|
|
1228
|
+
this.searchChange = new EventEmitter();
|
|
1229
|
+
/** Émis quand le Group by change (clé de colonne, ou `null` si désactivé). */
|
|
1230
|
+
this.groupByChange = new EventEmitter();
|
|
1231
|
+
/** Émis quand la liste des colonnes visibles change (clés dans l’ordre d’affichage). */
|
|
1232
|
+
this.columnsChange = new EventEmitter();
|
|
1233
|
+
/** Map clé→TemplateRef pré-calculée depuis _cellDefs. Mise à jour dans ngAfterContentInit et sur _cellDefs.changes. */
|
|
1234
|
+
this._cellDefMap = new Map();
|
|
550
1235
|
this.renderedColumns = [];
|
|
551
1236
|
this.tableDataSource = new MatTableDataSource([]);
|
|
1237
|
+
/** Constante exposée au template pour le matColumnDef de la ligne de groupe. */
|
|
1238
|
+
this._GROUP_ROW_COLUMN = GROUP_ROW_COLUMN;
|
|
1239
|
+
/** Clés d'accès aux propriétés des lignes projetées — exposées pour le template. */
|
|
1240
|
+
this._ROW_RAW_KEY = ROW_RAW_KEY;
|
|
1241
|
+
this._ROW_INDEX_KEY = ROW_INDEX_KEY;
|
|
1242
|
+
this._ROW_GROUP_KEY = ROW_GROUP_KEY;
|
|
1243
|
+
this._ROW_GROUP_COUNT = ROW_GROUP_COUNT;
|
|
1244
|
+
this._ROW_GROUP_PAGE = ROW_GROUP_PAGE;
|
|
1245
|
+
this._ROW_GROUP_PAGE_COUNT = ROW_GROUP_PAGE_COUNT;
|
|
1246
|
+
/** Facade View : état et actions centralisés (Champs / Group by / Slice by) */
|
|
1247
|
+
this._view = new ViewFacade();
|
|
1248
|
+
/**
|
|
1249
|
+
* Snapshot stable synchronisé depuis _view.fields.activeColumns dans refreshViewModel().
|
|
1250
|
+
* Propriété (non getter) pour qu'Angular puisse la suivre sans NG0100.
|
|
1251
|
+
*/
|
|
552
1252
|
this.activeColumns = [];
|
|
553
1253
|
this.activeSliceFilter = () => true;
|
|
554
1254
|
// Propriétés stables pour les bindings du template (contre erreur NG0100)
|
|
@@ -556,55 +1256,71 @@ class TableComponent {
|
|
|
556
1256
|
this._sliceColumns = [];
|
|
557
1257
|
this._sliceData = [];
|
|
558
1258
|
this._sliceShowToggle = true;
|
|
559
|
-
|
|
560
|
-
this.
|
|
561
|
-
|
|
562
|
-
this.
|
|
563
|
-
this.
|
|
1259
|
+
/** Snapshot stable de la visibilité du panneau slice — jamais calculé depuis @ViewChild */
|
|
1260
|
+
this._showSlicePanel = false;
|
|
1261
|
+
/** Snapshot stable de l'état collapsed du panneau slice, mis à jour via (collapsedChange) */
|
|
1262
|
+
this._slicePanelCollapsed = false;
|
|
1263
|
+
this._sliceByMenuItems = [];
|
|
564
1264
|
this.searchQuery = '';
|
|
565
|
-
this.activeGroupByKey = null;
|
|
566
1265
|
this._matSortActive = '';
|
|
567
1266
|
this._matSortDirection = '';
|
|
568
|
-
this._collapsedGroups = new Set();
|
|
569
1267
|
this._activeSort = { active: '', direction: '' };
|
|
570
1268
|
this._sortSubscribed = null;
|
|
571
1269
|
this._destroy$ = new Subject();
|
|
572
|
-
this._defaultGroupCollapsed = false;
|
|
573
|
-
this._groupPages = new Map();
|
|
574
1270
|
this._totalFilteredCount = 0;
|
|
575
|
-
|
|
576
|
-
this.
|
|
577
|
-
|
|
1271
|
+
/** Gestionnaire du mode Group By (état collapsed, pagination, tri des groupes). */
|
|
1272
|
+
this._groupBy = new GroupByManager(() => { this._projectCurrentPage(); this._cdr.markForCheck(); }, () => this.refreshViewModel());
|
|
1273
|
+
/** Gestionnaire du cycle de vie des colonnes lazy (fetch, annulation, statuts, données). */
|
|
1274
|
+
this._lazy = new LazyColumnManager();
|
|
578
1275
|
this._cdr = inject(ChangeDetectorRef);
|
|
579
1276
|
this._el = inject((ElementRef));
|
|
580
|
-
this.
|
|
581
|
-
|
|
1277
|
+
this._ngZone = inject(NgZone);
|
|
1278
|
+
this._i18nRaw = inject(JQT_I18N, { optional: true });
|
|
1279
|
+
/** Labels de l’interface. Fusionnés depuis JQT_I18N_DEFAULTS et le token JQT_I18N injecté. */
|
|
1280
|
+
this.i18n = { ...JQT_I18N_DEFAULTS, ...(this._i18nRaw ?? {}) };
|
|
1281
|
+
this.isGroupHeader = (_, row) => row[GROUP_HEADER_MARKER] === true;
|
|
1282
|
+
/** Gestionnaire export CSV. */
|
|
1283
|
+
this._export = new ExportManager(() => this._allFilteredRows, () => this.activeColumns, () => this.resolvedConfig.export, (col, row, idx) => this.resolveCellValue(col, row, idx));
|
|
1284
|
+
// ── Lifecycle
|
|
582
1285
|
this.pageSize = 5;
|
|
583
1286
|
this.pageSizeOptions = [5, 10, 20];
|
|
584
1287
|
this.pageSizeOptionsGroupBy = [5, 10, 20];
|
|
585
|
-
this.groupPageSize = 5;
|
|
586
|
-
this.groupSortOrder = 'asc';
|
|
587
1288
|
this.draggedColumnKey = null;
|
|
588
1289
|
this.dragOverColumnKey = null;
|
|
589
|
-
|
|
1290
|
+
// ── Mode édition (Préférences)
|
|
1291
|
+
/** Vrai quand le mode édition est actif (resize + drag colonnes, interactions désactivées). */
|
|
1292
|
+
this._editMode = false;
|
|
1293
|
+
/** Largeurs de colonnes personnalisées par l'utilisateur (clé → px). */
|
|
1294
|
+
this._columnWidths = {};
|
|
1295
|
+
/** Message de feedback après save/clear. `null` = pas de message. */
|
|
1296
|
+
this._preferencesMessage = null;
|
|
1297
|
+
/** Vrai si une configuration est sauvegardée pour ce tableau. */
|
|
1298
|
+
this._hasSavedConfig = false;
|
|
1299
|
+
this._preferencesManager = null;
|
|
1300
|
+
/** true une fois que le group-by par défaut a été initialisé (évite de reconfigurer GroupByManager à chaque changement de données). */
|
|
1301
|
+
this._defaultGroupByApplied = false;
|
|
1302
|
+
this._resizeState = null;
|
|
1303
|
+
/** Dynamic slice keys à restaurer après ngAfterViewInit (slicePanelRef pas encore dispo). */
|
|
1304
|
+
this._pendingDynamicSliceKeys = null;
|
|
1305
|
+
/** Filtres de slice à restaurer après ngAfterViewInit (slicePanelRef pas encore dispo). */
|
|
1306
|
+
this._pendingSliceFilters = null;
|
|
590
1307
|
this.resolvedConfig = { columns: [] };
|
|
591
1308
|
this._columnMap = new Map();
|
|
592
1309
|
this._resolvedData = [];
|
|
593
1310
|
this._preservePageIndex = null;
|
|
594
1311
|
this._initialSearchApplied = false;
|
|
595
|
-
// ── Optimisation performance
|
|
1312
|
+
// ── Optimisation performance
|
|
596
1313
|
this._allFilteredRows = [];
|
|
597
1314
|
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();
|
|
1315
|
+
/** Cache groupBy — invalidé dès que rows/key/tri changent. */
|
|
1316
|
+
this._groupCache = null;
|
|
606
1317
|
this._pendingRender = null;
|
|
607
|
-
this.
|
|
1318
|
+
this._staticSliceHiddenKeys = new Set();
|
|
1319
|
+
this._configHiddenKeys = new Set();
|
|
1320
|
+
/** Clés des colonnes visibles du dernier émission columnsChange (pour éviter les émissions dupliquement). */
|
|
1321
|
+
this._prevColumnKeys = [];
|
|
1322
|
+
this.trackRowFn = (_index, row) => row[GROUP_HEADER_MARKER] ? `__group_${row[ROW_GROUP_KEY]}` : row[ROW_INDEX_KEY] ?? _index;
|
|
1323
|
+
this._preferencesMessageTimer = null;
|
|
608
1324
|
}
|
|
609
1325
|
set paginator(value) {
|
|
610
1326
|
this._paginator = value;
|
|
@@ -614,15 +1330,40 @@ class TableComponent {
|
|
|
614
1330
|
}
|
|
615
1331
|
}
|
|
616
1332
|
get paginator() { return this._paginator; }
|
|
1333
|
+
// Délégués vers _view (conservés pour compatibilité template sans refactoring HTML)
|
|
1334
|
+
get _staticSlicesForMenu() { return this._view.staticSlicesForMenu; }
|
|
1335
|
+
get _activeDynamicSliceColumns() { return this._view.sliceBy.activeDynamicColumns; }
|
|
1336
|
+
get _availableColumnsForDynamicSlice() { return this._view.sliceBy.availableForDynamic; }
|
|
1337
|
+
get _allDynamicSliceColumns() { return this._view.sliceBy.allDynamicColumns; }
|
|
1338
|
+
get _activeSliceByLabel() { return this._view.sliceBy.activeLabel; }
|
|
1339
|
+
get activeGroupByKey() { return this._view.groupBy.activeKey; }
|
|
1340
|
+
/** Délégués vers GroupByManager pour la compatibilité des templates. */
|
|
1341
|
+
get groupPageSize() { return this._groupBy.groupPageSize; }
|
|
1342
|
+
set groupPageSize(v) { this._groupBy.groupPageSize = v; }
|
|
1343
|
+
get groupSortOrder() { return this._groupBy.groupSortOrder; }
|
|
1344
|
+
set groupSortOrder(v) { this._groupBy.groupSortOrder = v; }
|
|
1345
|
+
/** Compatibilité template : retourne la map de statuts lazy. */
|
|
1346
|
+
get _lazyColumnStatus() { return this._lazy.status; }
|
|
1347
|
+
/** Compatibilité template : retourne la map de données lazy. */
|
|
1348
|
+
get _lazyColumnData() { return this._lazy.data; }
|
|
1349
|
+
/** Retourne true si la ligne de groupe est une ligne normale (ni lazy, ni too-many). */
|
|
1350
|
+
isNormalGroupRow(row) {
|
|
1351
|
+
return row[ROW_GROUP_KEY] !== GROUP_KEY_LAZY_LOADING && row[ROW_GROUP_KEY] !== GROUP_KEY_TOO_MANY;
|
|
1352
|
+
}
|
|
1353
|
+
isGroupRowLoading(row) { return row[ROW_GROUP_KEY] === GROUP_KEY_LAZY_LOADING; }
|
|
1354
|
+
isGroupRowTooMany(row) { return row[ROW_GROUP_KEY] === GROUP_KEY_TOO_MANY; }
|
|
1355
|
+
/** Gère le clic sur une ligne d'en-tête de groupe (no-op pour les états spéciaux). */
|
|
1356
|
+
onGroupHeaderClick(row) {
|
|
1357
|
+
if (!this.isNormalGroupRow(row))
|
|
1358
|
+
return;
|
|
1359
|
+
this.toggleGroupCollapse(row[ROW_GROUP_KEY]);
|
|
1360
|
+
}
|
|
617
1361
|
// Seuil au-delà duquel le rendu groupé est bloqué pour protéger le navigateur
|
|
618
1362
|
static { this.MAX_GROUP_COUNT = 500; }
|
|
619
1363
|
ngOnChanges(changes) {
|
|
620
1364
|
const dataChanged = !!(changes['config'] || changes['data'] || changes['dataSource']);
|
|
621
1365
|
if (dataChanged) {
|
|
622
|
-
this.
|
|
623
|
-
this._lazyFetchCancels.clear();
|
|
624
|
-
this._lazyColumnStatus = new Map();
|
|
625
|
-
this._lazyColumnData = new Map();
|
|
1366
|
+
this._lazy.cancelAll();
|
|
626
1367
|
}
|
|
627
1368
|
if (changes['config'] && !this._initialSearchApplied) {
|
|
628
1369
|
const initial = this.config?.search?.initialQuery;
|
|
@@ -636,28 +1377,81 @@ class TableComponent {
|
|
|
636
1377
|
this.activeColumns.filter(c => c.lazy).forEach(c => this.triggerLazyFetch(c));
|
|
637
1378
|
}
|
|
638
1379
|
}
|
|
1380
|
+
ngAfterContentInit() {
|
|
1381
|
+
this._rebuildCellDefMap();
|
|
1382
|
+
this._cellDefs.changes.pipe(takeUntil(this._destroy$)).subscribe(() => {
|
|
1383
|
+
this._rebuildCellDefMap();
|
|
1384
|
+
this._cdr.markForCheck();
|
|
1385
|
+
});
|
|
1386
|
+
}
|
|
639
1387
|
ngAfterViewInit() {
|
|
640
1388
|
this.attachPaginator();
|
|
641
1389
|
this.attachSort();
|
|
642
1390
|
this._measureHeaderHeight();
|
|
1391
|
+
this._applyPendingDynamicSliceKeys();
|
|
1392
|
+
}
|
|
1393
|
+
_applyPendingDynamicSliceKeys() {
|
|
1394
|
+
const hasPendingDynamic = !!this._pendingDynamicSliceKeys;
|
|
1395
|
+
const hasPendingFilters = !!this._pendingSliceFilters;
|
|
1396
|
+
if (!hasPendingDynamic && !hasPendingFilters)
|
|
1397
|
+
return;
|
|
1398
|
+
const allCols = this.resolvedConfig.columns ?? [];
|
|
1399
|
+
const keys = hasPendingDynamic ? [...this._pendingDynamicSliceKeys] : [];
|
|
1400
|
+
const filters = this._pendingSliceFilters ? { ...this._pendingSliceFilters } : null;
|
|
1401
|
+
this._pendingDynamicSliceKeys = null;
|
|
1402
|
+
this._pendingSliceFilters = null;
|
|
1403
|
+
// Tout est différé dans le même setTimeout pour :
|
|
1404
|
+
// 1. Éviter NG0100 (modifications après vérification dans ngAfterViewInit)
|
|
1405
|
+
// 2. Garantir que les dynamic slices sont ajoutées AVANT la restauration des filtres
|
|
1406
|
+
// (les indices dans sliceFilters dépendent de l'ordre final de _cachedSlices)
|
|
1407
|
+
setTimeout(() => {
|
|
1408
|
+
keys.forEach(key => {
|
|
1409
|
+
const col = allCols.find(c => c.key === key);
|
|
1410
|
+
if (col)
|
|
1411
|
+
this.addDynamicSlice(col);
|
|
1412
|
+
});
|
|
1413
|
+
if (filters) {
|
|
1414
|
+
if (this.slicePanelRef) {
|
|
1415
|
+
this.slicePanelRef.restoreFilters(filters);
|
|
1416
|
+
}
|
|
1417
|
+
else {
|
|
1418
|
+
// slicePanelRef toujours null (données pas encore arrivées) :
|
|
1419
|
+
// remettre en attente pour que _recomputeShowSlicePanel les applique
|
|
1420
|
+
this._pendingSliceFilters = filters;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1423
|
+
this._cdr.markForCheck();
|
|
1424
|
+
});
|
|
643
1425
|
}
|
|
644
1426
|
ngOnDestroy() {
|
|
645
1427
|
if (this._pendingRender !== null) {
|
|
646
1428
|
clearTimeout(this._pendingRender);
|
|
647
1429
|
}
|
|
1430
|
+
if (this._preferencesMessageTimer !== null) {
|
|
1431
|
+
clearTimeout(this._preferencesMessageTimer);
|
|
1432
|
+
}
|
|
1433
|
+
// Nettoie un éventuel resize en cours (listeners window non retirés)
|
|
1434
|
+
this._resizeState = null;
|
|
648
1435
|
this._destroy$.next();
|
|
649
1436
|
this._destroy$.complete();
|
|
1437
|
+
this._view.destroy();
|
|
650
1438
|
}
|
|
651
1439
|
_measureHeaderHeight() {
|
|
652
|
-
const
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
1440
|
+
const measure = () => {
|
|
1441
|
+
const headerRow = this._el.nativeElement.querySelector('tr.mat-mdc-header-row');
|
|
1442
|
+
if (headerRow) {
|
|
1443
|
+
const h = headerRow.getBoundingClientRect().height;
|
|
1444
|
+
if (h > 0) {
|
|
1445
|
+
this._el.nativeElement.style.setProperty('--actual-header-height', h + 'px');
|
|
1446
|
+
return;
|
|
1447
|
+
}
|
|
657
1448
|
}
|
|
658
|
-
|
|
1449
|
+
// Header pas encore rendu (ex: lazy Angular Material), on réessaie après le prochain frame
|
|
1450
|
+
this._ngZone.runOutsideAngular(() => requestAnimationFrame(() => measure()));
|
|
1451
|
+
};
|
|
1452
|
+
this._ngZone.runOutsideAngular(() => requestAnimationFrame(() => measure()));
|
|
659
1453
|
}
|
|
660
|
-
// ── Getters d'affichage
|
|
1454
|
+
// ── Getters d'affichage
|
|
661
1455
|
get title() {
|
|
662
1456
|
return this.resolvedConfig.title ?? '';
|
|
663
1457
|
}
|
|
@@ -668,60 +1462,48 @@ class TableComponent {
|
|
|
668
1462
|
return this.activeColumns;
|
|
669
1463
|
}
|
|
670
1464
|
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;
|
|
1465
|
+
get showViewButton() { return this._view.enabled || this.showExportButton || this.showPreferencesMenu; }
|
|
1466
|
+
get showSliceExpandBtn() { return this._showSlicePanel && this._slicePanelCollapsed && this._sliceShowToggle; }
|
|
1467
|
+
get showExportButton() {
|
|
1468
|
+
return this.resolvedConfig.export?.enabled === true;
|
|
677
1469
|
}
|
|
678
|
-
get
|
|
679
|
-
|
|
680
|
-
return 'Aucun';
|
|
681
|
-
const col = this.groupByColumns.find((c) => c.key === this.activeGroupByKey);
|
|
682
|
-
return col?.header || this.activeGroupByKey;
|
|
683
|
-
}
|
|
684
|
-
get totalColumnCount() {
|
|
685
|
-
return new Set((this.resolvedConfig.columns || []).map((c) => c.key)).size;
|
|
1470
|
+
get showPreferencesMenu() {
|
|
1471
|
+
return this.resolvedConfig.preferences?.enabled === true;
|
|
686
1472
|
}
|
|
687
|
-
get
|
|
688
|
-
|
|
1473
|
+
get hasSavedPreferences() { return this._hasSavedConfig; }
|
|
1474
|
+
get showToolbar() { return this.showSearchBar || this.showViewButton || this.showSliceExpandBtn; }
|
|
1475
|
+
expandSlicePanel() {
|
|
1476
|
+
this.slicePanelRef?.togglePanel();
|
|
689
1477
|
}
|
|
690
|
-
get
|
|
691
|
-
|
|
1478
|
+
get showFields() { return this._view.showFields; }
|
|
1479
|
+
get showGroupBySection() { return this._view.showGroupBySection; }
|
|
1480
|
+
get filteredRowCount() {
|
|
1481
|
+
return this.activeGroupByKey ? this._totalFilteredCount : this._allFilteredRows.length;
|
|
692
1482
|
}
|
|
1483
|
+
get activeGroupByLabel() { return this._view.activeGroupByLabel; }
|
|
1484
|
+
get totalColumnCount() { return this._view.totalColumnCount; }
|
|
1485
|
+
get activeSliceByLabel() { return this._view.sliceBy.activeLabel; }
|
|
1486
|
+
get activeDynamicSliceColumns() { return this._view.sliceBy.activeDynamicColumns; }
|
|
693
1487
|
removeDynamicSliceByKey(key) {
|
|
694
1488
|
this.slicePanelRef?.removeDynamicSliceByKey(key);
|
|
695
1489
|
}
|
|
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 ────────────────────────────────────────────────────
|
|
1490
|
+
get groupByColumns() { return this._view.groupByColumns; }
|
|
1491
|
+
colLabel(col) { return this._view.colLabel(col); }
|
|
1492
|
+
// ── Actions utilisateur
|
|
712
1493
|
onSearchChange() {
|
|
713
1494
|
this._preservePageIndex = 0;
|
|
714
1495
|
this.refreshViewModel();
|
|
715
1496
|
this.paginator?.firstPage();
|
|
1497
|
+
this.searchChange.emit(this.searchQuery);
|
|
716
1498
|
}
|
|
717
1499
|
onGroupByChange(key) {
|
|
718
|
-
this.
|
|
719
|
-
this.
|
|
720
|
-
this.
|
|
721
|
-
this.
|
|
722
|
-
this.groupPageSize = this.resolveDefaultGroupPageSize();
|
|
1500
|
+
this._view.setGroupBy(key);
|
|
1501
|
+
this._groupBy.setDefaultCollapsed(key !== null);
|
|
1502
|
+
this._groupBy.reset();
|
|
1503
|
+
this._groupBy.groupPageSize = this.resolveDefaultGroupPageSize();
|
|
723
1504
|
this.refreshViewModel();
|
|
724
1505
|
this.paginator?.firstPage();
|
|
1506
|
+
this.groupByChange.emit(key);
|
|
725
1507
|
if (key) {
|
|
726
1508
|
this.viewMenuTrigger?.closeMenu();
|
|
727
1509
|
const colDef = (this.resolvedConfig.columns || []).find(c => c.key === key);
|
|
@@ -730,113 +1512,43 @@ class TableComponent {
|
|
|
730
1512
|
}
|
|
731
1513
|
}
|
|
732
1514
|
else {
|
|
733
|
-
setTimeout(() => {
|
|
1515
|
+
this._ngZone.runOutsideAngular(() => setTimeout(() => {
|
|
734
1516
|
this.tableBodyScrollRef?.nativeElement.scrollTo({ top: 0, behavior: 'smooth' });
|
|
735
|
-
}, 0);
|
|
1517
|
+
}, 0));
|
|
736
1518
|
}
|
|
737
1519
|
}
|
|
738
1520
|
onGroupPageSizeChange(size) {
|
|
739
|
-
this.
|
|
740
|
-
this._groupPages.clear();
|
|
741
|
-
this.refreshViewModel();
|
|
1521
|
+
this._groupBy.onGroupPageSizeChange(Number(size));
|
|
742
1522
|
}
|
|
743
1523
|
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
|
-
}
|
|
1524
|
+
this._groupBy.onGroupSortToggle(dir, (key) => {
|
|
1525
|
+
this._ngZone.runOutsideAngular(() => setTimeout(() => this.scrollToGroupHeader(key), 0));
|
|
1526
|
+
});
|
|
755
1527
|
}
|
|
756
|
-
// ── Lazy loading
|
|
1528
|
+
// ── Lazy loading
|
|
757
1529
|
getLazyColumnStatus(key) {
|
|
758
|
-
return this.
|
|
1530
|
+
return this._lazy.getStatus(key);
|
|
759
1531
|
}
|
|
760
1532
|
getLazyRenderType(rowValue) {
|
|
761
|
-
|
|
762
|
-
return 'loading';
|
|
763
|
-
if (rowValue === '__lazy_error__')
|
|
764
|
-
return 'error';
|
|
765
|
-
return 'value';
|
|
1533
|
+
return this._lazy.getRenderType(rowValue);
|
|
766
1534
|
}
|
|
767
1535
|
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);
|
|
1536
|
+
this._lazy.retry(column, this._resolvedData, this._lazyCallbacks());
|
|
782
1537
|
}
|
|
783
1538
|
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
|
-
});
|
|
1539
|
+
this._lazy.fetch(column, this._resolvedData, this._lazyCallbacks());
|
|
1540
|
+
}
|
|
1541
|
+
_lazyCallbacks() {
|
|
1542
|
+
return {
|
|
1543
|
+
onRefresh: () => this.refreshViewModel(),
|
|
1544
|
+
onMarkForCheck: () => this._cdr.markForCheck(),
|
|
1545
|
+
};
|
|
825
1546
|
}
|
|
826
|
-
// ── Groupement
|
|
1547
|
+
// ── Groupement
|
|
827
1548
|
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();
|
|
1549
|
+
this._groupBy.toggleGroupCollapse(groupKey, (key) => {
|
|
1550
|
+
this._ngZone.runOutsideAngular(() => setTimeout(() => this.scrollToGroupHeader(key), 0));
|
|
1551
|
+
});
|
|
840
1552
|
}
|
|
841
1553
|
scrollToGroupHeader(groupKey) {
|
|
842
1554
|
const container = this.tableBodyScrollRef?.nativeElement;
|
|
@@ -853,63 +1565,86 @@ class TableComponent {
|
|
|
853
1565
|
container.scrollTo({ top: targetScrollTop, behavior: 'smooth' });
|
|
854
1566
|
}
|
|
855
1567
|
isGroupCollapsed(groupKey) {
|
|
856
|
-
return this.
|
|
1568
|
+
return this._groupBy.isGroupCollapsed(groupKey);
|
|
857
1569
|
}
|
|
858
1570
|
groupPageBack(groupKey) {
|
|
859
|
-
|
|
860
|
-
if (current > 0) {
|
|
861
|
-
this._groupPages.set(groupKey, current - 1);
|
|
862
|
-
this._projectCurrentPage();
|
|
863
|
-
this._cdr.markForCheck();
|
|
864
|
-
}
|
|
1571
|
+
this._groupBy.groupPageBack(groupKey);
|
|
865
1572
|
}
|
|
866
1573
|
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
|
-
}
|
|
1574
|
+
this._groupBy.groupPageForward(groupKey, totalCount);
|
|
874
1575
|
}
|
|
875
|
-
// ── Colonnes
|
|
1576
|
+
// ── Colonnes
|
|
876
1577
|
get hasAddableColumns() {
|
|
877
|
-
return this.menuBaseColumns.length > 0 || this.menuOptionalColumns.length > 0;
|
|
1578
|
+
return this._view.menuBaseColumns.length > 0 || this._view.menuOptionalColumns.length > 0;
|
|
878
1579
|
}
|
|
879
1580
|
get menuBaseColumns() {
|
|
880
|
-
return this.
|
|
1581
|
+
return this._view.menuBaseColumns;
|
|
881
1582
|
}
|
|
882
1583
|
get menuOptionalColumns() {
|
|
883
|
-
return this.
|
|
1584
|
+
return this._view.menuOptionalColumns;
|
|
884
1585
|
}
|
|
885
1586
|
get availableColumnsToAdd() {
|
|
886
1587
|
const activeKeys = new Set(this.activeColumns.map((column) => column.key));
|
|
887
1588
|
return (this.resolvedConfig.columns || []).filter((c) => c.optional && !activeKeys.has(c.key));
|
|
888
1589
|
}
|
|
889
1590
|
get allowColumnRemoval() {
|
|
890
|
-
return this.
|
|
1591
|
+
return this._view.allowColumnRemoval;
|
|
891
1592
|
}
|
|
892
|
-
// ── Slices
|
|
1593
|
+
// ── Slices
|
|
893
1594
|
get hasSliceConfig() {
|
|
894
1595
|
return (this.resolvedConfig.slices?.length ?? 0) > 0;
|
|
895
1596
|
}
|
|
896
1597
|
get showSlicePanel() {
|
|
897
|
-
return this.
|
|
1598
|
+
return this._showSlicePanel;
|
|
1599
|
+
}
|
|
1600
|
+
onSlicePanelCollapsedChange(collapsed) {
|
|
1601
|
+
this._slicePanelCollapsed = collapsed;
|
|
1602
|
+
this._cdr.markForCheck();
|
|
1603
|
+
}
|
|
1604
|
+
_recomputeShowSlicePanel() {
|
|
1605
|
+
const hasCfg = this._sliceConfigs.length > 0 || this._view.sliceBy.activeDynamicColumns.length > 0;
|
|
1606
|
+
const wasVisible = this._showSlicePanel;
|
|
1607
|
+
this._showSlicePanel = hasCfg && this._resolvedData.length > 0;
|
|
1608
|
+
// Le SlicePanelComponent vient d'être rendu pour la 1ère fois
|
|
1609
|
+
// (données arrivées) → appliquer les filtres en attente
|
|
1610
|
+
if (!wasVisible && this._showSlicePanel && this._pendingSliceFilters) {
|
|
1611
|
+
const filters = this._pendingSliceFilters;
|
|
1612
|
+
this._pendingSliceFilters = null;
|
|
1613
|
+
setTimeout(() => {
|
|
1614
|
+
this.slicePanelRef?.restoreFilters(filters);
|
|
1615
|
+
this._cdr.markForCheck();
|
|
1616
|
+
});
|
|
1617
|
+
}
|
|
898
1618
|
}
|
|
899
1619
|
get showAddSliceButton() {
|
|
900
|
-
return this.
|
|
1620
|
+
return this._view.showSliceBySection;
|
|
901
1621
|
}
|
|
902
1622
|
get staticSlicesForMenu() {
|
|
903
|
-
return this.
|
|
1623
|
+
return this._view.staticSlicesForMenu;
|
|
904
1624
|
}
|
|
905
1625
|
get availableColumnsForDynamicSlice() {
|
|
906
|
-
return this.
|
|
1626
|
+
return this._view.sliceBy.availableForDynamic;
|
|
907
1627
|
}
|
|
908
1628
|
isStaticSliceVisible(columnKey) {
|
|
909
|
-
return this.
|
|
1629
|
+
return !this._staticSliceHiddenKeys.has(columnKey);
|
|
910
1630
|
}
|
|
911
1631
|
toggleStaticSlice(columnKey) {
|
|
912
|
-
this.
|
|
1632
|
+
const next = new Set(this._staticSliceHiddenKeys);
|
|
1633
|
+
if (next.has(columnKey))
|
|
1634
|
+
next.delete(columnKey);
|
|
1635
|
+
else
|
|
1636
|
+
next.add(columnKey);
|
|
1637
|
+
this._staticSliceHiddenKeys = next;
|
|
1638
|
+
this._view.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
|
|
1639
|
+
this._sliceConfigs = this._computeSliceConfigs();
|
|
1640
|
+
// Si une slice vient d'être cachée, réinitialise le filtre actif pour éviter
|
|
1641
|
+
// un filtre orphelin (la slice n'est plus visible mais son filtre resterait actif)
|
|
1642
|
+
if (next.has(columnKey)) {
|
|
1643
|
+
this.activeSliceFilter = () => true;
|
|
1644
|
+
}
|
|
1645
|
+
this._recomputeShowSlicePanel();
|
|
1646
|
+
this._scheduleRender();
|
|
1647
|
+
this._cdr.markForCheck();
|
|
913
1648
|
}
|
|
914
1649
|
addDynamicSlice(column) {
|
|
915
1650
|
this.slicePanelRef?.addDynamicSlice(column);
|
|
@@ -921,7 +1656,7 @@ class TableComponent {
|
|
|
921
1656
|
return this._allDynamicSliceColumns;
|
|
922
1657
|
}
|
|
923
1658
|
isDynamicSliceActive(key) {
|
|
924
|
-
return this.
|
|
1659
|
+
return this._view.isDynamicSliceActive(key);
|
|
925
1660
|
}
|
|
926
1661
|
toggleDynamicSlice(col) {
|
|
927
1662
|
if (this.isDynamicSliceActive(col.key)) {
|
|
@@ -938,34 +1673,22 @@ class TableComponent {
|
|
|
938
1673
|
this.paginator?.firstPage();
|
|
939
1674
|
}
|
|
940
1675
|
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();
|
|
1676
|
+
this._view.onDynamicSliceKeysChange(keys);
|
|
1677
|
+
this._recomputeShowSlicePanel();
|
|
956
1678
|
this._cdr.markForCheck();
|
|
957
1679
|
}
|
|
958
1680
|
get emptyStateLabel() {
|
|
959
|
-
return this.resolvedConfig.labels?.empty ||
|
|
1681
|
+
return this.resolvedConfig.labels?.empty || this.i18n.emptyState;
|
|
960
1682
|
}
|
|
961
1683
|
get loadingStateLabel() {
|
|
962
|
-
return this.resolvedConfig.labels?.loading ||
|
|
1684
|
+
return this.resolvedConfig.labels?.loading || this.i18n.loadingState;
|
|
963
1685
|
}
|
|
964
1686
|
get effectiveIsLoading() {
|
|
965
|
-
return this.isLoading
|
|
1687
|
+
return this.isLoading;
|
|
966
1688
|
}
|
|
967
1689
|
get isEmpty() {
|
|
968
|
-
|
|
1690
|
+
// _pendingRender : le rendu interne n'est pas encore terminé → pas de message vide prématuré
|
|
1691
|
+
return !this.isLoading && this._pendingRender === null && this._allFilteredRows.length === 0;
|
|
969
1692
|
}
|
|
970
1693
|
get showPagination() {
|
|
971
1694
|
if (this.activeGroupByKey !== null)
|
|
@@ -973,35 +1696,16 @@ class TableComponent {
|
|
|
973
1696
|
return this.resolvedConfig.pagination?.enabled !== false;
|
|
974
1697
|
}
|
|
975
1698
|
get isColumnDragDropEnabled() {
|
|
976
|
-
return this.
|
|
1699
|
+
return this._view.isColumnDragDropEnabled;
|
|
977
1700
|
}
|
|
978
1701
|
get totalRows() {
|
|
979
1702
|
return this.activeGroupByKey
|
|
980
|
-
? this.tableDataSource.data.filter((r) => !r
|
|
1703
|
+
? this.tableDataSource.data.filter((r) => !r[GROUP_HEADER_MARKER]).length
|
|
981
1704
|
: this._allFilteredRows.length;
|
|
982
1705
|
}
|
|
983
|
-
// ── Gestion des colonnes
|
|
1706
|
+
// ── Gestion des colonnes
|
|
984
1707
|
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
|
-
}
|
|
1708
|
+
this._view.addColumn(column);
|
|
1005
1709
|
this._preservePageIndex = this.paginator?.pageIndex ?? null;
|
|
1006
1710
|
this.refreshViewModel();
|
|
1007
1711
|
if (column.lazy) {
|
|
@@ -1011,70 +1715,94 @@ class TableComponent {
|
|
|
1011
1715
|
this.columnAdded.emit(column);
|
|
1012
1716
|
}
|
|
1013
1717
|
onColumnVisibilityChange(column, checked) {
|
|
1014
|
-
const isVisible = this.isColumnVisible(column.key);
|
|
1718
|
+
const isVisible = this._view.isColumnVisible(column.key);
|
|
1015
1719
|
if (checked && !isVisible) {
|
|
1016
1720
|
this.onAddColumn(column);
|
|
1017
1721
|
return;
|
|
1018
1722
|
}
|
|
1019
1723
|
if (!checked && isVisible) {
|
|
1020
|
-
|
|
1724
|
+
const removed = this._view.removeColumn(column.key);
|
|
1725
|
+
if (!removed)
|
|
1021
1726
|
return;
|
|
1022
|
-
}
|
|
1023
|
-
if (this.activeColumns.length <= 1) {
|
|
1024
|
-
return;
|
|
1025
|
-
}
|
|
1026
|
-
this.userCustomizedColumns = true;
|
|
1027
|
-
this.activeColumns = this.activeColumns.filter((item) => item.key !== column.key);
|
|
1028
1727
|
this._preservePageIndex = this.paginator?.pageIndex ?? null;
|
|
1029
1728
|
this.refreshViewModel();
|
|
1030
1729
|
this.columnRemoved.emit(column);
|
|
1031
1730
|
}
|
|
1032
1731
|
}
|
|
1033
1732
|
isColumnVisible(columnKey) {
|
|
1034
|
-
return this.activeColumns.some(
|
|
1733
|
+
return this.activeColumns.some(c => c.key === columnKey);
|
|
1035
1734
|
}
|
|
1036
1735
|
onRemoveColumn(columnKey, event) {
|
|
1037
1736
|
event?.stopPropagation();
|
|
1038
|
-
|
|
1737
|
+
const column = this.activeColumns.find(item => item.key === columnKey);
|
|
1738
|
+
if (!column)
|
|
1039
1739
|
return;
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
if (!column || !this.canRemoveColumn(column)) {
|
|
1740
|
+
const removed = this._view.removeColumn(columnKey);
|
|
1741
|
+
if (!removed)
|
|
1043
1742
|
return;
|
|
1044
|
-
}
|
|
1045
|
-
this.userCustomizedColumns = true;
|
|
1046
|
-
this.activeColumns = this.activeColumns.filter((item) => item.key !== columnKey);
|
|
1047
1743
|
this.refreshViewModel();
|
|
1048
1744
|
this.columnRemoved.emit(column);
|
|
1049
1745
|
}
|
|
1050
|
-
// ── Lignes
|
|
1746
|
+
// ── Lignes
|
|
1747
|
+
_rebuildCellDefMap() {
|
|
1748
|
+
this._cellDefMap = new Map((this._cellDefs ?? []).map(d => [d.columnKey, d.template]));
|
|
1749
|
+
}
|
|
1051
1750
|
getCellTemplate(key) {
|
|
1052
|
-
return this.
|
|
1751
|
+
return this._cellDefMap.get(key) ?? null;
|
|
1053
1752
|
}
|
|
1054
1753
|
get hasCustomCellDefs() {
|
|
1055
|
-
return
|
|
1754
|
+
return this._cellDefMap.size > 0;
|
|
1056
1755
|
}
|
|
1057
1756
|
get hasRowClickHandler() {
|
|
1058
|
-
return this.rowSelected.observed || !!this.
|
|
1757
|
+
return this.rowSelected.observed || !!this.resolvedConfig.onRowSelected;
|
|
1758
|
+
}
|
|
1759
|
+
/** Largeur minimale du tableau en px, calculée depuis les largeurs explicites des colonnes actives.
|
|
1760
|
+
* Retourne null si aucune colonne n'a de largeur définie (le tableau remplit alors 100% du conteneur).
|
|
1761
|
+
* Seules les valeurs explicitement en `px` sont sommables ; les pourcentages ou autres unités sont ignorés. */
|
|
1762
|
+
get tableMinWidth() {
|
|
1763
|
+
const hasResized = Object.keys(this._columnWidths).length > 0;
|
|
1764
|
+
const hasSomeExplicitWidth = hasResized || this.activeColumns.some(c => c.width);
|
|
1765
|
+
if (!hasSomeExplicitWidth)
|
|
1766
|
+
return null;
|
|
1767
|
+
let sum = 0;
|
|
1768
|
+
for (const col of this.activeColumns) {
|
|
1769
|
+
if (this._columnWidths[col.key] != null) {
|
|
1770
|
+
sum += this._columnWidths[col.key];
|
|
1771
|
+
}
|
|
1772
|
+
else if (col.width) {
|
|
1773
|
+
const isPx = col.width.trim().toLowerCase().endsWith('px');
|
|
1774
|
+
const px = Number.parseFloat(col.width);
|
|
1775
|
+
if (isPx && !Number.isNaN(px)) {
|
|
1776
|
+
sum += px;
|
|
1777
|
+
}
|
|
1778
|
+
else {
|
|
1779
|
+
sum += 150;
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
else {
|
|
1783
|
+
sum += 120; // largeur minimale estimée pour une colonne sans largeur explicite
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
return sum + 'px';
|
|
1059
1787
|
}
|
|
1060
1788
|
getRowClass(row) {
|
|
1061
1789
|
const fn = this.resolvedConfig.rowClass;
|
|
1062
|
-
if (!fn || row?.
|
|
1790
|
+
if (!fn || row?.[GROUP_HEADER_MARKER])
|
|
1063
1791
|
return {};
|
|
1064
|
-
return fn(row
|
|
1792
|
+
return fn(row[ROW_RAW_KEY] ?? row, row[ROW_INDEX_KEY] ?? 0);
|
|
1065
1793
|
}
|
|
1066
1794
|
onRowClick(row, event) {
|
|
1067
|
-
if (row?.
|
|
1795
|
+
if (row?.[GROUP_HEADER_MARKER])
|
|
1068
1796
|
return;
|
|
1069
1797
|
if (!this.hasRowClickHandler)
|
|
1070
1798
|
return;
|
|
1071
|
-
const resolved = (row?.
|
|
1799
|
+
const resolved = (row?.[ROW_RAW_KEY] ?? row);
|
|
1800
|
+
this.resolvedConfig.onRowSelected?.(resolved, event);
|
|
1072
1801
|
this.rowSelected.emit(resolved);
|
|
1073
|
-
this.config?.onRowSelected?.(resolved, event);
|
|
1074
1802
|
}
|
|
1075
|
-
// ── Drag & drop
|
|
1803
|
+
// ── Drag & drop
|
|
1076
1804
|
onHeaderDragStart(columnKey, event) {
|
|
1077
|
-
if (!this.isColumnDragDropEnabled) {
|
|
1805
|
+
if (!this.isColumnDragDropEnabled && !this._editMode) {
|
|
1078
1806
|
return;
|
|
1079
1807
|
}
|
|
1080
1808
|
this.draggedColumnKey = columnKey;
|
|
@@ -1085,28 +1813,22 @@ class TableComponent {
|
|
|
1085
1813
|
}
|
|
1086
1814
|
}
|
|
1087
1815
|
onHeaderDragOver(columnKey, event) {
|
|
1088
|
-
if (!this.isColumnDragDropEnabled || !this.draggedColumnKey) {
|
|
1816
|
+
if ((!this.isColumnDragDropEnabled && !this._editMode) || !this.draggedColumnKey) {
|
|
1089
1817
|
return;
|
|
1090
1818
|
}
|
|
1091
1819
|
event.preventDefault();
|
|
1092
1820
|
this.dragOverColumnKey = columnKey;
|
|
1093
1821
|
}
|
|
1094
1822
|
onHeaderDrop(targetColumnKey, event) {
|
|
1095
|
-
if (!this.isColumnDragDropEnabled || !this.draggedColumnKey) {
|
|
1823
|
+
if ((!this.isColumnDragDropEnabled && !this._editMode) || !this.draggedColumnKey) {
|
|
1096
1824
|
return;
|
|
1097
1825
|
}
|
|
1098
1826
|
event.preventDefault();
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
if (previousIndex === -1 || currentIndex === -1 || previousIndex === currentIndex) {
|
|
1827
|
+
const moved = this._view.reorderColumns(this.draggedColumnKey, targetColumnKey);
|
|
1828
|
+
if (!moved) {
|
|
1102
1829
|
this.onHeaderDragEnd();
|
|
1103
1830
|
return;
|
|
1104
1831
|
}
|
|
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
1832
|
this.refreshViewModel();
|
|
1111
1833
|
this.onHeaderDragEnd();
|
|
1112
1834
|
}
|
|
@@ -1117,8 +1839,190 @@ class TableComponent {
|
|
|
1117
1839
|
isHeaderDropTarget(columnKey) {
|
|
1118
1840
|
return !!this.draggedColumnKey && this.dragOverColumnKey === columnKey && this.draggedColumnKey !== columnKey;
|
|
1119
1841
|
}
|
|
1842
|
+
// ── Préférences : mode édition
|
|
1843
|
+
enterEditMode() {
|
|
1844
|
+
this._editMode = true;
|
|
1845
|
+
this._preferencesMessage = null;
|
|
1846
|
+
this._cdr.markForCheck();
|
|
1847
|
+
}
|
|
1848
|
+
confirmEditMode() {
|
|
1849
|
+
this._editMode = false;
|
|
1850
|
+
this._cdr.markForCheck();
|
|
1851
|
+
}
|
|
1852
|
+
/** Démarre le resize d'une colonne (mousedown sur la poignée droite). */
|
|
1853
|
+
onResizeStart(columnKey, event) {
|
|
1854
|
+
if (!this._editMode)
|
|
1855
|
+
return;
|
|
1856
|
+
event.preventDefault();
|
|
1857
|
+
event.stopPropagation();
|
|
1858
|
+
const th = event.target.closest('th');
|
|
1859
|
+
if (!th)
|
|
1860
|
+
return;
|
|
1861
|
+
const scrollEl = this.tableBodyScrollRef?.nativeElement;
|
|
1862
|
+
if (scrollEl) {
|
|
1863
|
+
const allTh = scrollEl.querySelectorAll('th.mat-mdc-header-cell');
|
|
1864
|
+
allTh.forEach(thEl => {
|
|
1865
|
+
const key = thEl.getAttribute('mat-column-header') || thEl.getAttribute('mat-sort-header') || '';
|
|
1866
|
+
const colClass = Array.from(thEl.classList).find(c => c.startsWith('mat-column-'));
|
|
1867
|
+
const colKey = colClass ? colClass.replace('mat-column-', '') : null;
|
|
1868
|
+
if (colKey && this._columnWidths[colKey] == null) {
|
|
1869
|
+
this._columnWidths = { ...this._columnWidths, [colKey]: thEl.offsetWidth };
|
|
1870
|
+
}
|
|
1871
|
+
});
|
|
1872
|
+
}
|
|
1873
|
+
this._resizeState = { key: columnKey, startX: event.clientX, startWidth: th.offsetWidth };
|
|
1874
|
+
const onMove = (e) => {
|
|
1875
|
+
if (!this._resizeState) {
|
|
1876
|
+
window.removeEventListener('mousemove', onMove);
|
|
1877
|
+
window.removeEventListener('mouseup', onUp);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
const delta = e.clientX - this._resizeState.startX;
|
|
1881
|
+
const newWidth = Math.max(60, this._resizeState.startWidth + delta);
|
|
1882
|
+
this._columnWidths = { ...this._columnWidths, [this._resizeState.key]: newWidth };
|
|
1883
|
+
// detectChanges() pour forcer la mise à jour immédiate en mode OnPush
|
|
1884
|
+
// (les listeners window tournent hors zone Angular avec runOutsideAngular)
|
|
1885
|
+
this._cdr.detectChanges();
|
|
1886
|
+
};
|
|
1887
|
+
const onUp = () => {
|
|
1888
|
+
this._resizeState = null;
|
|
1889
|
+
window.removeEventListener('mousemove', onMove);
|
|
1890
|
+
window.removeEventListener('mouseup', onUp);
|
|
1891
|
+
};
|
|
1892
|
+
// Listeners hors zone Angular pour éviter des cycles CD à chaque mousemove
|
|
1893
|
+
this._ngZone.runOutsideAngular(() => {
|
|
1894
|
+
window.addEventListener('mousemove', onMove);
|
|
1895
|
+
window.addEventListener('mouseup', onUp);
|
|
1896
|
+
});
|
|
1897
|
+
}
|
|
1898
|
+
/** Retourne la largeur CSS d'une colonne (px si personnalisée, sinon la valeur de config ou auto). */
|
|
1899
|
+
getColumnWidth(key) {
|
|
1900
|
+
if (this._columnWidths[key] != null)
|
|
1901
|
+
return this._columnWidths[key] + 'px';
|
|
1902
|
+
const col = this._columnMap.get(key);
|
|
1903
|
+
return col?.width ?? 'auto';
|
|
1904
|
+
}
|
|
1905
|
+
// ── Préférences : save / clear
|
|
1906
|
+
savePreferences() {
|
|
1907
|
+
if (!this._preferencesManager)
|
|
1908
|
+
return;
|
|
1909
|
+
const dynamicSliceKeys = this._view.sliceBy.activeDynamicColumns.map(c => c.key);
|
|
1910
|
+
const config = {
|
|
1911
|
+
search: this.searchQuery || undefined,
|
|
1912
|
+
groupBy: this.activeGroupByKey,
|
|
1913
|
+
columnOrder: this.activeColumns.map(c => c.key),
|
|
1914
|
+
visibleColumns: this.activeColumns.map(c => c.key),
|
|
1915
|
+
columnWidths: Object.keys(this._columnWidths).length ? { ...this._columnWidths } : undefined,
|
|
1916
|
+
slicePanelCollapsed: this._slicePanelCollapsed,
|
|
1917
|
+
hiddenSliceKeys: [...this._staticSliceHiddenKeys],
|
|
1918
|
+
dynamicSliceKeys: dynamicSliceKeys.length ? dynamicSliceKeys : undefined,
|
|
1919
|
+
sliceFilters: this.slicePanelRef ? this.slicePanelRef.getActiveFilters() : undefined,
|
|
1920
|
+
};
|
|
1921
|
+
this._preferencesManager.save(config);
|
|
1922
|
+
this._hasSavedConfig = true;
|
|
1923
|
+
this._showPreferencesMessage(this.i18n.preferencesSavedMessage);
|
|
1924
|
+
}
|
|
1925
|
+
clearPreferences() {
|
|
1926
|
+
if (!this._preferencesManager)
|
|
1927
|
+
return;
|
|
1928
|
+
this._preferencesManager.clear();
|
|
1929
|
+
this._hasSavedConfig = false;
|
|
1930
|
+
// Capturer les dynamic slices actives AVANT le reset (resetToDefaults() les vide)
|
|
1931
|
+
const dynamicSlicesToRemove = this._view.sliceBy.activeDynamicColumns.slice();
|
|
1932
|
+
// Reset complet de l'état vers les valeurs par défaut de la config
|
|
1933
|
+
this.searchQuery = '';
|
|
1934
|
+
this._columnWidths = {};
|
|
1935
|
+
this._slicePanelCollapsed = false;
|
|
1936
|
+
this._groupBy.reset();
|
|
1937
|
+
this._groupBy.setDefaultCollapsed(false);
|
|
1938
|
+
// Slices statiques : revenir aux seules celles cachées par la config (pas par l'utilisateur)
|
|
1939
|
+
this._staticSliceHiddenKeys = new Set(this._configHiddenKeys);
|
|
1940
|
+
this._view.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
|
|
1941
|
+
// Colonnes et groupBy : reset via la facade (retire les optionnelles, remet l'ordre config)
|
|
1942
|
+
this._view.resetToDefaults();
|
|
1943
|
+
this._defaultGroupByApplied = false;
|
|
1944
|
+
// Dynamic slices : les retirer du slice panel avec la liste capturée avant le reset
|
|
1945
|
+
dynamicSlicesToRemove.forEach(col => {
|
|
1946
|
+
this.slicePanelRef?.removeDynamicSliceByKey(col.key);
|
|
1947
|
+
});
|
|
1948
|
+
// Filtre de slice actif
|
|
1949
|
+
this.activeSliceFilter = () => true;
|
|
1950
|
+
if (this.slicePanelRef)
|
|
1951
|
+
this.slicePanelRef.restoreFilters({});
|
|
1952
|
+
// Rafraîchit toute la vue
|
|
1953
|
+
this.activeColumns = this._view.fields.activeColumns;
|
|
1954
|
+
this.renderedColumns = this.activeColumns.map(c => c.key);
|
|
1955
|
+
this._pendingDynamicSliceKeys = null;
|
|
1956
|
+
this._pendingSliceFilters = null;
|
|
1957
|
+
this.refreshViewModel();
|
|
1958
|
+
this._showPreferencesMessage(this.i18n.preferencesClearedMessage);
|
|
1959
|
+
}
|
|
1960
|
+
_showPreferencesMessage(msg) {
|
|
1961
|
+
if (this._preferencesMessageTimer !== null)
|
|
1962
|
+
clearTimeout(this._preferencesMessageTimer);
|
|
1963
|
+
this._preferencesMessage = msg;
|
|
1964
|
+
this._cdr.markForCheck();
|
|
1965
|
+
this._preferencesMessageTimer = setTimeout(() => {
|
|
1966
|
+
this._preferencesMessageTimer = null;
|
|
1967
|
+
this._preferencesMessage = null;
|
|
1968
|
+
this._cdr.markForCheck();
|
|
1969
|
+
}, 4000);
|
|
1970
|
+
}
|
|
1971
|
+
_applyPreferences(saved) {
|
|
1972
|
+
if (!saved)
|
|
1973
|
+
return;
|
|
1974
|
+
// Recherche
|
|
1975
|
+
if (saved.search)
|
|
1976
|
+
this.searchQuery = saved.search;
|
|
1977
|
+
// Largeurs colonnes
|
|
1978
|
+
if (saved.columnWidths)
|
|
1979
|
+
this._columnWidths = { ...saved.columnWidths };
|
|
1980
|
+
// Group by
|
|
1981
|
+
if (saved.groupBy !== undefined) {
|
|
1982
|
+
this._view.setGroupBy(saved.groupBy);
|
|
1983
|
+
if (saved.groupBy) {
|
|
1984
|
+
this._groupBy.setDefaultCollapsed(true);
|
|
1985
|
+
this._groupBy.reset();
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
// État panneau slice
|
|
1989
|
+
if (saved.slicePanelCollapsed !== undefined)
|
|
1990
|
+
this._slicePanelCollapsed = saved.slicePanelCollapsed;
|
|
1991
|
+
// Slices statiques cachées — on utilise la valeur exacte sauvegardée (pas les defaults de config)
|
|
1992
|
+
if (saved.hiddenSliceKeys !== undefined) {
|
|
1993
|
+
this._staticSliceHiddenKeys = new Set(saved.hiddenSliceKeys);
|
|
1994
|
+
this._view.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
|
|
1995
|
+
}
|
|
1996
|
+
// Colonnes visibles + ordre — reconstruit activeColumns en une seule passe.
|
|
1997
|
+
// On part de saved.columnOrder (ordre sauvegardé) si disponible, sinon saved.visibleColumns.
|
|
1998
|
+
// Cela gère à la fois : colonnes optionnelles ajoutées, colonnes retirées, et réordonnancement.
|
|
1999
|
+
const targetKeys = saved.columnOrder ?? saved.visibleColumns;
|
|
2000
|
+
if (targetKeys?.length) {
|
|
2001
|
+
const allByKey = new Map((this.resolvedConfig.columns ?? []).map(c => [c.key, c]));
|
|
2002
|
+
// Colonnes dans l'ordre sauvegardé (on ne garde que celles connues de la config)
|
|
2003
|
+
const ordered = targetKeys
|
|
2004
|
+
.map(key => allByKey.get(key))
|
|
2005
|
+
.filter((c) => c !== undefined);
|
|
2006
|
+
// Colonnes actives actuelllement non présentes dans targetKeys (non-optional passées sous silence)
|
|
2007
|
+
// → on les exclut : l'utilisateur avait explicitement choisi ce sous-ensemble
|
|
2008
|
+
if (ordered.length > 0) {
|
|
2009
|
+
this._view.setActiveColumns(ordered);
|
|
2010
|
+
}
|
|
2011
|
+
}
|
|
2012
|
+
// Dynamic slices — différé : slicePanelRef pas encore disponible au moment de ngOnChanges
|
|
2013
|
+
if (saved.dynamicSliceKeys?.length) {
|
|
2014
|
+
this._pendingDynamicSliceKeys = saved.dynamicSliceKeys;
|
|
2015
|
+
}
|
|
2016
|
+
// Filtres actifs du slice panel
|
|
2017
|
+
// Filtres actifs du slice panel — toujours stockés en pending :
|
|
2018
|
+
// appliqués par _applyPendingDynamicSliceKeys (si dynamic slices) ou
|
|
2019
|
+
// par _recomputeShowSlicePanel (quand les données arrivent et rendent le panel visible)
|
|
2020
|
+
if (saved.sliceFilters && Object.keys(saved.sliceFilters).length) {
|
|
2021
|
+
this._pendingSliceFilters = saved.sliceFilters;
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
1120
2024
|
canRemoveColumn(column) {
|
|
1121
|
-
return (this.allowColumnRemoval &&
|
|
2025
|
+
return (this._view.allowColumnRemoval &&
|
|
1122
2026
|
column.removable !== false &&
|
|
1123
2027
|
this.activeColumns.length > 1);
|
|
1124
2028
|
}
|
|
@@ -1131,20 +2035,47 @@ class TableComponent {
|
|
|
1131
2035
|
}
|
|
1132
2036
|
return String(value);
|
|
1133
2037
|
}
|
|
1134
|
-
// ──
|
|
2038
|
+
// ── Export CSV
|
|
2039
|
+
onExport() {
|
|
2040
|
+
this._export.export();
|
|
2041
|
+
}
|
|
2042
|
+
// ── Pipeline de données (privé)
|
|
1135
2043
|
refreshViewModel() {
|
|
1136
2044
|
this._resolvedData = this.resolveData();
|
|
1137
2045
|
this.resolvedConfig = this.buildEffectiveConfig();
|
|
1138
2046
|
this._columnMap = new Map((this.resolvedConfig.columns || []).map(c => [c.key, c]));
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
this.
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
2047
|
+
// ── Initialiser le gestionnaire de préférences si l'option est activée
|
|
2048
|
+
const prefsId = this.resolvedConfig.preferences?.enabled
|
|
2049
|
+
? this.resolvedConfig.preferences.tableId
|
|
2050
|
+
: null;
|
|
2051
|
+
const currentId = prefsId ?? null;
|
|
2052
|
+
// Préférences à appliquer après _view.update() (quand activeColumns est déjà peuplé)
|
|
2053
|
+
let pendingPrefs = null;
|
|
2054
|
+
if (currentId && this._preferencesManager?.key !== ('jqt_prefs_' + currentId)) {
|
|
2055
|
+
this._preferencesManager = new TablePreferencesManager(currentId);
|
|
2056
|
+
this._hasSavedConfig = this._preferencesManager.hasSaved();
|
|
2057
|
+
pendingPrefs = this._preferencesManager.load();
|
|
2058
|
+
}
|
|
2059
|
+
else if (!currentId) {
|
|
2060
|
+
this._preferencesManager = null;
|
|
2061
|
+
this._hasSavedConfig = false;
|
|
2062
|
+
}
|
|
2063
|
+
// Délègue à la facade : mise à jour de l'état View (colonnes, groupBy meta, sliceBy meta)
|
|
2064
|
+
this._view.update({
|
|
2065
|
+
config: this.resolvedConfig.view,
|
|
2066
|
+
columns: this.resolvedConfig.columns ?? [],
|
|
2067
|
+
sliceConfigs: this.resolvedConfig.slices ?? [],
|
|
2068
|
+
defaultGroupBy: this.resolvedConfig.defaultGroupBy,
|
|
2069
|
+
});
|
|
2070
|
+
if (!pendingPrefs && !this._defaultGroupByApplied && !this._view.groupBy.userCustomized && this._view.groupBy.activeKey != null) {
|
|
2071
|
+
this._defaultGroupByApplied = true;
|
|
2072
|
+
this._groupBy.setDefaultCollapsed(true);
|
|
2073
|
+
this._groupBy.reset();
|
|
2074
|
+
this._groupBy.groupPageSize = this.resolveDefaultGroupPageSize();
|
|
2075
|
+
}
|
|
2076
|
+
// Synchronise la propriété stable (évite NG0100 — la facade est source de vérité)
|
|
2077
|
+
this.activeColumns = this._view.fields.activeColumns;
|
|
2078
|
+
this.renderedColumns = this.activeColumns.map(c => c.key);
|
|
1148
2079
|
this.pageSize = this.paginator?.pageSize || this.resolvePageSize();
|
|
1149
2080
|
this.pageSizeOptions = this.resolvePageSizeOptions();
|
|
1150
2081
|
this.pageSizeOptionsGroupBy = this.resolvePageSizeOptionsGroupBy();
|
|
@@ -1153,72 +2084,100 @@ class TableComponent {
|
|
|
1153
2084
|
}
|
|
1154
2085
|
this.attachPaginator();
|
|
1155
2086
|
this.attachSort();
|
|
1156
|
-
//
|
|
1157
|
-
|
|
2087
|
+
// Réinitialise _staticSliceHiddenKeys seulement si les clés hidden de la config ont vraiment changé
|
|
2088
|
+
// ET qu'il n'y a pas de préférences sauvegardées (qui géreront hiddenSliceKeys elles-mêmes).
|
|
2089
|
+
const newConfigHidden = new Set((this.resolvedConfig.slices ?? []).filter(s => s.hidden && s.columnKey).map(s => s.columnKey));
|
|
2090
|
+
if (!TableComponent._setsEqual(newConfigHidden, this._configHiddenKeys)) {
|
|
2091
|
+
this._configHiddenKeys = newConfigHidden;
|
|
2092
|
+
// N'écraser _staticSliceHiddenKeys que si on n'est PAS en train d'appliquer des prefs
|
|
2093
|
+
// (pendingPrefs les gèrera lui-même via _applyPreferences → hiddenSliceKeys)
|
|
2094
|
+
if (!pendingPrefs) {
|
|
2095
|
+
this._staticSliceHiddenKeys = new Set(newConfigHidden);
|
|
2096
|
+
this._view.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
// Applique les préférences ICI, APRÈS _view.update() ET après _configHiddenKeys mis à jour,
|
|
2100
|
+
// mais avant _computeSliceConfigs() pour que hiddenSliceKeys soit déjà restauré.
|
|
2101
|
+
if (pendingPrefs) {
|
|
2102
|
+
this._applyPreferences(pendingPrefs);
|
|
2103
|
+
}
|
|
2104
|
+
// Propriétés stables pour les bindings du template
|
|
2105
|
+
// On recalcule _sliceConfigs en reflétant l'état courant des slices cachées,
|
|
2106
|
+
// pour que SlicePanelComponent.ngOnInit reçoive la bonne valeur de `hidden`
|
|
2107
|
+
// même quand le composant est créé pour la 1ère fois après un toggle.
|
|
2108
|
+
this._sliceConfigs = this._computeSliceConfigs();
|
|
1158
2109
|
this._sliceColumns = this.resolvedConfig.columns ?? [];
|
|
1159
2110
|
this._sliceData = this._resolvedData;
|
|
1160
2111
|
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
|
-
});
|
|
2112
|
+
this._recomputeShowSlicePanel();
|
|
1167
2113
|
if (!this._activeSort.active && this.resolvedConfig.defaultSort) {
|
|
1168
2114
|
this._matSortActive = this.resolvedConfig.defaultSort.active;
|
|
1169
2115
|
this._matSortDirection = this.resolvedConfig.defaultSort.direction;
|
|
1170
2116
|
}
|
|
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();
|
|
2117
|
+
this._buildSliceByMenuItems();
|
|
1192
2118
|
this._scheduleRender();
|
|
1193
2119
|
}
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
2120
|
+
/** Retourne uniquement les slices visibles (non cachées par _staticSliceHiddenKeys).
|
|
2121
|
+
* Le SlicePanelComponent affiche tout ce qu'il reçoit — c'est ici que se fait le filtrage. */
|
|
2122
|
+
_computeSliceConfigs() {
|
|
2123
|
+
return (this.resolvedConfig.slices ?? []).filter(s => !s.columnKey || !this._staticSliceHiddenKeys.has(s.columnKey));
|
|
2124
|
+
}
|
|
2125
|
+
static _setsEqual(a, b) {
|
|
2126
|
+
if (a.size !== b.size)
|
|
2127
|
+
return false;
|
|
2128
|
+
for (const v of a)
|
|
2129
|
+
if (!b.has(v))
|
|
2130
|
+
return false;
|
|
2131
|
+
return true;
|
|
2132
|
+
}
|
|
2133
|
+
_buildSliceByMenuItems() {
|
|
2134
|
+
const staticSliceMap = new Map(this._staticSlicesForMenu.map(s => [s.key, s]));
|
|
2135
|
+
const dynamicSliceSet = new Set(this._allDynamicSliceColumns.map(c => c.key));
|
|
2136
|
+
const seen = new Set();
|
|
2137
|
+
const items = [];
|
|
2138
|
+
for (const col of this.resolvedConfig.columns ?? []) {
|
|
2139
|
+
if (seen.has(col.key))
|
|
2140
|
+
continue;
|
|
2141
|
+
if (staticSliceMap.has(col.key)) {
|
|
2142
|
+
const s = staticSliceMap.get(col.key);
|
|
2143
|
+
items.push({ isStatic: true, key: s.key, title: s.title, icon: s.icon });
|
|
2144
|
+
seen.add(col.key);
|
|
2145
|
+
}
|
|
2146
|
+
else if (dynamicSliceSet.has(col.key)) {
|
|
2147
|
+
items.push({ isStatic: false, key: col.key, title: this.colLabel(col), icon: col.icon, col });
|
|
2148
|
+
seen.add(col.key);
|
|
2149
|
+
}
|
|
1201
2150
|
}
|
|
1202
|
-
|
|
1203
|
-
if (
|
|
1204
|
-
|
|
1205
|
-
this._activeSliceByLabel = first?.title || first?.columnKey || '1';
|
|
1206
|
-
return;
|
|
2151
|
+
for (const s of this._staticSlicesForMenu) {
|
|
2152
|
+
if (!seen.has(s.key)) {
|
|
2153
|
+
items.push({ isStatic: true, key: s.key, title: s.title, icon: s.icon });
|
|
1207
2154
|
}
|
|
1208
|
-
this._activeSliceByLabel = this._activeDynamicSliceColumns[0]?.header || '1';
|
|
1209
|
-
return;
|
|
1210
2155
|
}
|
|
1211
|
-
this.
|
|
2156
|
+
this._sliceByMenuItems = items;
|
|
2157
|
+
}
|
|
2158
|
+
get sliceByMenuItems() { return this._sliceByMenuItems; }
|
|
2159
|
+
onSliceMenuItemClick(item) {
|
|
2160
|
+
if (item.isStatic) {
|
|
2161
|
+
this.toggleStaticSlice(item.key);
|
|
2162
|
+
}
|
|
2163
|
+
else if (item.col) {
|
|
2164
|
+
this.toggleDynamicSlice(item.col);
|
|
2165
|
+
}
|
|
2166
|
+
}
|
|
2167
|
+
isSliceMenuItemActive(item) {
|
|
2168
|
+
return item.isStatic ? this.isStaticSliceVisible(item.key) : this.isDynamicSliceActive(item.key);
|
|
1212
2169
|
}
|
|
1213
2170
|
_scheduleRender() {
|
|
1214
2171
|
if (this._pendingRender !== null) {
|
|
1215
2172
|
clearTimeout(this._pendingRender);
|
|
1216
2173
|
this._pendingRender = null;
|
|
1217
2174
|
}
|
|
1218
|
-
this.
|
|
1219
|
-
this._pendingRender =
|
|
1220
|
-
|
|
1221
|
-
|
|
2175
|
+
this._ngZone.runOutsideAngular(() => {
|
|
2176
|
+
this._pendingRender = setTimeout(() => {
|
|
2177
|
+
this._pendingRender = null;
|
|
2178
|
+
this._ngZone.run(() => this._executeRender());
|
|
2179
|
+
}, 0);
|
|
2180
|
+
});
|
|
1222
2181
|
}
|
|
1223
2182
|
_executeRender() {
|
|
1224
2183
|
const categoryRows = this.getCategoryFilteredData();
|
|
@@ -1239,7 +2198,9 @@ class TableComponent {
|
|
|
1239
2198
|
this._preservePageIndex = null;
|
|
1240
2199
|
}
|
|
1241
2200
|
this._projectCurrentPage();
|
|
1242
|
-
|
|
2201
|
+
// detectChanges() au lieu de markForCheck() : met à jour uniquement CE composant
|
|
2202
|
+
// sans déclencher un cycle CD global qui provoquerait NG0100 dans les parents.
|
|
2203
|
+
this._cdr.detectChanges();
|
|
1243
2204
|
}
|
|
1244
2205
|
_projectCurrentPage() {
|
|
1245
2206
|
if (this.activeGroupByKey) {
|
|
@@ -1252,7 +2213,7 @@ class TableComponent {
|
|
|
1252
2213
|
const projected = [];
|
|
1253
2214
|
pageRows.forEach((row, i) => {
|
|
1254
2215
|
const idx = start + i;
|
|
1255
|
-
const p = {
|
|
2216
|
+
const p = { [ROW_RAW_KEY]: row, [ROW_INDEX_KEY]: idx };
|
|
1256
2217
|
this.activeColumns.forEach((col) => {
|
|
1257
2218
|
p[col.key] = this.resolveCellValue(col, row, idx);
|
|
1258
2219
|
});
|
|
@@ -1266,13 +2227,38 @@ class TableComponent {
|
|
|
1266
2227
|
const columns = config.columns ||
|
|
1267
2228
|
this.columnsConfig ||
|
|
1268
2229
|
this.resolveColumnsFromCompat(rows);
|
|
2230
|
+
// Mode HTML : [view] prend le dessus sur config.view si fourni (Phase 3).
|
|
2231
|
+
const view = this.view ?? config.view;
|
|
2232
|
+
const autoTableId = config.preferences?.tableId || TableComponent._hashColumns((columns || []).map(c => c.key));
|
|
2233
|
+
const exportConfig = {
|
|
2234
|
+
enabled: true,
|
|
2235
|
+
filename: autoTableId,
|
|
2236
|
+
...config.export,
|
|
2237
|
+
};
|
|
2238
|
+
const prefsConfig = {
|
|
2239
|
+
enabled: true,
|
|
2240
|
+
tableId: autoTableId,
|
|
2241
|
+
...config.preferences,
|
|
2242
|
+
};
|
|
1269
2243
|
return {
|
|
1270
2244
|
...config,
|
|
1271
2245
|
columns,
|
|
2246
|
+
view,
|
|
1272
2247
|
slices: config.slices || [],
|
|
1273
2248
|
enableSliceToggle: config.enableSliceToggle ?? true,
|
|
2249
|
+
export: exportConfig,
|
|
2250
|
+
preferences: prefsConfig,
|
|
1274
2251
|
};
|
|
1275
2252
|
}
|
|
2253
|
+
static _hashColumns(keys) {
|
|
2254
|
+
const str = keys.join('|');
|
|
2255
|
+
let h = 0x811c9dc5;
|
|
2256
|
+
for (let i = 0; i < str.length; i++) {
|
|
2257
|
+
h ^= str.charCodeAt(i);
|
|
2258
|
+
h = Math.imul(h, 0x01000193) >>> 0;
|
|
2259
|
+
}
|
|
2260
|
+
return h.toString(16).padStart(8, '0');
|
|
2261
|
+
}
|
|
1276
2262
|
resolveData() {
|
|
1277
2263
|
if (Array.isArray(this.data)) {
|
|
1278
2264
|
return this.data;
|
|
@@ -1306,8 +2292,12 @@ class TableComponent {
|
|
|
1306
2292
|
let filtered = data.filter(this.activeSliceFilter);
|
|
1307
2293
|
const query = this.searchQuery?.trim().toLowerCase();
|
|
1308
2294
|
if (query) {
|
|
1309
|
-
|
|
1310
|
-
|
|
2295
|
+
const restrictedKeys = this.resolvedConfig.search?.searchColumns;
|
|
2296
|
+
const searchCols = restrictedKeys?.length
|
|
2297
|
+
? [...this._columnMap.values()].filter(col => restrictedKeys.includes(col.key))
|
|
2298
|
+
: [...this._columnMap.values()];
|
|
2299
|
+
filtered = filtered.filter((row) => searchCols.some((col) => {
|
|
2300
|
+
const val = this.getDisplayValueForSearch(col, row);
|
|
1311
2301
|
return val != null && String(val).toLowerCase().includes(query);
|
|
1312
2302
|
}));
|
|
1313
2303
|
}
|
|
@@ -1318,7 +2308,7 @@ class TableComponent {
|
|
|
1318
2308
|
if (!groupKey) {
|
|
1319
2309
|
const result = [];
|
|
1320
2310
|
rows.forEach((row, index) => {
|
|
1321
|
-
const projected = {
|
|
2311
|
+
const projected = { [ROW_RAW_KEY]: row, [ROW_INDEX_KEY]: index };
|
|
1322
2312
|
this.activeColumns.forEach((col) => {
|
|
1323
2313
|
projected[col.key] = this.resolveCellValue(col, row, index);
|
|
1324
2314
|
});
|
|
@@ -1330,22 +2320,23 @@ class TableComponent {
|
|
|
1330
2320
|
const groupLazyStatus = groupColDef?.lazy ? (this._lazyColumnStatus.get(groupKey) ?? 'idle') : 'loaded';
|
|
1331
2321
|
if (groupLazyStatus !== 'loaded') {
|
|
1332
2322
|
this._totalFilteredCount = rows.length;
|
|
1333
|
-
return [{
|
|
2323
|
+
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
2324
|
}
|
|
1335
2325
|
const effectiveSort = this._activeSort.active && this._activeSort.direction
|
|
1336
2326
|
? this._activeSort
|
|
1337
2327
|
: this.resolvedConfig.defaultSort ?? { active: '', direction: '' };
|
|
1338
2328
|
// 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.
|
|
2329
|
+
const cacheValid = this._groupCache !== null &&
|
|
2330
|
+
this._groupCache.rows === rows &&
|
|
2331
|
+
this._groupCache.key === groupKey &&
|
|
2332
|
+
this._groupCache.sortOrder === this._groupBy.groupSortOrder &&
|
|
2333
|
+
this._groupCache.sortActive === (effectiveSort.active ?? '') &&
|
|
2334
|
+
this._groupCache.sortDir === (effectiveSort.direction ?? '');
|
|
1344
2335
|
let groupOrder;
|
|
1345
2336
|
let groupMap;
|
|
1346
2337
|
if (cacheValid) {
|
|
1347
|
-
groupOrder = this.
|
|
1348
|
-
groupMap = this.
|
|
2338
|
+
groupOrder = this._groupCache.order;
|
|
2339
|
+
groupMap = this._groupCache.map;
|
|
1349
2340
|
}
|
|
1350
2341
|
else {
|
|
1351
2342
|
// Pré-calcul des valeurs de groupe en un seul passage (évite O(n) find() par comparaison)
|
|
@@ -1376,7 +2367,7 @@ class TableComponent {
|
|
|
1376
2367
|
const va = groupValueCache.get(a);
|
|
1377
2368
|
const vb = groupValueCache.get(b);
|
|
1378
2369
|
const cmp = va.localeCompare(vb);
|
|
1379
|
-
return this.groupSortOrder === 'desc' ? -cmp : cmp;
|
|
2370
|
+
return this._groupBy.groupSortOrder === 'desc' ? -cmp : cmp;
|
|
1380
2371
|
});
|
|
1381
2372
|
groupOrder = [];
|
|
1382
2373
|
groupMap = new Map();
|
|
@@ -1396,34 +2387,36 @@ class TableComponent {
|
|
|
1396
2387
|
});
|
|
1397
2388
|
}
|
|
1398
2389
|
// Mise en cache
|
|
1399
|
-
this.
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
2390
|
+
this._groupCache = {
|
|
2391
|
+
rows,
|
|
2392
|
+
key: groupKey,
|
|
2393
|
+
sortOrder: this._groupBy.groupSortOrder,
|
|
2394
|
+
sortActive: effectiveSort.active ?? '',
|
|
2395
|
+
sortDir: effectiveSort.direction ?? '',
|
|
2396
|
+
order: groupOrder,
|
|
2397
|
+
map: groupMap,
|
|
2398
|
+
};
|
|
1406
2399
|
}
|
|
1407
2400
|
this._totalFilteredCount = rows.length;
|
|
1408
2401
|
// Protection contre les jeux de données à trop haute cardinalité
|
|
1409
2402
|
if (groupOrder.length > TableComponent.MAX_GROUP_COUNT) {
|
|
1410
|
-
return [{
|
|
2403
|
+
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
2404
|
}
|
|
1412
2405
|
const pageSize = this.groupPageSize;
|
|
1413
2406
|
const result = [];
|
|
1414
2407
|
groupOrder.forEach((groupValue) => {
|
|
1415
2408
|
const groupRows = groupMap.get(groupValue);
|
|
1416
2409
|
const totalCount = groupRows.length;
|
|
1417
|
-
const currentPage = pageSize === 0 ? 0 :
|
|
2410
|
+
const currentPage = pageSize === 0 ? 0 : this._groupBy.getPage(groupValue);
|
|
1418
2411
|
const pageCount = pageSize === 0 ? 1 : Math.max(1, Math.ceil(totalCount / pageSize));
|
|
1419
2412
|
const collapsed = this.isGroupCollapsed(groupValue);
|
|
1420
|
-
result.push({
|
|
2413
|
+
result.push({ [GROUP_HEADER_MARKER]: true, [ROW_GROUP_KEY]: groupValue, [ROW_GROUP_COUNT]: totalCount, [ROW_GROUP_PAGE]: currentPage, [ROW_GROUP_PAGE_COUNT]: pageCount });
|
|
1421
2414
|
if (!collapsed) {
|
|
1422
2415
|
const start = pageSize === 0 ? 0 : currentPage * pageSize;
|
|
1423
2416
|
const end = pageSize === 0 ? totalCount : Math.min(start + pageSize, totalCount);
|
|
1424
2417
|
groupRows.slice(start, end).forEach((row, i) => {
|
|
1425
2418
|
const idx = start + i;
|
|
1426
|
-
const projected = {
|
|
2419
|
+
const projected = { [ROW_RAW_KEY]: row, [ROW_INDEX_KEY]: idx };
|
|
1427
2420
|
this.activeColumns.forEach((col) => {
|
|
1428
2421
|
projected[col.key] = this.resolveCellValue(col, row, idx);
|
|
1429
2422
|
});
|
|
@@ -1455,6 +2448,25 @@ class TableComponent {
|
|
|
1455
2448
|
return direction === 'asc' ? cmp : -cmp;
|
|
1456
2449
|
};
|
|
1457
2450
|
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Retourne la valeur telle qu'elle est affichée dans la cellule, en suivant exactement
|
|
2453
|
+
* le même chemin que resolveCellValue — utilisée pour la recherche plein-texte.
|
|
2454
|
+
* Ne passe pas par sortValue (valeur brute de tri) contrairement à getLazyAwareValue.
|
|
2455
|
+
*/
|
|
2456
|
+
getDisplayValueForSearch(col, row) {
|
|
2457
|
+
if (col.lazy) {
|
|
2458
|
+
const status = this._lazyColumnStatus.get(col.key) ?? 'idle';
|
|
2459
|
+
if (status !== 'loaded')
|
|
2460
|
+
return null;
|
|
2461
|
+
const rowMap = this._lazyColumnData.get(col.key);
|
|
2462
|
+
return normalizeCellValue(rowMap?.get(row) ?? '');
|
|
2463
|
+
}
|
|
2464
|
+
if (col.searchValue) {
|
|
2465
|
+
return col.searchValue(row, 0);
|
|
2466
|
+
}
|
|
2467
|
+
const rawValue = col.value ? col.value(row, 0) : row?.[col.key];
|
|
2468
|
+
return normalizeCellValue(rawValue);
|
|
2469
|
+
}
|
|
1458
2470
|
getLazyAwareValue(columnKey, row) {
|
|
1459
2471
|
const colDef = this._columnMap.get(columnKey);
|
|
1460
2472
|
if (colDef?.lazy) {
|
|
@@ -1472,9 +2484,9 @@ class TableComponent {
|
|
|
1472
2484
|
if (col.lazy) {
|
|
1473
2485
|
const status = this._lazyColumnStatus.get(col.key) ?? 'idle';
|
|
1474
2486
|
if (status === 'loading')
|
|
1475
|
-
return
|
|
2487
|
+
return LAZY_LOADING_VALUE;
|
|
1476
2488
|
if (status === 'error')
|
|
1477
|
-
return
|
|
2489
|
+
return LAZY_ERROR_VALUE;
|
|
1478
2490
|
if (status === 'loaded') {
|
|
1479
2491
|
const rowMap = this._lazyColumnData.get(col.key);
|
|
1480
2492
|
return normalizeCellValue(rowMap?.get(row) ?? '');
|
|
@@ -1534,6 +2546,7 @@ class TableComponent {
|
|
|
1534
2546
|
this._paginatorSubscribed = this.paginator;
|
|
1535
2547
|
this.paginator.page.pipe(takeUntil(this._destroy$)).subscribe(() => {
|
|
1536
2548
|
this.pageSize = this.paginator.pageSize;
|
|
2549
|
+
this.pageChange.emit({ pageIndex: this.paginator.pageIndex, pageSize: this.paginator.pageSize });
|
|
1537
2550
|
if (this._pendingRender !== null)
|
|
1538
2551
|
return;
|
|
1539
2552
|
this._projectCurrentPage();
|
|
@@ -1552,16 +2565,17 @@ class TableComponent {
|
|
|
1552
2565
|
active: this.sort.active,
|
|
1553
2566
|
direction: this.sort.direction,
|
|
1554
2567
|
};
|
|
2568
|
+
this.sortChange.emit({ active: this.sort.active, direction: this.sort.direction });
|
|
1555
2569
|
this._scheduleRender();
|
|
1556
2570
|
});
|
|
1557
2571
|
}
|
|
1558
2572
|
}
|
|
1559
2573
|
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 }); }
|
|
2574
|
+
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}:host ::ng-deep th.mat-mdc-header-cell.drag-enabled{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
2575
|
}
|
|
1562
2576
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TableComponent, decorators: [{
|
|
1563
2577
|
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"] }]
|
|
2578
|
+
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}:host ::ng-deep th.mat-mdc-header-cell.drag-enabled{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
2579
|
}], propDecorators: { config: [{
|
|
1566
2580
|
type: Input
|
|
1567
2581
|
}], dataSource: [{
|
|
@@ -1572,6 +2586,8 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
|
|
|
1572
2586
|
type: Input
|
|
1573
2587
|
}], columnsConfig: [{
|
|
1574
2588
|
type: Input
|
|
2589
|
+
}], view: [{
|
|
2590
|
+
type: Input
|
|
1575
2591
|
}], columnLabels: [{
|
|
1576
2592
|
type: Input
|
|
1577
2593
|
}], isLoading: [{
|
|
@@ -1586,6 +2602,16 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
|
|
|
1586
2602
|
type: Output
|
|
1587
2603
|
}], rowSelected: [{
|
|
1588
2604
|
type: Output
|
|
2605
|
+
}], sortChange: [{
|
|
2606
|
+
type: Output
|
|
2607
|
+
}], pageChange: [{
|
|
2608
|
+
type: Output
|
|
2609
|
+
}], searchChange: [{
|
|
2610
|
+
type: Output
|
|
2611
|
+
}], groupByChange: [{
|
|
2612
|
+
type: Output
|
|
2613
|
+
}], columnsChange: [{
|
|
2614
|
+
type: Output
|
|
1589
2615
|
}], paginator: [{
|
|
1590
2616
|
type: ViewChild,
|
|
1591
2617
|
args: [MatPaginator]
|
|
@@ -1614,5 +2640,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
|
|
|
1614
2640
|
* Generated bundle index. Do not edit.
|
|
1615
2641
|
*/
|
|
1616
2642
|
|
|
1617
|
-
export { JqtCellDefDirective, SlicePanelComponent, TableComponent };
|
|
2643
|
+
export { JQT_I18N, JQT_I18N_DEFAULTS, JqtCellDefDirective, SlicePanelComponent, TableComponent, col };
|
|
1618
2644
|
//# sourceMappingURL=oneteme-jquery-table.mjs.map
|