@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.
Files changed (29) hide show
  1. package/README.md +439 -122
  2. package/esm2022/lib/component/export.manager.mjs +45 -0
  3. package/esm2022/lib/component/group-by.manager.mjs +99 -0
  4. package/esm2022/lib/component/lazy-column-manager.mjs +99 -0
  5. package/esm2022/lib/component/slice-panel/slice-panel.component.mjs +43 -37
  6. package/esm2022/lib/component/slice-panel/slice-panel.model.mjs +1 -1
  7. package/esm2022/lib/component/table-preferences.manager.mjs +38 -0
  8. package/esm2022/lib/component/table.component.mjs +732 -370
  9. package/esm2022/lib/component/table.constants.mjs +31 -0
  10. package/esm2022/lib/component/table.utils.mjs +2 -2
  11. package/esm2022/lib/component/view/view.facade.mjs +304 -0
  12. package/esm2022/lib/jqt-i18n.token.mjs +41 -0
  13. package/esm2022/lib/jquery-table.model.mjs +14 -2
  14. package/esm2022/public-api.mjs +2 -1
  15. package/fesm2022/oneteme-jquery-table.mjs +1434 -408
  16. package/fesm2022/oneteme-jquery-table.mjs.map +1 -1
  17. package/lib/component/export.manager.d.ts +16 -0
  18. package/lib/component/group-by.manager.d.ts +44 -0
  19. package/lib/component/lazy-column-manager.d.ts +26 -0
  20. package/lib/component/slice-panel/slice-panel.component.d.ts +11 -3
  21. package/lib/component/slice-panel/slice-panel.model.d.ts +11 -1
  22. package/lib/component/table-preferences.manager.d.ts +17 -0
  23. package/lib/component/table.component.d.ts +165 -27
  24. package/lib/component/table.constants.d.ts +30 -0
  25. package/lib/component/view/view.facade.d.ts +95 -0
  26. package/lib/jqt-i18n.token.d.ts +87 -0
  27. package/lib/jquery-table.model.d.ts +82 -0
  28. package/package.json +2 -2
  29. 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 i6 from '@angular/material/menu';
12
+ import * as i7 from '@angular/material/menu';
11
13
  import { MatMenuModule } from '@angular/material/menu';
