@oneteme/jquery-table 0.0.1 → 0.0.2

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