12
- import * as i4 from '@angular/material/paginator';
14
+ import * as i5 from '@angular/material/paginator';
13
15
  import { MatPaginatorIntl, MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
14
- import * as i9 from '@angular/material/select';
16
+ import * as i10 from '@angular/material/select';
15
17
  import { MatSelectModule } from '@angular/material/select';
16
- import * as i5 from '@angular/material/sort';
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 * as i8 from '@angular/material/form-field';
22
- import * as i10 from '@angular/material/core';
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
- this._hiddenStaticSliceKeys = new Set();
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 visibleStaticCount = (this.sliceConfigs || []).filter((s) => !s.columnKey || !this._hiddenStaticSliceKeys.has(s.columnKey)).length;
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 || []).filter((s) => !s.columnKey || !this._hiddenStaticSliceKeys.has(s.columnKey)).length;
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 || []).filter((s) => !s.columnKey || !this._hiddenStaticSliceKeys.has(s.columnKey)).length;
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 || []).find((s) => !s.columnKey || !this._hiddenStaticSliceKeys.has(s.columnKey));
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(columnKey) {
210
- return !this._hiddenStaticSliceKeys.has(columnKey);
260
+ isStaticSliceVisible(_columnKey) {
261
+ return true;
211
262
  }
212
- toggleStaticSlice(columnKey) {
213
- const next = new Set(this._hiddenStaticSliceKeys);
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
- // ── Dynamic slice management ──────────────────────────────────
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
- return category ? category.filter(row) : false;
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 staticSlices = (this.sliceConfigs || []).filter((s) => !s.columnKey || !this._hiddenStaticSliceKeys.has(s.columnKey));
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\" fontSet=\"material-icons-outlined\">filter_list</mat-icon>\n {{ slices.length > 1 ? 'Filtres' : 'Filtre' }}\n </span>\n <button\n *ngIf=\"showToggle\"\n type=\"button\"\n class=\"slice-toggle\"\n [attr.aria-label]=\"isCollapsed ? 'D\u00E9plier le filtre' : 'Replier le filtre'\"\n (click)=\"togglePanel()\"\n >\n <span class=\"slice-toggle-icon\">{{ isCollapsed ? '\u203A' : '\u2039' }}</span>\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\" fontSet=\"material-icons-outlined\">{{ getSliceIcon(slice) }}</mat-icon>\n <p class=\"slice-title\">{{ slice.title || 'Filtrer' }}</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\">Aucune donn\u00E9e</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-icon{width:100%;height:100%;display:inline-flex;align-items:center;justify-content:center;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;font-size:16px}.slice-panel.collapsed .slice-toggle:hover{border-color:var(--jqt-text-muted)}.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 }); }
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\" fontSet=\"material-icons-outlined\">filter_list</mat-icon>\n {{ slices.length > 1 ? 'Filtres' : 'Filtre' }}\n </span>\n <button\n *ngIf=\"showToggle\"\n type=\"button\"\n class=\"slice-toggle\"\n [attr.aria-label]=\"isCollapsed ? 'D\u00E9plier le filtre' : 'Replier le filtre'\"\n (click)=\"togglePanel()\"\n >\n <span class=\"slice-toggle-icon\">{{ isCollapsed ? '\u203A' : '\u2039' }}</span>\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\" fontSet=\"material-icons-outlined\">{{ getSliceIcon(slice) }}</mat-icon>\n <p class=\"slice-title\">{{ slice.title || 'Filtrer' }}</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\">Aucune donn\u00E9e</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-icon{width:100%;height:100%;display:inline-flex;align-items:center;justify-content:center;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;font-size:16px}.slice-panel.collapsed .slice-toggle:hover{border-color:var(--jqt-text-muted)}.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"] }]
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('fr-FR');
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
- this._staticSlicesForMenu = [];
560
- this._activeDynamicSliceColumns = [];
561
- this._availableColumnsForDynamicSlice = [];
562
- this._allDynamicSliceColumns = [];
563
- this._activeSliceByLabel = 'Aucun';
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
- this._lazyColumnStatus = new Map();
576
- this._lazyColumnData = new Map();
577
- this._lazyFetchCancels = new Map();
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.isGroupHeader = (_, row) => row.__groupHeader === true;
581
- // ── Lifecycle ──────────────────────────────────────────────────────────────
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
- this.userCustomizedColumns = false;
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
- // Cache groupBy : invalide uniquement quand les données/tri/groupKey changent
599
- this._groupCacheRows = null;
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.trackRowFn = (_index, row) => row.__groupHeader ? `__group_${row.__groupKey}` : row.__index ?? _index;
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._lazyFetchCancels.forEach(s => { s.next(); s.complete(); });
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 headerRow = this._el.nativeElement.querySelector('tr.mat-mdc-header-row');
653
- if (headerRow) {
654
- const h = headerRow.getBoundingClientRect().height;
655
- if (h > 0) {
656
- this._el.nativeElement.style.setProperty('--actual-header-height', h + 'px');
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.resolvedConfig.view?.enabled === true; }
672
- get showToolbar() { return this.showSearchBar || this.showViewButton; }
673
- get showFields() { return this.showViewButton; }
674
- get showGroupBySection() { return this.showViewButton && this.groupByColumns.length > 0; }
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 activeGroupByLabel() {
679
- if (!this.activeGroupByKey)
680
- return 'Aucun';
681
- const col = this.groupByColumns.find((c) => c.key === this.activeGroupByKey);
682
- return col?.header || this.activeGroupByKey;
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 activeSliceByLabel() {
688
- return this._activeSliceByLabel;
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 activeDynamicSliceColumns() {
691
- return this._activeDynamicSliceColumns;
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
- const all = this.resolvedConfig.columns || [];
698
- const seen = new Set();
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.activeGroupByKey = key;
719
- this._defaultGroupCollapsed = key !== null;
720
- this._collapsedGroups.clear();
721
- this._groupPages.clear();
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.groupPageSize = Number(size);
740
- this._groupPages.clear();
741
- this.refreshViewModel();
1521
+ this._groupBy.onGroupPageSizeChange(Number(size));
742
1522
  }
743
1523
  onGroupSortToggle(dir) {
744
- if (this.groupSortOrder === dir)
745
- return;
746
- this.groupSortOrder = dir;
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._lazyColumnStatus.get(key) ?? 'idle';
1530
+ return this._lazy.getStatus(key);
759
1531
  }
760
1532
  getLazyRenderType(rowValue) {
761
- if (rowValue === '__lazy_loading__')
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
- const k = column.key;
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
- if (!column.lazy)
785
- return;
786
- const k = column.key;
787
- if (this._lazyColumnStatus.get(k) === 'loading')
788
- return;
789
- const prev = this._lazyFetchCancels.get(k);
790
- if (prev) {
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
- if (this._collapsedGroups.has(groupKey)) {
829
- this._collapsedGroups.delete(groupKey);
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._defaultGroupCollapsed ? !this._collapsedGroups.has(groupKey) : this._collapsedGroups.has(groupKey);
1568
+ return this._groupBy.isGroupCollapsed(groupKey);
857
1569
  }
858
1570
  groupPageBack(groupKey) {
859
- const current = this._groupPages.get(groupKey) ?? 0;
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
- const maxPage = Math.ceil(totalCount / this.groupPageSize) - 1;
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.uniqueColumnsByKey((this.resolvedConfig.columns || []).filter((c) => !c.optional));
1581
+ return this._view.menuBaseColumns;
881
1582
  }
882
1583
  get menuOptionalColumns() {
883
- return this.uniqueColumnsByKey((this.resolvedConfig.columns || []).filter((c) => c.optional));
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.resolvedConfig.view?.enableColumnRemoval !== false;
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.slicePanelRef?.showPanel ?? (this.hasSliceConfig && this._resolvedData.length > 0);
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.showViewButton && ((this.resolvedConfig.slices?.length ?? 0) > 0 || this._allDynamicSliceColumns.length > 0);
1620
+ return this._view.showSliceBySection;
901
1621
  }
902
1622
  get staticSlicesForMenu() {
903
- return this._staticSlicesForMenu;
1623
+ return this._view.staticSlicesForMenu;
904
1624
  }
905
1625
  get availableColumnsForDynamicSlice() {
906
- return this._availableColumnsForDynamicSlice;
1626
+ return this._view.sliceBy.availableForDynamic;
907
1627
  }
908
1628
  isStaticSliceVisible(columnKey) {
909
- return this.slicePanelRef?.isStaticSliceVisible(columnKey) ?? true;
1629
+ return !this._staticSliceHiddenKeys.has(columnKey);
910
1630
  }
911
1631
  toggleStaticSlice(columnKey) {
912
- this.slicePanelRef?.toggleStaticSlice(columnKey);
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._activeDynamicSliceColumns.some(c => c.key === key);
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
- const allCols = this.resolvedConfig.columns ?? [];
942
- const staticKeys = new Set((this.resolvedConfig.slices ?? []).map(s => s.columnKey).filter((k) => !!k));
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 || 'Aucune donnée';
1681
+ return this.resolvedConfig.labels?.empty || this.i18n.emptyState;
960
1682
  }
961
1683
  get loadingStateLabel() {
962
- return this.resolvedConfig.labels?.loading || 'Chargement des données...';
1684
+ return this.resolvedConfig.labels?.loading || this.i18n.loadingState;
963
1685
  }
964
1686
  get effectiveIsLoading() {
965
- return this.isLoading || this._pendingRender !== null;
1687
+ return this.isLoading;
966
1688
  }
967
1689
  get isEmpty() {
968
- return !this.effectiveIsLoading && this._allFilteredRows.length === 0;
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.resolvedConfig.view?.enableColumnDragDrop === true;
1699
+ return this._view.isColumnDragDropEnabled;
977
1700
  }
978
1701
  get totalRows() {
979
1702
  return this.activeGroupByKey
980
- ? this.tableDataSource.data.filter((r) => !r.__groupHeader).length
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.userCustomizedColumns = true;
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
- if (!column.optional && !this.allowColumnRemoval) {
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((column) => column.key === columnKey);
1733
+ return this.activeColumns.some(c => c.key === columnKey);
1035
1734
  }
1036
1735
  onRemoveColumn(columnKey, event) {
1037
1736
  event?.stopPropagation();
1038
- if (!this.allowColumnRemoval) {
1737
+ const column = this.activeColumns.find(item => item.key === columnKey);
1738
+ if (!column)
1039
1739
  return;
1040
- }
1041
- const column = this.activeColumns.find((item) => item.key === columnKey);
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._cellDefs?.find((d) => d.columnKey === key)?.template ?? null;
1751
+ return this._cellDefMap.get(key) ?? null;
1053
1752
  }
1054
1753
  get hasCustomCellDefs() {
1055
- return (this._cellDefs?.length ?? 0) > 0;
1754
+ return this._cellDefMap.size > 0;
1056
1755
  }
1057
1756
  get hasRowClickHandler() {
1058
- return this.rowSelected.observed || !!this.config?.onRowSelected;
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?.__groupHeader)
1790
+ if (!fn || row?.[GROUP_HEADER_MARKER])
1063
1791
  return {};
1064
- return fn(row.__raw ?? row, row.__index ?? 0);
1792
+ return fn(row[ROW_RAW_KEY] ?? row, row[ROW_INDEX_KEY] ?? 0);
1065
1793
  }
1066
1794
  onRowClick(row, event) {
1067
- if (row?.__groupHeader)
1795
+ if (row?.[GROUP_HEADER_MARKER])
1068
1796
  return;
1069
1797
  if (!this.hasRowClickHandler)
1070
1798
  return;
1071
- const resolved = (row?.__raw ?? 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 previousIndex = this.activeColumns.findIndex((column) => column.key === this.draggedColumnKey);
1100
- const currentIndex = this.activeColumns.findIndex((column) => column.key === targetColumnKey);
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
- // ── Pipeline de données (privé) ──────────────────────────────────────────
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
- const defaultColumns = (this.resolvedConfig.columns || []).filter(c => !c.optional);
1140
- if (!this.userCustomizedColumns || this.activeColumns.length === 0) {
1141
- this.activeColumns = [...defaultColumns];
1142
- }
1143
- else {
1144
- const allowedKeys = new Set((this.resolvedConfig.columns || []).map((column) => column.key));
1145
- this.activeColumns = this.activeColumns.filter((column) => allowedKeys.has(column.key));
1146
- }
1147
- this.renderedColumns = this.activeColumns.map((column) => column.key);
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
- // Mise à jour des propriétés stables pour les bindings du template
1157
- this._sliceConfigs = this.resolvedConfig.slices ?? [];
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._staticSlicesForMenu = (this.resolvedConfig.slices ?? [])
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
- const staticKeys = new Set((this.resolvedConfig.slices ?? []).map(s => s.columnKey).filter((k) => !!k));
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
- _computeSliceLabel() {
1195
- const visibleStaticCount = this.slicePanelRef?.staticSliceCount
1196
- ?? this._sliceConfigs.length;
1197
- const total = visibleStaticCount + this._activeDynamicSliceColumns.length;
1198
- if (total === 0) {
1199
- this._activeSliceByLabel = 'Aucun';
1200
- return;
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
- if (total === 1) {
1203
- if (visibleStaticCount === 1) {
1204
- const first = this._sliceConfigs.find(s => !s.columnKey || this.isStaticSliceVisible(s.columnKey));
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._activeSliceByLabel = `${total} actifs`;
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._pendingRender = setTimeout(() => {
1219
- this._pendingRender = null;
1220
- this._executeRender();
1221
- }, 0);
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
- this._cdr.markForCheck();
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 = { __raw: row, __index: idx };
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
- filtered = filtered.filter((row) => this.activeColumns.some((col) => {
1310
- const val = this.getLazyAwareValue(col.key, row);
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 = { __raw: row, __index: index };
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 [{ __groupHeader: true, __groupKey: '__lazy_loading__', __groupCount: rows.length, __groupPage: 0, __groupPageCount: 1 }];
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._groupCacheRows === rows &&
1340
- this._groupCacheKey === groupKey &&
1341
- this._groupCacheSortOrder === this.groupSortOrder &&
1342
- this._groupCacheSortActive === (effectiveSort.active ?? '') &&
1343
- this._groupCacheSortDir === (effectiveSort.direction ?? '');
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._groupCacheOrder;
1348
- groupMap = this._groupCacheMap;
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._groupCacheRows = rows;
1400
- this._groupCacheKey = groupKey;
1401
- this._groupCacheSortOrder = this.groupSortOrder;
1402
- this._groupCacheSortActive = effectiveSort.active ?? '';
1403
- this._groupCacheSortDir = effectiveSort.direction ?? '';
1404
- this._groupCacheOrder = groupOrder;
1405
- this._groupCacheMap = groupMap;
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 [{ __groupHeader: true, __groupKey: '__too_many_groups__', __groupCount: groupOrder.length, __groupPage: 0, __groupPageCount: 1 }];
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 : (this._groupPages.get(groupValue) ?? 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({ __groupHeader: true, __groupKey: groupValue, __groupCount: totalCount, __groupPage: currentPage, __groupPageCount: pageCount });
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 = { __raw: row, __index: idx };
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 '__lazy_loading__';
2487
+ return LAZY_LOADING_VALUE;
1476
2488
  if (status === 'error')
1477
- return '__lazy_error__';
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