@oneteme/jquery-table 0.0.8 → 0.0.10

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.
@@ -1,28 +1,27 @@
1
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';
2
+ import { InjectionToken, Directive, Input, EventEmitter, Component, ChangeDetectionStrategy, Output, inject, ChangeDetectorRef, ElementRef, NgZone, ViewChild, ContentChildren } from '@angular/core';
3
3
  import * as i1 from '@angular/common';
4
4
  import { CommonModule } from '@angular/common';
5
- import * as i2$1 from '@angular/forms';
5
+ import * as i2 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
8
  import { MatDividerModule } from '@angular/material/divider';
10
- import * as i2 from '@angular/material/icon';
9
+ import * as i6 from '@angular/material/icon';
11
10
  import { MatIconModule } from '@angular/material/icon';
12
- import * as i7 from '@angular/material/menu';
13
11
  import { MatMenuModule } from '@angular/material/menu';
14
- import * as i5 from '@angular/material/paginator';
12
+ import * as i4 from '@angular/material/paginator';
15
13
  import { MatPaginatorIntl, MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
16
- import * as i10 from '@angular/material/select';
14
+ import * as i8 from '@angular/material/select';
17
15
  import { MatSelectModule } from '@angular/material/select';
18
- import * as i6 from '@angular/material/sort';
16
+ import * as i5 from '@angular/material/sort';
19
17
  import { MatSort, MatSortModule } from '@angular/material/sort';
20
18
  import * as i3 from '@angular/material/table';
21
19
  import { MatTableDataSource, MatTableModule } from '@angular/material/table';
22
- import { Subject, take, takeUntil } from 'rxjs';
23
- import '@oneteme/jquery-core';
24
- import * as i9 from '@angular/material/form-field';
25
- import * as i11 from '@angular/material/core';
20
+ import { Subject, takeUntil } from 'rxjs';
21
+ import { OrganizerButtonComponent, OrganizerFacade, GroupByManager, LazyFieldManager, SlicePanelComponent } from '@oneteme/jquery-organizer';
22
+ export { OrganizerButtonComponent, SlicePanelComponent } from '@oneteme/jquery-organizer';
23
+ import * as i7 from '@angular/material/form-field';
24
+ import * as i9 from '@angular/material/core';
26
25
 
27
26
  /**
28
27
  * Raccourci pour déclarer une colonne de base.
@@ -65,7 +64,7 @@ const JQT_I18N_DEFAULTS = {
65
64
  filterEmptyState: 'Aucune donnée',
66
65
  exportButtonLabel: 'Exporter',
67
66
  preferencesMenuLabel: 'Préférences',
68
- preferencesEdit: 'Éditeur',
67
+ preferencesEdit: 'Éditer',
69
68
  preferencesSave: 'Sauvegarder',
70
69
  preferencesClear: 'Réinitialiser',
71
70
  preferencesSavedMessage: 'Configuration enregistrée. Vous pouvez la réinitialiser à tout moment via Préférences → Réinitialiser.',
@@ -111,476 +110,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
111
110
  args: ['jqtCellDef']
112
111
  }] } });
113
112
 
114
- const EMPTY_CATEGORY_KEY = '__empty__';
115
- const EMPTY_CATEGORY_LABEL = 'Vide';
116
- class SlicePanelComponent {
117
- constructor() {
118
- this.sliceConfigs = [];
119
- this.columns = [];
120
- this.data = [];
121
- this.lazyData = new Map();
122
- this.lazyStatus = new Map();
123
- this.showToggle = true;
124
- this.showCounts = true;
125
- this.collapsedByDefault = false;
126
- this.alwaysShow = false;
127
- this.filterChange = new EventEmitter();
128
- this.activeKeysChange = new EventEmitter();
129
- this.dynamicSliceKeysChange = new EventEmitter();
130
- this.collapsedChange = new EventEmitter();
131
- this._cdr = inject(ChangeDetectorRef);
132
- this._i18nRaw = inject(JQT_I18N, { optional: true });
133
- this.i18n = { ...JQT_I18N_DEFAULTS, ...(this._i18nRaw ?? {}) };
134
- this.isCollapsed = false;
135
- this.activeKeysBySlice = new Map();
136
- this._expandedSlices = new Set();
137
- this._dynamicSlices = [];
138
- // ── Cache
139
- this._cachedSlices = [];
140
- this._countCache = new Map();
141
- this.trackBySliceIndex = (index, _) => index;
142
- this.trackByCategoryKey = (_, cat) => cat.key;
143
- }
144
- // ── Lifecycle
145
- ngOnInit() {
146
- this.isCollapsed = this.collapsedByDefault;
147
- }
148
- ngOnChanges(changes) {
149
- if (changes['sliceConfigs'] || changes['data'] || changes['columns'] || changes['lazyData'] || changes['lazyStatus']) {
150
- this._refreshDynamicSlices();
151
- this._rebuildCache();
152
- const shouldReemitFilter = !!(changes['data'] || changes['columns'] || changes['lazyData'] || changes['lazyStatus']);
153
- const hasActiveFilters = [...this.activeKeysBySlice.values()].some(s => s.size > 0);
154
- if (hasActiveFilters && shouldReemitFilter) {
155
- this._emitFilter();
156
- }
157
- this._cdr.markForCheck();
158
- }
159
- }
160
- // ── Computed
161
- get showPanel() {
162
- const hasCfg = (this.sliceConfigs || []).length > 0 || this._dynamicSlices.length > 0;
163
- if (this.alwaysShow)
164
- return hasCfg;
165
- return hasCfg && (this.data?.length ?? 0) > 0;
166
- }
167
- get slices() {
168
- return this._cachedSlices;
169
- }
170
- get staticSliceCount() {
171
- return (this.sliceConfigs || []).length;
172
- }
173
- get staticSlicesForMenu() {
174
- return (this.sliceConfigs || [])
175
- .filter((s) => !!s.columnKey)
176
- .map((s) => ({ key: s.columnKey, title: s.title || s.columnKey }));
177
- }
178
- get activeDynamicSliceColumns() {
179
- return this._dynamicSlices
180
- .map((d) => (this.columns || []).find((c) => c.key === d.key))
181
- .filter((c) => !!c);
182
- }
183
- get availableColumnsForDynamicSlice() {
184
- const dynamicKeys = new Set(this._dynamicSlices.map((d) => d.key));
185
- const staticKeys = new Set((this.sliceConfigs || []).map((s) => s.columnKey).filter((k) => !!k));
186
- const existingKeys = new Set([...dynamicKeys, ...staticKeys]);
187
- const seen = new Set();
188
- return (this.columns || []).filter((c) => {
189
- if (existingKeys.has(c.key) || seen.has(c.key))
190
- return false;
191
- seen.add(c.key);
192
- return true;
193
- });
194
- }
195
- get activeSliceByLabel() {
196
- const staticActiveCount = (this.sliceConfigs || []).length;
197
- const total = staticActiveCount + this._dynamicSlices.length;
198
- if (total === 0)
199
- return 'Aucun';
200
- if (total === 1) {
201
- const first = (this.sliceConfigs || [])[0];
202
- if (first)
203
- return first.title || first.columnKey || '1';
204
- return this.activeDynamicSliceColumns[0]?.header || '1';
205
- }
206
- return `${total} actifs`;
207
- }
208
- // ── UI state
209
- isSliceCollapsed(sliceIndex) {
210
- if (this._cachedSlices.length === 1) {
211
- return this._expandedSlices.has(-1 - sliceIndex);
212
- }
213
- return !this._expandedSlices.has(sliceIndex);
214
- }
215
- toggleSliceCollapse(sliceIndex) {
216
- if (this.slices.length === 1) {
217
- const closedKey = -1 - sliceIndex;
218
- if (this._expandedSlices.has(closedKey)) {
219
- this._expandedSlices.delete(closedKey);
220
- }
221
- else {
222
- this._expandedSlices.add(closedKey);
223
- this._scrollSliceTitleIntoView(sliceIndex);
224
- }
225
- }
226
- else {
227
- if (this._expandedSlices.has(sliceIndex)) {
228
- this._expandedSlices.delete(sliceIndex);
229
- }
230
- else {
231
- this._expandedSlices.clear();
232
- this._expandedSlices.add(sliceIndex);
233
- this._scrollSliceTitleIntoView(sliceIndex);
234
- }
235
- }
236
- this._cdr.markForCheck();
237
- }
238
- togglePanel() {
239
- this.isCollapsed = !this.isCollapsed;
240
- this.collapsedChange.emit(this.isCollapsed);
241
- this._cdr.markForCheck();
242
- }
243
- isSliceDynamic(sliceIndex) {
244
- return sliceIndex >= this.staticSliceCount;
245
- }
246
- isSliceLoading(sliceIndex) {
247
- const staticSlice = (this.sliceConfigs || [])[sliceIndex];
248
- if (staticSlice?.columnKey) {
249
- const colDef = (this.columns || []).find(c => c.key === staticSlice.columnKey);
250
- if (colDef?.lazy) {
251
- const status = this.lazyStatus.get(staticSlice.columnKey) ?? 'idle';
252
- return status === 'loading' || status === 'idle';
253
- }
254
- return false;
255
- }
256
- const key = this._getDynamicSliceColumnKey(sliceIndex);
257
- if (!key)
258
- return false;
259
- const colDef = (this.columns || []).find(c => c.key === key);
260
- if (!colDef?.lazy)
261
- return false;
262
- const status = this.lazyStatus.get(key) ?? 'idle';
263
- return status === 'loading' || status === 'idle';
264
- }
265
- isStaticSliceVisible(_columnKey) {
266
- return true;
267
- }
268
- toggleStaticSlice(_columnKey) {
269
- // Délégué à table.component via _staticSliceHiddenKeys — ne rien faire ici.
270
- }
271
- // ── Selection
272
- getSliceIcon(slice) {
273
- if (slice.icon)
274
- return slice.icon;
275
- if (slice.columnKey) {
276
- return (this.columns || []).find(c => c.key === slice.columnKey)?.icon;
277
- }
278
- return undefined;
279
- }
280
- isSliceCategoryActive(sliceIndex, categoryKey) {
281
- return this.activeKeysBySlice.get(sliceIndex)?.has(categoryKey) ?? false;
282
- }
283
- isSliceAllActive(sliceIndex) {
284
- return !this.activeKeysBySlice.has(sliceIndex) || this.activeKeysBySlice.get(sliceIndex).size === 0;
285
- }
286
- sliceCategoryCount(sliceIndex, categoryKey) {
287
- const cacheKey = `${sliceIndex}:${categoryKey}`;
288
- if (this._countCache.has(cacheKey)) {
289
- return this._countCache.get(cacheKey);
290
- }
291
- const slice = this._cachedSlices[sliceIndex];
292
- if (!slice)
293
- return 0;
294
- const category = (slice.categories || []).find((c) => c.key === categoryKey);
295
- const count = category ? (this.data || []).filter((row) => category.filter(row)).length : 0;
296
- this._countCache.set(cacheKey, count);
297
- return count;
298
- }
299
- isSliceEmpty(sliceIndex) {
300
- const slice = this._cachedSlices[sliceIndex];
301
- if (!slice)
302
- return true;
303
- return (slice.categories || []).every(c => this.sliceCategoryCount(sliceIndex, c.key) === 0);
304
- }
305
- onSliceCategorySelected(sliceIndex, categoryKey) {
306
- const slice = this.slices[sliceIndex];
307
- if (!slice)
308
- return;
309
- if (!this.activeKeysBySlice.has(sliceIndex)) {
310
- this.activeKeysBySlice.set(sliceIndex, new Set());
311
- }
312
- const activeKeys = this.activeKeysBySlice.get(sliceIndex);
313
- if (slice.multiSelect === false) {
314
- if (activeKeys.has(categoryKey)) {
315
- activeKeys.clear();
316
- }
317
- else {
318
- activeKeys.clear();
319
- activeKeys.add(categoryKey);
320
- }
321
- }
322
- else {
323
- if (activeKeys.has(categoryKey)) {
324
- activeKeys.delete(categoryKey);
325
- }
326
- else {
327
- activeKeys.add(categoryKey);
328
- }
329
- }
330
- this._emitFilter();
331
- this._cdr.markForCheck();
332
- }
333
- onSliceAllSelected(sliceIndex) {
334
- if (this.activeKeysBySlice.has(sliceIndex)) {
335
- this.activeKeysBySlice.get(sliceIndex).clear();
336
- }
337
- this._emitFilter();
338
- this._cdr.markForCheck();
339
- }
340
- /** Retourne l'état des filtres actifs sous forme sérialisable (sliceIndex → tableau de clés).
341
- * Inclut les slices sans sélection (tableau vide) pour distinguer "aucune tranche sélectionnée"
342
- * (= filtre ouvert, rien coché) de "slice absente". */
343
- getActiveFilters() {
344
- const result = {};
345
- this._cachedSlices.forEach((_, idx) => {
346
- const keys = this.activeKeysBySlice.get(idx);
347
- result[idx] = keys ? [...keys] : [];
348
- });
349
- return result;
350
- }
351
- /** Restaure les filtres actifs depuis un snapshot sérialisé et réémet le filtre. */
352
- restoreFilters(filters) {
353
- this.activeKeysBySlice = new Map(Object.entries(filters).map(([idx, keys]) => [Number(idx), new Set(keys)]));
354
- this._emitFilter();
355
- this._cdr.markForCheck();
356
- }
357
- // ── Dynamic slice management
358
- addDynamicSlice(column) {
359
- const distinctValues = this._computeDistinctValues(column.key);
360
- const newSlice = {
361
- title: column.header,
362
- columnKey: column.key,
363
- categories: distinctValues.map((v) => this._buildCategoryFromValue(column.key, v)),
364
- };
365
- this._dynamicSlices = [...this._dynamicSlices, { key: column.key, slice: newSlice }];
366
- this._rebuildCache();
367
- this.dynamicSliceKeysChange.emit(this._dynamicSlices.map(d => d.key));
368
- this._emitFilter();
369
- this._cdr.markForCheck();
370
- }
371
- removeDynamicSlice(sliceIndex) {
372
- const dynamicIndex = sliceIndex - this.staticSliceCount;
373
- if (dynamicIndex < 0)
374
- return;
375
- const newActiveKeys = new Map();
376
- this.activeKeysBySlice.forEach((keys, idx) => {
377
- if (idx < sliceIndex)
378
- newActiveKeys.set(idx, keys);
379
- else if (idx > sliceIndex)
380
- newActiveKeys.set(idx - 1, keys);
381
- });
382
- this.activeKeysBySlice = newActiveKeys;
383
- this._dynamicSlices = this._dynamicSlices.filter((_, i) => i !== dynamicIndex);
384
- this._rebuildCache();
385
- this.dynamicSliceKeysChange.emit(this._dynamicSlices.map(d => d.key));
386
- this._emitFilter();
387
- this._cdr.markForCheck();
388
- }
389
- removeDynamicSliceByKey(key) {
390
- const dynamicIndex = this._dynamicSlices.findIndex((d) => d.key === key);
391
- if (dynamicIndex < 0)
392
- return;
393
- const sliceIndex = this.staticSliceCount + dynamicIndex;
394
- this.removeDynamicSlice(sliceIndex);
395
- }
396
- // ── Private helpers
397
- _emitFilter() {
398
- const slices = this.slices;
399
- const activeKeysBySlice = this.activeKeysBySlice;
400
- this.activeKeysChange.emit(slices.map((_, i) => {
401
- const keys = activeKeysBySlice.get(i);
402
- return keys ? [...keys] : [];
403
- }));
404
- const pred = (row) => slices.every((slice, sliceIndex) => {
405
- const activeKeys = activeKeysBySlice.get(sliceIndex);
406
- if (!activeKeys || activeKeys.size === 0)
407
- return true;
408
- return [...activeKeys].some((key) => {
409
- const category = (slice.categories || []).find((c) => c.key === key);
410
- if (!category)
411
- return false;
412
- // Si pas de filter défini (usage chart), on ne filtre pas en mémoire
413
- if (!category?.filter)
414
- return true;
415
- return category.filter(row);
416
- });
417
- });
418
- this.filterChange.emit(pred);
419
- }
420
- _getDynamicSliceColumnKey(sliceIndex) {
421
- const dynamicIndex = sliceIndex - this.staticSliceCount;
422
- return this._dynamicSlices[dynamicIndex]?.key ?? null;
423
- }
424
- _getSliceIdentity(slice, fallbackIndex) {
425
- if (slice.columnKey) {
426
- return `column:${slice.columnKey}`;
427
- }
428
- if (slice.title) {
429
- return `title:${slice.title}`;
430
- }
431
- const categoryKeys = (slice.categories || []).map((category) => category.key).join('|');
432
- return `index:${fallbackIndex}|categories:${categoryKeys}`;
433
- }
434
- _materializeSlice(slice) {
435
- if (!slice.columnKey || (slice.categories && slice.categories.length > 0)) {
436
- return slice;
437
- }
438
- const col = (this.columns || []).find(c => c.key === slice.columnKey);
439
- const isLazy = !!col?.lazy;
440
- if (isLazy && this.lazyStatus.get(slice.columnKey) !== 'loaded') {
441
- return { ...slice, categories: [] };
442
- }
443
- if (slice.bucket) {
444
- const distinctValues = [...new Set((this.data || []).map(slice.bucket))].sort();
445
- return {
446
- ...slice,
447
- categories: distinctValues.map((v) => ({
448
- key: v,
449
- label: v,
450
- filter: (row) => slice.bucket(row) === v,
451
- })),
452
- };
453
- }
454
- const distinctValues = this._computeDistinctValues(slice.columnKey);
455
- return {
456
- ...slice,
457
- categories: distinctValues.map((v) => this._buildCategoryFromValue(slice.columnKey, v)),
458
- };
459
- }
460
- _buildCategoryFromValue(columnKey, v) {
461
- if (v === EMPTY_CATEGORY_KEY) {
462
- return {
463
- key: EMPTY_CATEGORY_KEY,
464
- label: EMPTY_CATEGORY_LABEL,
465
- filter: (row) => {
466
- const val = this._getLazyAwareValue(columnKey, row);
467
- return val == null || val === '';
468
- },
469
- };
470
- }
471
- return {
472
- key: v,
473
- label: v,
474
- filter: (row) => String(this._getLazyAwareValue(columnKey, row) ?? '') === v,
475
- };
476
- }
477
- _computeDistinctValues(columnKey) {
478
- const rawValues = (this.data || []).map((row) => {
479
- const val = this._getLazyAwareValue(columnKey, row);
480
- return val != null && val !== '' ? String(val) : null;
481
- });
482
- const distinct = [...new Set(rawValues.filter((v) => v !== null))].sort();
483
- if (rawValues.some(v => v === null)) {
484
- distinct.push(EMPTY_CATEGORY_KEY);
485
- }
486
- return distinct;
487
- }
488
- _getLazyAwareValue(columnKey, row) {
489
- const colDef = (this.columns || []).find(c => c.key === columnKey);
490
- if (colDef?.lazy) {
491
- return this.lazyData.get(columnKey)?.get(row) ?? null;
492
- }
493
- if (colDef?.value) {
494
- return colDef.value(row, 0);
495
- }
496
- return row[columnKey];
497
- }
498
- _rebuildCache() {
499
- const previousSlices = this._cachedSlices;
500
- const previousActiveKeys = this.activeKeysBySlice;
501
- const allSlices = [...(this.sliceConfigs || []), ...this._dynamicSlices.map((d) => d.slice)];
502
- this._cachedSlices = allSlices.map((slice) => this._materializeSlice(slice));
503
- const previousKeysByIdentity = new Map();
504
- previousSlices.forEach((slice, index) => {
505
- const activeKeys = previousActiveKeys.get(index);
506
- if (!activeKeys || activeKeys.size === 0)
507
- return;
508
- previousKeysByIdentity.set(this._getSliceIdentity(slice, index), new Set(activeKeys));
509
- });
510
- const nextActiveKeys = new Map();
511
- this._cachedSlices.forEach((slice, index) => {
512
- const previousKeys = previousKeysByIdentity.get(this._getSliceIdentity(slice, index));
513
- if (!previousKeys || previousKeys.size === 0)
514
- return;
515
- const availableKeys = new Set((slice.categories || []).map((category) => category.key));
516
- const matchingKeys = [...previousKeys].filter((key) => availableKeys.has(key));
517
- if (matchingKeys.length > 0) {
518
- nextActiveKeys.set(index, new Set(matchingKeys));
519
- }
520
- });
521
- this.activeKeysBySlice = nextActiveKeys;
522
- this._countCache.clear();
523
- }
524
- _refreshDynamicSlices() {
525
- this._dynamicSlices = this._dynamicSlices.map(({ key, slice }) => {
526
- const distinctValues = this._computeDistinctValues(key);
527
- return {
528
- key,
529
- slice: {
530
- ...slice,
531
- categories: distinctValues.map((v) => this._buildCategoryFromValue(key, v)),
532
- },
533
- };
534
- });
535
- }
536
- _scrollSliceTitleIntoView(sliceIndex) {
537
- setTimeout(() => {
538
- const container = this.slicePanelBodyRef?.nativeElement;
539
- if (!container)
540
- return;
541
- const row = container.querySelector(`[data-slice-index="${sliceIndex}"]`);
542
- if (!row)
543
- return;
544
- row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
545
- }, 0);
546
- }
547
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SlicePanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
548
- 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 {{ 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 ? 'filter_list' : 'filter_list_off' }}</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 }); }
549
- }
550
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SlicePanelComponent, decorators: [{
551
- type: Component,
552
- 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 {{ 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 ? 'filter_list' : 'filter_list_off' }}</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"] }]
553
- }], propDecorators: { sliceConfigs: [{
554
- type: Input
555
- }], columns: [{
556
- type: Input
557
- }], data: [{
558
- type: Input
559
- }], lazyData: [{
560
- type: Input
561
- }], lazyStatus: [{
562
- type: Input
563
- }], showToggle: [{
564
- type: Input
565
- }], showCounts: [{
566
- type: Input
567
- }], collapsedByDefault: [{
568
- type: Input
569
- }], alwaysShow: [{
570
- type: Input
571
- }], filterChange: [{
572
- type: Output
573
- }], activeKeysChange: [{
574
- type: Output
575
- }], dynamicSliceKeysChange: [{
576
- type: Output
577
- }], collapsedChange: [{
578
- type: Output
579
- }], slicePanelBodyRef: [{
580
- type: ViewChild,
581
- args: ['slicePanelBody']
582
- }] } });
583
-
584
113
  /** Traductions françaises du paginator Material. */
585
114
  function getFrenchPaginatorIntl() {
586
115
  const intl = new MatPaginatorIntl();
@@ -639,435 +168,6 @@ function humanizeKey(key) {
639
168
  .replace(/^./, (value) => value.toUpperCase());
640
169
  }
641
170
 
642
- // ── Facade
643
- /**
644
- * Centralise l'état et les actions du panneau View (Champs / Group by / Slice by).
645
- * Pas de dépendance Angular/DOM : peut être extraite vers jquery-core à terme.
646
- *
647
- * Usage :
648
- * this._view = new ViewFacade();
649
- * this._view.events$.subscribe(e => { ... });
650
- * this._view.update({ config, columns, sliceConfigs });
651
- */
652
- class ViewFacade {
653
- constructor() {
654
- // ── State
655
- this.fields = {
656
- activeColumns: [],
657
- userCustomized: false,
658
- };
659
- this.groupBy = {
660
- activeKey: null,
661
- userCustomized: false,
662
- };
663
- this.sliceBy = {
664
- activeDynamicColumns: [],
665
- availableForDynamic: [],
666
- allDynamicColumns: [],
667
- activeLabel: 'Aucun',
668
- };
669
- this._panelConfig = { columns: [], sliceConfigs: [] };
670
- this._hiddenStaticKeys = new Set();
671
- // ── Events
672
- this._events$ = new Subject();
673
- this.events$ = this._events$.asObservable();
674
- }
675
- // ── Computed flags
676
- get enabled() {
677
- return this._panelConfig.config?.enabled === true;
678
- }
679
- get showFields() {
680
- return this.enabled;
681
- }
682
- get showGroupBySection() {
683
- return this.enabled && this.groupByColumns.length > 0;
684
- }
685
- get showSliceBySection() {
686
- return this.enabled && ((this._panelConfig.sliceConfigs?.length ?? 0) > 0 ||
687
- this.sliceBy.allDynamicColumns.length > 0);
688
- }
689
- get allowColumnRemoval() {
690
- return this._panelConfig.config?.enableColumnRemoval !== false;
691
- }
692
- get isColumnDragDropEnabled() {
693
- return this._panelConfig.config?.enableColumnDragDrop === true;
694
- }
695
- get menuBaseColumns() {
696
- return this._uniqueByKey((this._panelConfig.columns || []).filter(c => !c.optional));
697
- }
698
- get menuOptionalColumns() {
699
- return this._uniqueByKey((this._panelConfig.columns || []).filter(c => c.optional));
700
- }
701
- get totalColumnCount() {
702
- return new Set((this._panelConfig.columns || []).map(c => c.key)).size;
703
- }
704
- get groupByColumns() {
705
- const seen = new Set();
706
- return (this._panelConfig.columns || []).filter(c => {
707
- if (c.groupable === false || seen.has(c.key))
708
- return false;
709
- if (!c.header && c.groupable !== true)
710
- return false;
711
- seen.add(c.key);
712
- return true;
713
- });
714
- }
715
- get activeGroupByLabel() {
716
- if (!this.groupBy.activeKey)
717
- return this._panelConfig.noneLabel ?? 'Aucun';
718
- const col = this.groupByColumns.find(c => c.key === this.groupBy.activeKey);
719
- return col?.header || this.groupBy.activeKey;
720
- }
721
- get staticSlicesForMenu() {
722
- return (this._panelConfig.sliceConfigs ?? [])
723
- .filter(s => !!s.columnKey)
724
- .map(s => {
725
- const colIcon = (this._panelConfig.columns ?? []).find(c => c.key === s.columnKey)?.icon;
726
- return { key: s.columnKey, title: s.title || humanizeKey(s.columnKey), icon: s.icon ?? colIcon };
727
- });
728
- }
729
- colLabel(col) {
730
- return col.header || humanizeKey(col.key);
731
- }
732
- // ── Mise à jour de la config (appelée depuis refreshViewModel)
733
- update(panelConfig) {
734
- this._panelConfig = panelConfig;
735
- this._refreshDefaultColumns();
736
- this._refreshDynamicSliceMeta();
737
- if (!this.groupBy.userCustomized) {
738
- this.groupBy.activeKey = panelConfig.defaultGroupBy ?? null;
739
- }
740
- }
741
- // ── Actions Fields
742
- setActiveColumns(columns) {
743
- this.fields.activeColumns = columns;
744
- this.fields.userCustomized = true;
745
- }
746
- setUserCustomized(value) {
747
- this.fields.userCustomized = value;
748
- }
749
- /** Remet la vue dans son état initial : colonnes par défaut, groupBy par défaut, pas de dynamic slices. */
750
- resetToDefaults() {
751
- this.fields.userCustomized = false;
752
- this.groupBy.userCustomized = false;
753
- this._refreshDefaultColumns();
754
- this.groupBy.activeKey = this._panelConfig.defaultGroupBy ?? null;
755
- this.sliceBy.activeDynamicColumns = [];
756
- this._refreshDynamicSliceMeta();
757
- }
758
- addColumn(column) {
759
- const configCols = this._panelConfig.columns ?? [];
760
- const configIndex = configCols.findIndex(c => c.key === column.key);
761
- if (configIndex === -1) {
762
- this.fields.activeColumns = [...this.fields.activeColumns, column];
763
- }
764
- else {
765
- const insertBefore = this.fields.activeColumns.findIndex(c => {
766
- const ci = configCols.findIndex(cc => cc.key === c.key);
767
- return ci !== -1 && ci > configIndex;
768
- });
769
- if (insertBefore === -1) {
770
- this.fields.activeColumns = [...this.fields.activeColumns, column];
771
- }
772
- else {
773
- const copy = [...this.fields.activeColumns];
774
- copy.splice(insertBefore, 0, column);
775
- this.fields.activeColumns = copy;
776
- }
777
- }
778
- this.fields.userCustomized = true;
779
- this._events$.next({ type: 'fieldsChanged', fieldIds: this.fields.activeColumns.map(c => c.key) });
780
- }
781
- removeColumn(columnKey) {
782
- const col = this.fields.activeColumns.find(c => c.key === columnKey);
783
- if (!col)
784
- return false;
785
- if (!col.optional && !this.allowColumnRemoval)
786
- return false;
787
- if (this.fields.activeColumns.length <= 1)
788
- return false;
789
- this.fields.activeColumns = this.fields.activeColumns.filter(c => c.key !== columnKey);
790
- this.fields.userCustomized = true;
791
- this._events$.next({ type: 'fieldsChanged', fieldIds: this.fields.activeColumns.map(c => c.key) });
792
- return true;
793
- }
794
- toggleColumnVisibility(column, visible) {
795
- const isVisible = this.isColumnVisible(column.key);
796
- if (visible && !isVisible) {
797
- this.addColumn(column);
798
- }
799
- else if (!visible && isVisible) {
800
- this.removeColumn(column.key);
801
- }
802
- }
803
- reorderColumns(fromKey, toKey) {
804
- const prev = this.fields.activeColumns.findIndex(c => c.key === fromKey);
805
- const curr = this.fields.activeColumns.findIndex(c => c.key === toKey);
806
- if (prev === -1 || curr === -1 || prev === curr)
807
- return false;
808
- const reordered = [...this.fields.activeColumns];
809
- const [moved] = reordered.splice(prev, 1);
810
- reordered.splice(curr, 0, moved);
811
- this.fields.activeColumns = reordered;
812
- this.fields.userCustomized = true;
813
- this._events$.next({ type: 'fieldsChanged', fieldIds: this.fields.activeColumns.map(c => c.key) });
814
- return true;
815
- }
816
- isColumnVisible(key) {
817
- return this.fields.activeColumns.some(c => c.key === key);
818
- }
819
- // ── Actions Group by
820
- setGroupBy(key) {
821
- this.groupBy.activeKey = key;
822
- this.groupBy.userCustomized = true;
823
- this._events$.next({ type: 'groupByChanged', key });
824
- }
825
- // ── Actions Slice by (dynamique)
826
- /** Appelée quand le SlicePanelComponent émet dynamicSliceKeysChange */
827
- onDynamicSliceKeysChange(keys) {
828
- const allCols = this._panelConfig.columns ?? [];
829
- const staticKeys = new Set((this._panelConfig.sliceConfigs ?? []).map(s => s.columnKey).filter((k) => !!k));
830
- const dynamicKeySet = new Set(keys);
831
- const existingKeys = new Set([...staticKeys, ...dynamicKeySet]);
832
- this.sliceBy.activeDynamicColumns = keys
833
- .map(k => allCols.find(c => c.key === k))
834
- .filter((c) => !!c);
835
- const seen = new Set();
836
- this.sliceBy.availableForDynamic = allCols.filter(c => {
837
- if (c.sliceable === false || existingKeys.has(c.key) || seen.has(c.key))
838
- return false;
839
- seen.add(c.key);
840
- return true;
841
- });
842
- this._computeSliceLabel();
843
- this._events$.next({ type: 'dynamicSlicesChanged', keys });
844
- }
845
- isDynamicSliceActive(key) {
846
- return this.sliceBy.activeDynamicColumns.some(c => c.key === key);
847
- }
848
- // ── Contrat core
849
- /** Retourne un snapshot ViewState (contrat core) de l'état courant. */
850
- toViewState() {
851
- return {
852
- selectedFieldIds: this.fields.activeColumns.map(c => c.key),
853
- groupByKey: this.groupBy.activeKey,
854
- dynamicSliceKeys: this.sliceBy.activeDynamicColumns.map(c => c.key),
855
- };
856
- }
857
- /** Convertit une TableColumnProvider en ViewField (contrat core). */
858
- static toViewField(col) {
859
- return {
860
- id: col.key,
861
- label: col.header,
862
- icon: col.icon,
863
- groupable: col.groupable,
864
- sliceable: col.sliceable,
865
- optional: col.optional,
866
- };
867
- }
868
- // ── Destructor
869
- destroy() {
870
- this._events$.complete();
871
- }
872
- // ── Helpers privés
873
- _refreshDefaultColumns() {
874
- const defaultCols = (this._panelConfig.columns || []).filter(c => !c.optional);
875
- if (!this.fields.userCustomized || this.fields.activeColumns.length === 0) {
876
- this.fields.activeColumns = [...defaultCols];
877
- }
878
- else {
879
- const allowedKeys = new Set((this._panelConfig.columns || []).map(c => c.key));
880
- this.fields.activeColumns = this.fields.activeColumns.filter(c => allowedKeys.has(c.key));
881
- }
882
- }
883
- _refreshDynamicSliceMeta() {
884
- const allCols = this._panelConfig.columns ?? [];
885
- const staticKeys = new Set((this._panelConfig.sliceConfigs ?? []).map(s => s.columnKey).filter((k) => !!k));
886
- const activeDynKeys = new Set(this.sliceBy.activeDynamicColumns.map(c => c.key));
887
- const seen1 = new Set();
888
- this.sliceBy.availableForDynamic = allCols.filter(c => {
889
- if (c.sliceable === false || staticKeys.has(c.key) || activeDynKeys.has(c.key) || seen1.has(c.key))
890
- return false;
891
- if (!c.header && c.sliceable !== true)
892
- return false;
893
- seen1.add(c.key);
894
- return true;
895
- });
896
- const seen2 = new Set();
897
- this.sliceBy.allDynamicColumns = allCols.filter(c => {
898
- if (c.sliceable === false || staticKeys.has(c.key) || seen2.has(c.key))
899
- return false;
900
- if (!c.header && c.sliceable !== true)
901
- return false;
902
- seen2.add(c.key);
903
- return true;
904
- });
905
- this._computeSliceLabel();
906
- }
907
- /** Notifie la facade des slices statiques actuellement cachées (géré par table.component).
908
- * Recalcule le label slice afin qu'il reflète uniquement les slices visibles. */
909
- updateHiddenStaticKeys(keys) {
910
- this._hiddenStaticKeys = keys;
911
- this._computeSliceLabel();
912
- }
913
- _computeSliceLabel() {
914
- const visibleStatics = (this._panelConfig.sliceConfigs ?? []).filter(s => !s.columnKey || !this._hiddenStaticKeys.has(s.columnKey));
915
- const visibleStaticCount = visibleStatics.length;
916
- const total = visibleStaticCount + this.sliceBy.activeDynamicColumns.length;
917
- if (total === 0) {
918
- this.sliceBy.activeLabel = this._panelConfig.noneLabel ?? 'Aucun';
919
- return;
920
- }
921
- if (total === 1) {
922
- if (visibleStaticCount === 1) {
923
- const first = visibleStatics[0];
924
- this.sliceBy.activeLabel = first?.title || first?.columnKey || '1';
925
- return;
926
- }
927
- this.sliceBy.activeLabel = this.sliceBy.activeDynamicColumns[0]?.header || '1';
928
- return;
929
- }
930
- this.sliceBy.activeLabel = `${total} actifs`;
931
- }
932
- _uniqueByKey(cols) {
933
- const seen = new Set();
934
- return cols.filter(c => {
935
- if (seen.has(c.key))
936
- return false;
937
- seen.add(c.key);
938
- return true;
939
- });
940
- }
941
- }
942
-
943
- /**
944
- * Constantes sentinel utilisées pour les identifiants de colonnes et les valeurs
945
- * projetées dans le datasource interne de la table.
946
- *
947
- * Centraliser ici évite les typos silencieuses et facilite les tests unitaires.
948
- */
949
- /** Identifiant de la colonne virtuelle rendue sur toute la largeur pour les en-têtes de groupe. */
950
- const GROUP_ROW_COLUMN = '__group_row';
951
- /** Marqueur posé sur les objets de ligne pour signaler une ligne d'en-tête de groupe. */
952
- const GROUP_HEADER_MARKER = '__groupHeader';
953
- /** Clé du groupe spéciale « chargement lazy en cours ». */
954
- const GROUP_KEY_LAZY_LOADING = '__lazy_loading__';
955
- /** Clé du groupe spéciale « trop de groupes ». */
956
- const GROUP_KEY_TOO_MANY = '__too_many_groups__';
957
- /** Propriété injectée dans chaque ligne projetée : référence vers la ligne brute d'origine. */
958
- const ROW_RAW_KEY = '__raw';
959
- /** Propriété injectée dans chaque ligne projetée : index global de la ligne dans le jeu de données filtré. */
960
- const ROW_INDEX_KEY = '__index';
961
- /** Propriété injectée dans chaque ligne projetée : clé du groupe auquel appartient la ligne d'en-tête. */
962
- const ROW_GROUP_KEY = '__groupKey';
963
- /** Propriété injectée dans chaque ligne projetée : nombre de lignes dans le groupe. */
964
- const ROW_GROUP_COUNT = '__groupCount';
965
- /** Propriété injectée dans chaque ligne projetée : page courante du groupe. */
966
- const ROW_GROUP_PAGE = '__groupPage';
967
- /** Propriété injectée dans chaque ligne projetée : nombre de pages du groupe. */
968
- const ROW_GROUP_PAGE_COUNT = '__groupPageCount';
969
- /** Valeur sentinelle posée dans une cellule lazy en cours de chargement. */
970
- const LAZY_LOADING_VALUE = '__lazy_loading__';
971
- /** Valeur sentinelle posée dans une cellule lazy en erreur. */
972
- const LAZY_ERROR_VALUE = '__lazy_error__';
973
-
974
- /**
975
- * Gère le cycle de vie des colonnes à chargement différé (lazy) :
976
- * statuts, données, annulation des requêtes en cours.
977
- *
978
- * Pas de dépendance Angular/DOM — peut être instancié manuellement dans le composant.
979
- */
980
- class LazyColumnManager {
981
- constructor() {
982
- this._status = new Map();
983
- this._data = new Map();
984
- this._cancels = new Map();
985
- }
986
- get status() {
987
- return this._status;
988
- }
989
- get data() {
990
- return this._data;
991
- }
992
- getStatus(key) {
993
- return this._status.get(key) ?? 'idle';
994
- }
995
- getRenderType(rowValue) {
996
- if (rowValue === LAZY_LOADING_VALUE)
997
- return 'loading';
998
- if (rowValue === LAZY_ERROR_VALUE)
999
- return 'error';
1000
- return 'value';
1001
- }
1002
- /** Annule tous les fetches en cours et réinitialise l'état (appeler lors d'un changement de données). */
1003
- cancelAll() {
1004
- this._cancels.forEach(s => { s.next(); s.complete(); });
1005
- this._cancels.clear();
1006
- this._status = new Map();
1007
- this._data = new Map();
1008
- }
1009
- /** Réinitialise une colonne et relance son fetch. */
1010
- retry(column, resolvedData, callbacks) {
1011
- const k = column.key;
1012
- const prev = this._cancels.get(k);
1013
- if (prev) {
1014
- prev.next();
1015
- prev.complete();
1016
- }
1017
- this._cancels.delete(k);
1018
- const newStatus = new Map(this._status);
1019
- newStatus.delete(k);
1020
- this._status = newStatus;
1021
- const newData = new Map(this._data);
1022
- newData.delete(k);
1023
- this._data = newData;
1024
- this.fetch(column, resolvedData, callbacks);
1025
- }
1026
- /** Lance le chargement différé d'une colonne. No-op si déjà en cours. */
1027
- fetch(column, resolvedData, callbacks) {
1028
- if (!column.lazy)
1029
- return;
1030
- const k = column.key;
1031
- if (this._status.get(k) === 'loading')
1032
- return;
1033
- const prev = this._cancels.get(k);
1034
- if (prev) {
1035
- prev.next();
1036
- prev.complete();
1037
- }
1038
- const cancel$ = new Subject();
1039
- this._cancels.set(k, cancel$);
1040
- const newStatus = new Map(this._status);
1041
- newStatus.set(k, 'loading');
1042
- this._status = newStatus;
1043
- callbacks.onRefresh();
1044
- column.lazy.fetchFn().pipe(take(1), takeUntil(cancel$)).subscribe({
1045
- next: (values) => {
1046
- const rowMap = new Map();
1047
- values.forEach((value, i) => {
1048
- if (i < resolvedData.length)
1049
- rowMap.set(resolvedData[i], value);
1050
- });
1051
- const newData = new Map(this._data);
1052
- newData.set(k, rowMap);
1053
- this._data = newData;
1054
- const newStatus2 = new Map(this._status);
1055
- newStatus2.set(k, 'loaded');
1056
- this._status = newStatus2;
1057
- callbacks.onRefresh();
1058
- callbacks.onMarkForCheck();
1059
- },
1060
- error: () => {
1061
- const newStatusErr = new Map(this._status);
1062
- newStatusErr.set(k, 'error');
1063
- this._status = newStatusErr;
1064
- callbacks.onRefresh();
1065
- callbacks.onMarkForCheck();
1066
- },
1067
- });
1068
- }
1069
- }
1070
-
1071
171
  const STORAGE_PREFIX = 'jqt_prefs_';
1072
172
  /**
1073
173
  * Gère la persistance de la configuration d'un tableau dans le localStorage.
@@ -1106,105 +206,6 @@ class TablePreferencesManager {
1106
206
  }
1107
207
  }
1108
208
 
1109
- /**
1110
- * GroupByManager
1111
- *
1112
- * Encapsule tout l'état et la logique liés au mode Group By :
1113
- * - état collapsed / expanded des groupes
1114
- * - pagination interne par groupe
1115
- * - tri des groupes (asc / desc)
1116
- * - scroll vers un groupe
1117
- *
1118
- * Le composant TableComponent l'instancie et lui passe deux callbacks :
1119
- * - `onNeedProjection` : demande une reprojection légère de la page courante
1120
- * - `onNeedRender` : demande un rebuild complet (tri changé etc.)
1121
- */
1122
- class GroupByManager {
1123
- constructor(onNeedProjection, onNeedRender) {
1124
- this.onNeedProjection = onNeedProjection;
1125
- this.onNeedRender = onNeedRender;
1126
- /** Groupes actuellement ouverts (mode default-collapsed) ou fermés (mode default-expanded). */
1127
- this._collapsedGroups = new Set();
1128
- /** Page courante par clé de groupe. */
1129
- this._groupPages = new Map();
1130
- /** Taille de page pour la pagination interne des groupes. */
1131
- this.groupPageSize = 5;
1132
- /** Ordre de tri des groupes ('asc' | 'desc'). */
1133
- this.groupSortOrder = 'asc';
1134
- /**
1135
- * Quand `true`, tous les groupes sont collapsés par défaut.
1136
- * `_collapsedGroups` contient alors le seul groupe ouvert.
1137
- */
1138
- this._defaultGroupCollapsed = false;
1139
- }
1140
- // ── API publique
1141
- /** Initialise le mode par défaut selon qu'un groupBy vient d'être activé. */
1142
- setDefaultCollapsed(collapsed) {
1143
- this._defaultGroupCollapsed = collapsed;
1144
- }
1145
- /** Réinitialise l'état (à appeler quand le groupBy change de colonne). */
1146
- reset() {
1147
- this._collapsedGroups.clear();
1148
- this._groupPages.clear();
1149
- }
1150
- isGroupCollapsed(groupKey) {
1151
- return this._defaultGroupCollapsed
1152
- ? !this._collapsedGroups.has(groupKey)
1153
- : this._collapsedGroups.has(groupKey);
1154
- }
1155
- toggleGroupCollapse(groupKey, scrollTo) {
1156
- if (this._collapsedGroups.has(groupKey)) {
1157
- this._collapsedGroups.delete(groupKey);
1158
- }
1159
- else {
1160
- this._collapsedGroups.clear();
1161
- this._collapsedGroups.add(groupKey);
1162
- scrollTo(groupKey);
1163
- }
1164
- this.onNeedProjection();
1165
- }
1166
- /** Retourne le groupe actuellement ouvert (seulement en mode default-collapsed avec exactement 1 ouvert). */
1167
- getSingleOpenGroup() {
1168
- return this._defaultGroupCollapsed && this._collapsedGroups.size === 1
1169
- ? [...this._collapsedGroups][0]
1170
- : null;
1171
- }
1172
- // ── Pagination par groupe
1173
- getPage(groupKey) {
1174
- return this._groupPages.get(groupKey) ?? 0;
1175
- }
1176
- groupPageBack(groupKey) {
1177
- const current = this._groupPages.get(groupKey) ?? 0;
1178
- if (current > 0) {
1179
- this._groupPages.set(groupKey, current - 1);
1180
- this.onNeedProjection();
1181
- }
1182
- }
1183
- groupPageForward(groupKey, totalCount) {
1184
- const maxPage = Math.ceil(totalCount / this.groupPageSize) - 1;
1185
- const current = this._groupPages.get(groupKey) ?? 0;
1186
- if (current < maxPage) {
1187
- this._groupPages.set(groupKey, current + 1);
1188
- this.onNeedProjection();
1189
- }
1190
- }
1191
- onGroupPageSizeChange(size) {
1192
- this.groupPageSize = size;
1193
- this._groupPages.clear();
1194
- this.onNeedRender();
1195
- }
1196
- onGroupSortToggle(dir, scrollTo) {
1197
- if (this.groupSortOrder === dir)
1198
- return;
1199
- this.groupSortOrder = dir;
1200
- this._groupPages.clear();
1201
- this.onNeedRender();
1202
- const openGroup = this.getSingleOpenGroup();
1203
- if (openGroup)
1204
- scrollTo(openGroup);
1205
- }
1206
- }
1207
-
1208
209
  /**
1209
210
  * ExportManager
1210
211
  *
@@ -1250,6 +251,83 @@ class ExportManager {
1250
251
  }
1251
252
  }
1252
253
 
254
+ /**
255
+ * Constantes sentinel utilisées pour les identifiants de colonnes et les valeurs
256
+ * projetées dans le datasource interne de la table.
257
+ *
258
+ * Centraliser ici évite les typos silencieuses et facilite les tests unitaires.
259
+ */
260
+ /** Identifiant de la colonne virtuelle rendue sur toute la largeur pour les en-têtes de groupe. */
261
+ const GROUP_ROW_COLUMN = '__group_row';
262
+ /** Marqueur posé sur les objets de ligne pour signaler une ligne d'en-tête de groupe. */
263
+ const GROUP_HEADER_MARKER = '__groupHeader';
264
+ /** Clé du groupe spéciale « chargement lazy en cours ». */
265
+ const GROUP_KEY_LAZY_LOADING = '__lazy_loading__';
266
+ /** Clé du groupe spéciale « trop de groupes ». */
267
+ const GROUP_KEY_TOO_MANY = '__too_many_groups__';
268
+ /** Propriété injectée dans chaque ligne projetée : référence vers la ligne brute d'origine. */
269
+ const ROW_RAW_KEY = '__raw';
270
+ /** Propriété injectée dans chaque ligne projetée : index global de la ligne dans le jeu de données filtré. */
271
+ const ROW_INDEX_KEY = '__index';
272
+ /** Propriété injectée dans chaque ligne projetée : clé du groupe auquel appartient la ligne d'en-tête. */
273
+ const ROW_GROUP_KEY = '__groupKey';
274
+ /** Propriété injectée dans chaque ligne projetée : nombre de lignes dans le groupe. */
275
+ const ROW_GROUP_COUNT = '__groupCount';
276
+ /** Propriété injectée dans chaque ligne projetée : page courante du groupe. */
277
+ const ROW_GROUP_PAGE = '__groupPage';
278
+ /** Propriété injectée dans chaque ligne projetée : nombre de pages du groupe. */
279
+ const ROW_GROUP_PAGE_COUNT = '__groupPageCount';
280
+ /** Valeur sentinelle posée dans une cellule lazy en cours de chargement. */
281
+ const LAZY_LOADING_VALUE = '__lazy_loading__';
282
+ /** Valeur sentinelle posée dans une cellule lazy en erreur. */
283
+ const LAZY_ERROR_VALUE = '__lazy_error__';
284
+
285
+ class OrganizerButtonWrapperComponent {
286
+ constructor() {
287
+ this.hideMenuValues = false;
288
+ this.viewChange = new EventEmitter();
289
+ this.sliceStateChange = new EventEmitter();
290
+ }
291
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: OrganizerButtonWrapperComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
292
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: OrganizerButtonWrapperComponent, isStandalone: true, selector: "table-organizer-button", inputs: { config: "config", state: "state", hideMenuValues: "hideMenuValues" }, outputs: { viewChange: "viewChange", sliceStateChange: "sliceStateChange" }, ngImport: i0, template: `
293
+ <organizer-button
294
+ [config]="config"
295
+ [state]="state"
296
+ [hideMenuValues]="hideMenuValues"
297
+ (viewChange)="viewChange.emit($event)"
298
+ (sliceStateChange)="sliceStateChange.emit($event)">
299
+ </organizer-button>
300
+ `, isInline: true, dependencies: [{ kind: "component", type: OrganizerButtonComponent, selector: "organizer-button", inputs: ["config", "state", "hideMenuValues"], outputs: ["viewChange", "sliceStateChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
301
+ }
302
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: OrganizerButtonWrapperComponent, decorators: [{
303
+ type: Component,
304
+ args: [{
305
+ standalone: true,
306
+ selector: 'table-organizer-button',
307
+ imports: [OrganizerButtonComponent],
308
+ template: `
309
+ <organizer-button
310
+ [config]="config"
311
+ [state]="state"
312
+ [hideMenuValues]="hideMenuValues"
313
+ (viewChange)="viewChange.emit($event)"
314
+ (sliceStateChange)="sliceStateChange.emit($event)">
315
+ </organizer-button>
316
+ `,
317
+ changeDetection: ChangeDetectionStrategy.OnPush
318
+ }]
319
+ }], propDecorators: { config: [{
320
+ type: Input
321
+ }], state: [{
322
+ type: Input
323
+ }], hideMenuValues: [{
324
+ type: Input
325
+ }], viewChange: [{
326
+ type: Output
327
+ }], sliceStateChange: [{
328
+ type: Output
329
+ }] } });
330
+
1253
331
  class TableComponent {
1254
332
  constructor() {
1255
333
  this.isLoading = false;
@@ -1282,12 +360,12 @@ class TableComponent {
1282
360
  this._ROW_GROUP_PAGE = ROW_GROUP_PAGE;
1283
361
  this._ROW_GROUP_PAGE_COUNT = ROW_GROUP_PAGE_COUNT;
1284
362
  /** Facade View : état et actions centralisés (Champs / Group by / Slice by) */
1285
- this._view = new ViewFacade();
363
+ this._organizer = new OrganizerFacade();
1286
364
  /**
1287
- * Snapshot stable synchronisé depuis _view.fields.activeColumns dans refreshViewModel().
365
+ * Snapshot stable synchronisé depuis _organizer.fields.activeFields dans refreshViewModel().
1288
366
  * Propriété (non getter) pour qu'Angular puisse la suivre sans NG0100.
1289
367
  */
1290
- this.activeColumns = [];
368
+ this.activeFields = [];
1291
369
  this.activeSliceFilter = () => true;
1292
370
  // Propriétés stables pour les bindings du template (contre erreur NG0100)
1293
371
  this._sliceConfigs = [];
@@ -1309,7 +387,7 @@ class TableComponent {
1309
387
  /** Gestionnaire du mode Group By (état collapsed, pagination, tri des groupes). */
1310
388
  this._groupBy = new GroupByManager(() => { this._projectCurrentPage(); this._cdr.markForCheck(); }, () => this.refreshViewModel());
1311
389
  /** Gestionnaire du cycle de vie des colonnes lazy (fetch, annulation, statuts, données). */
1312
- this._lazy = new LazyColumnManager();
390
+ this._lazy = new LazyFieldManager();
1313
391
  this._cdr = inject(ChangeDetectorRef);
1314
392
  this._el = inject((ElementRef));
1315
393
  this._ngZone = inject(NgZone);
@@ -1318,7 +396,7 @@ class TableComponent {
1318
396
  this.i18n = { ...JQT_I18N_DEFAULTS, ...(this._i18nRaw ?? {}) };
1319
397
  this.isGroupHeader = (_, row) => row[GROUP_HEADER_MARKER] === true;
1320
398
  /** Gestionnaire export CSV. */
1321
- this._export = new ExportManager(() => this._allFilteredRows, () => this.activeColumns, () => this.resolvedConfig.export, (col, row, idx) => this.resolveCellValue(col, row, idx));
399
+ this._export = new ExportManager(() => this._allFilteredRows, () => this.activeFields, () => this.resolvedConfig.export, (col, row, idx) => this.resolveCellValue(col, row, idx));
1322
400
  // ── Lifecycle
1323
401
  this.pageSize = 5;
1324
402
  this.pageSizeOptions = [5, 10, 20];
@@ -1356,6 +434,7 @@ class TableComponent {
1356
434
  this._configHiddenKeys = new Set();
1357
435
  /** Clés des colonnes visibles du dernier émission columnsChange (pour éviter les émissions dupliquement). */
1358
436
  this._prevColumnKeys = [];
437
+ this._organizerConfigCache = null;
1359
438
  this.trackRowFn = (_index, row) => row[GROUP_HEADER_MARKER] ? `__group_${row[ROW_GROUP_KEY]}` : row[ROW_INDEX_KEY] ?? _index;
1360
439
  this._preferencesMessageTimer = null;
1361
440
  }
@@ -1375,12 +454,12 @@ class TableComponent {
1375
454
  }
1376
455
  get paginator() { return this._paginator; }
1377
456
  // Délégués vers _view (conservés pour compatibilité template sans refactoring HTML)
1378
- get _staticSlicesForMenu() { return this._view.staticSlicesForMenu; }
1379
- get _activeDynamicSliceColumns() { return this._view.sliceBy.activeDynamicColumns; }
1380
- get _availableColumnsForDynamicSlice() { return this._view.sliceBy.availableForDynamic; }
1381
- get _allDynamicSliceColumns() { return this._view.sliceBy.allDynamicColumns; }
1382
- get _activeSliceByLabel() { return this._view.sliceBy.activeLabel; }
1383
- get activeGroupByKey() { return this._view.groupBy.activeKey; }
457
+ get _staticSlicesForMenu() { return this._organizer.staticSlicesForMenu; }
458
+ get _activeDynamicSliceColumns() { return this._organizer.sliceBy.activeDynamicFields; }
459
+ get _availableColumnsForDynamicSlice() { return this._organizer.sliceBy.availableForDynamic; }
460
+ get _allDynamicSliceColumns() { return this._organizer.sliceBy.allDynamicFields; }
461
+ get _activeSliceByLabel() { return this._organizer.sliceBy.activeLabel; }
462
+ get activeGroupByKey() { return this._organizer.groupBy.activeKey; }
1384
463
  /** Délégués vers GroupByManager pour la compatibilité des templates. */
1385
464
  get groupPageSize() { return this._groupBy.groupPageSize; }
1386
465
  set groupPageSize(v) { this._groupBy.groupPageSize = v; }
@@ -1413,9 +492,10 @@ class TableComponent {
1413
492
  if (changes['config']) {
1414
493
  this.applyConfigSearchReset();
1415
494
  this.applyInitialSearchQuery();
495
+ this._invalidateOrganizerConfig(); // les colonnes ont pu changer
1416
496
  }
1417
497
  if (dataChanged) {
1418
- this.activeColumns.filter(c => c.lazy).forEach(c => this.triggerLazyFetch(c));
498
+ this.activeFields.filter(c => c.lazy).forEach(c => this.triggerLazyFetch(c));
1419
499
  }
1420
500
  }
1421
501
  ngAfterContentInit() {
@@ -1441,25 +521,24 @@ class TableComponent {
1441
521
  const filters = this._pendingSliceFilters ? { ...this._pendingSliceFilters } : null;
1442
522
  this._pendingDynamicSliceKeys = null;
1443
523
  this._pendingSliceFilters = null;
1444
- // Tout est différé dans le même setTimeout pour :
1445
- // 1. Éviter NG0100 (modifications après vérification dans ngAfterViewInit)
1446
- // 2. Garantir que les dynamic slices sont ajoutées AVANT la restauration des filtres
1447
- // (les indices dans sliceFilters dépendent de l'ordre final de _cachedSlices)
1448
524
  setTimeout(() => {
525
+ if (this._resolvedData.length === 0) {
526
+ console.log('[SliceRestore] ngAfterViewInit: données absentes, restauration différée à _recomputeShowSlicePanel', { keys, filters });
527
+ if (filters)
528
+ this._pendingSliceFilters = filters;
529
+ if (keys.length > 0)
530
+ this._pendingDynamicSliceKeys = [...keys];
531
+ return;
532
+ }
533
+ console.log('[SliceRestore] ngAfterViewInit: données présentes, application immédiate', { keys, filters });
1449
534
  keys.forEach(key => {
1450
535
  const col = allCols.find(c => c.key === key);
1451
536
  if (col)
1452
537
  this.addDynamicSlice(col);
1453
538
  });
1454
- if (filters) {
1455
- if (this.slicePanelRef) {
1456
- this.slicePanelRef.restoreFilters(filters);
1457
- }
1458
- else {
1459
- // slicePanelRef toujours null (données pas encore arrivées) :
1460
- // remettre en attente pour que _recomputeShowSlicePanel les applique
1461
- this._pendingSliceFilters = filters;
1462
- }
539
+ if (filters && this.slicePanelRef) {
540
+ console.log('[SliceRestore] restoreFilters depuis ngAfterViewInit', filters);
541
+ this.slicePanelRef.restoreFilters(filters);
1463
542
  }
1464
543
  this._cdr.markForCheck();
1465
544
  });
@@ -1475,7 +554,7 @@ class TableComponent {
1475
554
  this._resizeState = null;
1476
555
  this._destroy$.next();
1477
556
  this._destroy$.complete();
1478
- this._view.destroy();
557
+ this._organizer.destroy();
1479
558
  }
1480
559
  _measureHeaderHeight() {
1481
560
  const measure = () => {
@@ -1492,7 +571,73 @@ class TableComponent {
1492
571
  };
1493
572
  this._ngZone.runOutsideAngular(() => requestAnimationFrame(() => measure()));
1494
573
  }
1495
- // ── Getters d'affichage
574
+ _invalidateOrganizerConfig() { this._organizerConfigCache = null; }
575
+ get organizerConfig() {
576
+ if (this._organizerConfigCache)
577
+ return this._organizerConfigCache;
578
+ const allCols = [...this._organizer.menuBaseFields, ...this._organizer.menuOptionalFields];
579
+ this._organizerConfigCache = {
580
+ fields: this._organizer.showFields ? allCols.map(c => ({
581
+ id: c.key,
582
+ label: this.colLabel(c),
583
+ icon: c.icon,
584
+ state: 'ready',
585
+ })) : undefined,
586
+ groups: this._organizer.showGroupBySection ? this._organizer.groupByFields.map(c => ({
587
+ id: c.key,
588
+ label: this.colLabel(c),
589
+ icon: c.icon,
590
+ })) : undefined,
591
+ slices: this._organizer.showSliceBySection ? this._sliceByMenuItems.map(item => ({
592
+ id: item.key,
593
+ label: item.title,
594
+ icon: item.icon,
595
+ })) : undefined,
596
+ showExport: this.showExportButton,
597
+ onExport: () => this.onExport(),
598
+ showPreferences: this.showPreferencesMenu,
599
+ hasSavedPreferences: this.hasSavedPreferences,
600
+ onPreferencesEdit: () => this.enterEditMode(),
601
+ onPreferencesSave: () => this.savePreferences(),
602
+ onPreferencesClear: () => this.clearPreferences(),
603
+ };
604
+ return this._organizerConfigCache;
605
+ }
606
+ get organizerState() {
607
+ return {
608
+ visibleFields: this.visibleColumns.map(c => c.key),
609
+ selectedGroupBy: this.activeGroupByKey ?? undefined,
610
+ selectedSlices: this._sliceByMenuItems
611
+ .filter(i => this.isSliceMenuItemActive(i))
612
+ .map(i => i.key),
613
+ };
614
+ }
615
+ onOrganizerViewChange(event) {
616
+ if (event.type === 'fieldToggled') {
617
+ const allCols = [...this._organizer.menuBaseFields, ...this._organizer.menuOptionalFields];
618
+ const newVisible = new Set(event.state.visibleFields ?? []);
619
+ for (const col of allCols) {
620
+ const isNowVisible = newVisible.has(col.key);
621
+ if (this.isColumnVisible(col.key) !== isNowVisible) {
622
+ this.onColumnVisibilityChange(col, isNowVisible);
623
+ }
624
+ }
625
+ }
626
+ else if (event.type === 'groupBySelected') {
627
+ const newKey = event.state.selectedGroupBy ?? null;
628
+ this.onGroupByChange(newKey === this.activeGroupByKey ? null : newKey);
629
+ }
630
+ else if (event.type === 'sliceSelected') {
631
+ const prevSlices = new Set(this.organizerState.selectedSlices ?? []);
632
+ const clickedId = (event.state.selectedSlices ?? []).find(id => !prevSlices.has(id));
633
+ if (clickedId) {
634
+ const item = this._sliceByMenuItems.find(i => i.key === clickedId);
635
+ if (item)
636
+ this.onSliceMenuItemClick(item);
637
+ }
638
+ }
639
+ this._cdr.markForCheck();
640
+ }
1496
641
  get title() {
1497
642
  return this.resolvedConfig.title ?? '';
1498
643
  }
@@ -1500,10 +645,11 @@ class TableComponent {
1500
645
  return !!this.resolvedConfig.title;
1501
646
  }
1502
647
  get visibleColumns() {
1503
- return this.activeColumns;
648
+ return this.activeFields;
1504
649
  }
1505
650
  get showSearchBar() { return this.resolvedConfig.search?.enabled === true; }
1506
- get showViewButton() { return this._view.enabled || this.showExportButton || this.showPreferencesMenu; }
651
+ get showViewButton() { return this._organizer.enabled || this.showExportButton || this.showPreferencesMenu; }
652
+ get showOrganizerButton() { return this._organizer.enabled || this.showExportButton || this.showPreferencesMenu; }
1507
653
  get showSliceExpandBtn() { return this._showSlicePanel && this._slicePanelCollapsed && this._sliceShowToggle; }
1508
654
  get showExportButton() {
1509
655
  return this.resolvedConfig.export?.enabled === true;
@@ -1521,21 +667,20 @@ class TableComponent {
1521
667
  expandSlicePanel() {
1522
668
  this.slicePanelRef?.togglePanel();
1523
669
  }
1524
- get showFields() { return this._view.showFields; }
1525
- get showGroupBySection() { return this._view.showGroupBySection; }
670
+ get showFields() { return this._organizer.showFields; }
671
+ get showGroupBySection() { return this._organizer.showGroupBySection; }
1526
672
  get filteredRowCount() {
1527
673
  return this.activeGroupByKey ? this._totalFilteredCount : this._allFilteredRows.length;
1528
674
  }
1529
- get activeGroupByLabel() { return this._view.activeGroupByLabel; }
1530
- get totalColumnCount() { return this._view.totalColumnCount; }
1531
- get activeSliceByLabel() { return this._view.sliceBy.activeLabel; }
1532
- get activeDynamicSliceColumns() { return this._view.sliceBy.activeDynamicColumns; }
675
+ get activeGroupByLabel() { return this._organizer.activeGroupByLabel; }
676
+ get totalColumnCount() { return this._organizer.totalFieldCount; }
677
+ get activeSliceByLabel() { return this._organizer.sliceBy.activeLabel; }
678
+ get activeDynamicSliceColumns() { return this._organizer.sliceBy.activeDynamicFields; }
1533
679
  removeDynamicSliceByKey(key) {
1534
680
  this.slicePanelRef?.removeDynamicSliceByKey(key);
1535
681
  }
1536
- get groupByColumns() { return this._view.groupByColumns; }
1537
- colLabel(col) { return this._view.colLabel(col); }
1538
- // ── Actions utilisateur
682
+ get groupByColumns() { return this._organizer.groupByFields; }
683
+ colLabel(col) { return this._organizer.fieldLabel(col); }
1539
684
  onSearchChange() {
1540
685
  this._preservePageIndex = 0;
1541
686
  this.refreshViewModel();
@@ -1543,7 +688,7 @@ class TableComponent {
1543
688
  this.searchChange.emit(this.searchQuery);
1544
689
  }
1545
690
  onGroupByChange(key) {
1546
- this._view.setGroupBy(key);
691
+ this._organizer.setGroupBy(key);
1547
692
  this._groupBy.setDefaultCollapsed(key !== null);
1548
693
  this._groupBy.reset();
1549
694
  this._groupBy.groupPageSize = this.resolveDefaultGroupPageSize();
@@ -1551,7 +696,6 @@ class TableComponent {
1551
696
  this.paginator?.firstPage();
1552
697
  this.groupByChange.emit(key);
1553
698
  if (key) {
1554
- this.viewMenuTrigger?.closeMenu();
1555
699
  const colDef = (this.resolvedConfig.columns || []).find(c => c.key === key);
1556
700
  if (colDef?.lazy && this._lazyColumnStatus.get(key) !== 'loaded') {
1557
701
  this.triggerLazyFetch(colDef);
@@ -1571,7 +715,6 @@ class TableComponent {
1571
715
  this._ngZone.runOutsideAngular(() => setTimeout(() => this.scrollToGroupHeader(key), 0));
1572
716
  });
1573
717
  }
1574
- // ── Lazy loading
1575
718
  getLazyColumnStatus(key) {
1576
719
  return this._lazy.getStatus(key);
1577
720
  }
@@ -1590,7 +733,6 @@ class TableComponent {
1590
733
  onMarkForCheck: () => this._cdr.markForCheck(),
1591
734
  };
1592
735
  }
1593
- // ── Groupement
1594
736
  toggleGroupCollapse(groupKey) {
1595
737
  this._groupBy.toggleGroupCollapse(groupKey, (key) => {
1596
738
  this._ngZone.runOutsideAngular(() => setTimeout(() => this.scrollToGroupHeader(key), 0));
@@ -1619,24 +761,22 @@ class TableComponent {
1619
761
  groupPageForward(groupKey, totalCount) {
1620
762
  this._groupBy.groupPageForward(groupKey, totalCount);
1621
763
  }
1622
- // ── Colonnes
1623
764
  get hasAddableColumns() {
1624
- return this._view.menuBaseColumns.length > 0 || this._view.menuOptionalColumns.length > 0;
765
+ return this._organizer.menuBaseFields.length > 0 || this._organizer.menuOptionalFields.length > 0;
1625
766
  }
1626
767
  get menuBaseColumns() {
1627
- return this._view.menuBaseColumns;
768
+ return this._organizer.menuBaseFields;
1628
769
  }
1629
770
  get menuOptionalColumns() {
1630
- return this._view.menuOptionalColumns;
771
+ return this._organizer.menuOptionalFields;
1631
772
  }
1632
773
  get availableColumnsToAdd() {
1633
- const activeKeys = new Set(this.activeColumns.map((column) => column.key));
774
+ const activeKeys = new Set(this.activeFields.map((column) => column.key));
1634
775
  return (this.resolvedConfig.columns || []).filter((c) => c.optional && !activeKeys.has(c.key));
1635
776
  }
1636
777
  get allowColumnRemoval() {
1637
- return this._view.allowColumnRemoval;
778
+ return this._organizer.allowFieldRemoval;
1638
779
  }
1639
- // ── Slices
1640
780
  get hasSliceConfig() {
1641
781
  return (this.resolvedConfig.slices?.length ?? 0) > 0;
1642
782
  }
@@ -1648,28 +788,43 @@ class TableComponent {
1648
788
  this._cdr.markForCheck();
1649
789
  }
1650
790
  _recomputeShowSlicePanel() {
1651
- const hasCfg = this._sliceConfigs.length > 0 || this._view.sliceBy.activeDynamicColumns.length > 0;
791
+ const hasCfg = this._sliceConfigs.length > 0 || this._organizer.sliceBy.activeDynamicFields.length > 0;
1652
792
  const wasVisible = this._showSlicePanel;
1653
793
  this._showSlicePanel = hasCfg && this._resolvedData.length > 0;
1654
- // Le SlicePanelComponent vient d'être rendu pour la 1ère fois
1655
- // (données arrivées) → appliquer les filtres en attente
1656
- if (!wasVisible && this._showSlicePanel && this._pendingSliceFilters) {
794
+ // Les données viennent d'arriver : appliquer les dynamic slices ET les filtres en attente.
795
+ // Les dynamic slices doivent être ajoutées AVANT restoreFilters car les indices de
796
+ // sliceFilters dépendent de l'ordre final de _cachedSlices.
797
+ if (!wasVisible && this._showSlicePanel && (this._pendingSliceFilters || this._pendingDynamicSliceKeys)) {
1657
798
  const filters = this._pendingSliceFilters;
799
+ const dynamicKeys = this._pendingDynamicSliceKeys;
1658
800
  this._pendingSliceFilters = null;
801
+ this._pendingDynamicSliceKeys = null;
802
+ const allCols = this.resolvedConfig.columns ?? [];
803
+ console.log('[SliceRestore] _recomputeShowSlicePanel: données arrivées', { dynamicKeys, filters });
1659
804
  setTimeout(() => {
1660
- this.slicePanelRef?.restoreFilters(filters);
805
+ if (dynamicKeys) {
806
+ dynamicKeys.forEach(key => {
807
+ const col = allCols.find(c => c.key === key);
808
+ if (col)
809
+ this.addDynamicSlice(col);
810
+ });
811
+ }
812
+ if (filters && this.slicePanelRef) {
813
+ console.log('[SliceRestore] restoreFilters depuis _recomputeShowSlicePanel', filters);
814
+ this.slicePanelRef.restoreFilters(filters);
815
+ }
1661
816
  this._cdr.markForCheck();
1662
817
  });
1663
818
  }
1664
819
  }
1665
820
  get showAddSliceButton() {
1666
- return this._view.showSliceBySection;
821
+ return this._organizer.showSliceBySection;
1667
822
  }
1668
823
  get staticSlicesForMenu() {
1669
- return this._view.staticSlicesForMenu;
824
+ return this._organizer.staticSlicesForMenu;
1670
825
  }
1671
826
  get availableColumnsForDynamicSlice() {
1672
- return this._view.sliceBy.availableForDynamic;
827
+ return this._organizer.sliceBy.availableForDynamic;
1673
828
  }
1674
829
  isStaticSliceVisible(columnKey) {
1675
830
  return !this._staticSliceHiddenKeys.has(columnKey);
@@ -1681,7 +836,7 @@ class TableComponent {
1681
836
  else
1682
837
  next.add(columnKey);
1683
838
  this._staticSliceHiddenKeys = next;
1684
- this._view.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
839
+ this._organizer.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
1685
840
  this._sliceConfigs = this._computeSliceConfigs();
1686
841
  // Si une slice vient d'être cachée, réinitialise le filtre actif pour éviter
1687
842
  // un filtre orphelin (la slice n'est plus visible mais son filtre resterait actif)
@@ -1702,7 +857,7 @@ class TableComponent {
1702
857
  return this._allDynamicSliceColumns;
1703
858
  }
1704
859
  isDynamicSliceActive(key) {
1705
- return this._view.isDynamicSliceActive(key);
860
+ return this._organizer.isDynamicSliceActive(key);
1706
861
  }
1707
862
  toggleDynamicSlice(col) {
1708
863
  if (this.isDynamicSliceActive(col.key)) {
@@ -1719,7 +874,7 @@ class TableComponent {
1719
874
  this.paginator?.firstPage();
1720
875
  }
1721
876
  onDynamicSliceKeysChange(keys) {
1722
- this._view.onDynamicSliceKeysChange(keys);
877
+ this._organizer.onDynamicSliceKeysChange(keys);
1723
878
  this._recomputeShowSlicePanel();
1724
879
  this._cdr.markForCheck();
1725
880
  }
@@ -1742,10 +897,10 @@ class TableComponent {
1742
897
  return this.resolvedConfig.pagination?.enabled !== false;
1743
898
  }
1744
899
  get resolvedPaginationShowFirstLastButtons() {
1745
- return this.resolvedConfig.pagination?.showFirstLastButtons === true;
900
+ return this.resolvedConfig.pagination?.showFirstLastButtons !== false;
1746
901
  }
1747
902
  get isColumnDragDropEnabled() {
1748
- return this._view.isColumnDragDropEnabled;
903
+ return this._organizer.isFieldDragDropEnabled;
1749
904
  }
1750
905
  get totalRows() {
1751
906
  return this.activeGroupByKey
@@ -1754,7 +909,7 @@ class TableComponent {
1754
909
  }
1755
910
  // ── Gestion des colonnes
1756
911
  onAddColumn(column) {
1757
- this._view.addColumn(column);
912
+ this._organizer.addField(column);
1758
913
  this._preservePageIndex = this.paginator?.pageIndex ?? null;
1759
914
  this.refreshViewModel();
1760
915
  if (column.lazy) {
@@ -1764,13 +919,13 @@ class TableComponent {
1764
919
  this.columnAdded.emit(column);
1765
920
  }
1766
921
  onColumnVisibilityChange(column, checked) {
1767
- const isVisible = this._view.isColumnVisible(column.key);
922
+ const isVisible = this._organizer.isFieldVisible(column.key);
1768
923
  if (checked && !isVisible) {
1769
924
  this.onAddColumn(column);
1770
925
  return;
1771
926
  }
1772
927
  if (!checked && isVisible) {
1773
- const removed = this._view.removeColumn(column.key);
928
+ const removed = this._organizer.removeField(column.key);
1774
929
  if (!removed)
1775
930
  return;
1776
931
  this._preservePageIndex = this.paginator?.pageIndex ?? null;
@@ -1779,14 +934,14 @@ class TableComponent {
1779
934
  }
1780
935
  }
1781
936
  isColumnVisible(columnKey) {
1782
- return this.activeColumns.some(c => c.key === columnKey);
937
+ return this.activeFields.some(c => c.key === columnKey);
1783
938
  }
1784
939
  onRemoveColumn(columnKey, event) {
1785
940
  event?.stopPropagation();
1786
- const column = this.activeColumns.find(item => item.key === columnKey);
941
+ const column = this.activeFields.find(item => item.key === columnKey);
1787
942
  if (!column)
1788
943
  return;
1789
- const removed = this._view.removeColumn(columnKey);
944
+ const removed = this._organizer.removeField(columnKey);
1790
945
  if (!removed)
1791
946
  return;
1792
947
  this.refreshViewModel();
@@ -1810,11 +965,11 @@ class TableComponent {
1810
965
  * Seules les valeurs explicitement en `px` sont sommables ; les pourcentages ou autres unités sont ignorés. */
1811
966
  get tableMinWidth() {
1812
967
  const hasResized = Object.keys(this._columnWidths).length > 0;
1813
- const hasSomeExplicitWidth = hasResized || this.activeColumns.some(c => c.width);
968
+ const hasSomeExplicitWidth = hasResized || this.activeFields.some(c => c.width);
1814
969
  if (!hasSomeExplicitWidth)
1815
970
  return null;
1816
971
  let sum = 0;
1817
- for (const col of this.activeColumns) {
972
+ for (const col of this.activeFields) {
1818
973
  if (this._columnWidths[col.key] != null) {
1819
974
  sum += this._columnWidths[col.key];
1820
975
  }
@@ -1873,7 +1028,7 @@ class TableComponent {
1873
1028
  return;
1874
1029
  }
1875
1030
  event.preventDefault();
1876
- const moved = this._view.reorderColumns(this.draggedColumnKey, targetColumnKey);
1031
+ const moved = this._organizer.reorderFields(this.draggedColumnKey, targetColumnKey);
1877
1032
  if (!moved) {
1878
1033
  this.onHeaderDragEnd();
1879
1034
  return;
@@ -1955,12 +1110,12 @@ class TableComponent {
1955
1110
  savePreferences() {
1956
1111
  if (!this._preferencesManager)
1957
1112
  return;
1958
- const dynamicSliceKeys = this._view.sliceBy.activeDynamicColumns.map(c => c.key);
1113
+ const dynamicSliceKeys = this._organizer.sliceBy.activeDynamicFields.map(c => c.key);
1959
1114
  const config = {
1960
1115
  search: this.searchQuery || undefined,
1961
1116
  groupBy: this.activeGroupByKey,
1962
- columnOrder: this.activeColumns.map(c => c.key),
1963
- visibleColumns: this.activeColumns.map(c => c.key),
1117
+ columnOrder: this.activeFields.map(c => c.key),
1118
+ visibleColumns: this.activeFields.map(c => c.key),
1964
1119
  columnWidths: Object.keys(this._columnWidths).length ? { ...this._columnWidths } : undefined,
1965
1120
  slicePanelCollapsed: this._slicePanelCollapsed,
1966
1121
  hiddenSliceKeys: [...this._staticSliceHiddenKeys],
@@ -1977,7 +1132,7 @@ class TableComponent {
1977
1132
  this._preferencesManager.clear();
1978
1133
  this._hasSavedConfig = false;
1979
1134
  // Capturer les dynamic slices actives AVANT le reset (resetToDefaults() les vide)
1980
- const dynamicSlicesToRemove = this._view.sliceBy.activeDynamicColumns.slice();
1135
+ const dynamicSlicesToRemove = this._organizer.sliceBy.activeDynamicFields.slice();
1981
1136
  // Reset complet de l'état vers les valeurs par défaut de la config
1982
1137
  this.searchQuery = '';
1983
1138
  this._columnWidths = {};
@@ -1992,9 +1147,9 @@ class TableComponent {
1992
1147
  this._groupBy.setDefaultCollapsed(false);
1993
1148
  // Slices statiques : revenir aux seules celles cachées par la config (pas par l'utilisateur)
1994
1149
  this._staticSliceHiddenKeys = new Set(this._configHiddenKeys);
1995
- this._view.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
1150
+ this._organizer.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
1996
1151
  // Colonnes et groupBy : reset via la facade (retire les optionnelles, remet l'ordre config)
1997
- this._view.resetToDefaults();
1152
+ this._organizer.resetToDefaults();
1998
1153
  this._defaultGroupByApplied = false;
1999
1154
  // Dynamic slices : les retirer du slice panel avec la liste capturée avant le reset
2000
1155
  dynamicSlicesToRemove.forEach(col => {
@@ -2005,8 +1160,8 @@ class TableComponent {
2005
1160
  if (this.slicePanelRef)
2006
1161
  this.slicePanelRef.restoreFilters({});
2007
1162
  // Rafraîchit toute la vue
2008
- this.activeColumns = this._view.fields.activeColumns;
2009
- this.renderedColumns = this.activeColumns.map(c => c.key);
1163
+ this.activeFields = this._organizer.fields.activeFields;
1164
+ this.renderedColumns = this.activeFields.map(c => c.key);
2010
1165
  this._pendingDynamicSliceKeys = null;
2011
1166
  this._pendingSliceFilters = null;
2012
1167
  this.refreshViewModel();
@@ -2034,7 +1189,7 @@ class TableComponent {
2034
1189
  this._columnWidths = { ...saved.columnWidths };
2035
1190
  // Group by
2036
1191
  if (saved.groupBy !== undefined) {
2037
- this._view.setGroupBy(saved.groupBy);
1192
+ this._organizer.setGroupBy(saved.groupBy);
2038
1193
  if (saved.groupBy) {
2039
1194
  this._groupBy.setDefaultCollapsed(true);
2040
1195
  this._groupBy.reset();
@@ -2046,9 +1201,9 @@ class TableComponent {
2046
1201
  // Slices statiques cachées — on utilise la valeur exacte sauvegardée (pas les defaults de config)
2047
1202
  if (saved.hiddenSliceKeys !== undefined) {
2048
1203
  this._staticSliceHiddenKeys = new Set(saved.hiddenSliceKeys);
2049
- this._view.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
1204
+ this._organizer.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
2050
1205
  }
2051
- // Colonnes visibles + ordre — reconstruit activeColumns en une seule passe.
1206
+ // Colonnes visibles + ordre — reconstruit activeFields en une seule passe.
2052
1207
  // On part de saved.columnOrder (ordre sauvegardé) si disponible, sinon saved.visibleColumns.
2053
1208
  // Cela gère à la fois : colonnes optionnelles ajoutées, colonnes retirées, et réordonnancement.
2054
1209
  const targetKeys = saved.columnOrder ?? saved.visibleColumns;
@@ -2061,7 +1216,7 @@ class TableComponent {
2061
1216
  // Colonnes actives actuelllement non présentes dans targetKeys (non-optional passées sous silence)
2062
1217
  // → on les exclut : l'utilisateur avait explicitement choisi ce sous-ensemble
2063
1218
  if (ordered.length > 0) {
2064
- this._view.setActiveColumns(ordered);
1219
+ this._organizer.setActiveFields(ordered);
2065
1220
  }
2066
1221
  }
2067
1222
  // Dynamic slices — différé : slicePanelRef pas encore disponible au moment de ngOnChanges
@@ -2077,9 +1232,9 @@ class TableComponent {
2077
1232
  }
2078
1233
  }
2079
1234
  canRemoveColumn(column) {
2080
- return (this._view.allowColumnRemoval &&
1235
+ return (this._organizer.allowFieldRemoval &&
2081
1236
  column.removable !== false &&
2082
- this.activeColumns.length > 1);
1237
+ this.activeFields.length > 1);
2083
1238
  }
2084
1239
  isCellEmpty(value) {
2085
1240
  return value === '' || value == null;
@@ -2104,7 +1259,7 @@ class TableComponent {
2104
1259
  ? this.resolvedConfig.preferences.tableId
2105
1260
  : null;
2106
1261
  const currentId = prefsId ?? null;
2107
- // Préférences à appliquer après _view.update() (quand activeColumns est déjà peuplé)
1262
+ // Préférences à appliquer après _organizer.update() (quand activeFields est déjà peuplé)
2108
1263
  let pendingPrefs = null;
2109
1264
  if (currentId && this._preferencesManager?.key !== ('jqt_prefs_' + currentId)) {
2110
1265
  this._preferencesManager = new TablePreferencesManager(currentId);
@@ -2116,21 +1271,21 @@ class TableComponent {
2116
1271
  this._hasSavedConfig = false;
2117
1272
  }
2118
1273
  // Délègue à la facade : mise à jour de l'état View (colonnes, groupBy meta, sliceBy meta)
2119
- this._view.update({
1274
+ this._organizer.update({
2120
1275
  config: this.resolvedConfig.view,
2121
- columns: this.resolvedConfig.columns ?? [],
1276
+ fields: this.resolvedConfig.columns ?? [],
2122
1277
  sliceConfigs: this.resolvedConfig.slices ?? [],
2123
1278
  defaultGroupBy: this.resolvedConfig.defaultGroupBy,
2124
1279
  });
2125
- if (!pendingPrefs && !this._defaultGroupByApplied && !this._view.groupBy.userCustomized && this._view.groupBy.activeKey != null) {
1280
+ if (!pendingPrefs && !this._defaultGroupByApplied && !this._organizer.groupBy.userCustomized && this._organizer.groupBy.activeKey != null) {
2126
1281
  this._defaultGroupByApplied = true;
2127
1282
  this._groupBy.setDefaultCollapsed(true);
2128
1283
  this._groupBy.reset();
2129
1284
  this._groupBy.groupPageSize = this.resolveDefaultGroupPageSize();
2130
1285
  }
2131
1286
  // Synchronise la propriété stable (évite NG0100 — la facade est source de vérité)
2132
- this.activeColumns = this._view.fields.activeColumns;
2133
- this.renderedColumns = this.activeColumns.map(c => c.key);
1287
+ this.activeFields = this._organizer.fields.activeFields;
1288
+ this.renderedColumns = this.activeFields.map(c => c.key);
2134
1289
  this.pageSize = this.paginator?.pageSize || this.resolvePageSize();
2135
1290
  this.pageSizeOptions = this.resolvePageSizeOptions();
2136
1291
  this.pageSizeOptionsGroupBy = this.resolvePageSizeOptionsGroupBy();
@@ -2148,10 +1303,10 @@ class TableComponent {
2148
1303
  // (pendingPrefs les gèrera lui-même via _applyPreferences → hiddenSliceKeys)
2149
1304
  if (!pendingPrefs) {
2150
1305
  this._staticSliceHiddenKeys = new Set(newConfigHidden);
2151
- this._view.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
1306
+ this._organizer.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
2152
1307
  }
2153
1308
  }
2154
- // Applique les préférences ICI, APRÈS _view.update() ET après _configHiddenKeys mis à jour,
1309
+ // Applique les préférences ICI, APRÈS _organizer.update() ET après _configHiddenKeys mis à jour,
2155
1310
  // mais avant _computeSliceConfigs() pour que hiddenSliceKeys soit déjà restauré.
2156
1311
  if (pendingPrefs) {
2157
1312
  this._applyPreferences(pendingPrefs);
@@ -2209,6 +1364,7 @@ class TableComponent {
2209
1364
  }
2210
1365
  }
2211
1366
  this._sliceByMenuItems = items;
1367
+ this._invalidateOrganizerConfig(); // les slices ont changé
2212
1368
  }
2213
1369
  get sliceByMenuItems() { return this._sliceByMenuItems; }
2214
1370
  onSliceMenuItemClick(item) {
@@ -2269,7 +1425,7 @@ class TableComponent {
2269
1425
  pageRows.forEach((row, i) => {
2270
1426
  const idx = start + i;
2271
1427
  const p = { [ROW_RAW_KEY]: row, [ROW_INDEX_KEY]: idx };
2272
- this.activeColumns.forEach((col) => {
1428
+ this.activeFields.forEach((col) => {
2273
1429
  p[col.key] = this.resolveCellValue(col, row, idx);
2274
1430
  });
2275
1431
  projected.push(p);
@@ -2382,7 +1538,7 @@ class TableComponent {
2382
1538
  const result = [];
2383
1539
  rows.forEach((row, index) => {
2384
1540
  const projected = { [ROW_RAW_KEY]: row, [ROW_INDEX_KEY]: index };
2385
- this.activeColumns.forEach((col) => {
1541
+ this.activeFields.forEach((col) => {
2386
1542
  projected[col.key] = this.resolveCellValue(col, row, index);
2387
1543
  });
2388
1544
  result.push(projected);
@@ -2490,7 +1646,7 @@ class TableComponent {
2490
1646
  groupRows.slice(start, end).forEach((row, i) => {
2491
1647
  const idx = start + i;
2492
1648
  const projected = { [ROW_RAW_KEY]: row, [ROW_INDEX_KEY]: idx };
2493
- this.activeColumns.forEach((col) => {
1649
+ this.activeFields.forEach((col) => {
2494
1650
  projected[col.key] = this.resolveCellValue(col, row, idx);
2495
1651
  });
2496
1652
  result.push(projected);
@@ -2644,11 +1800,11 @@ class TableComponent {
2644
1800
  }
2645
1801
  }
2646
1802
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
2647
- 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", clearSearchInput: "clearSearchInput" }, 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\">filter_list</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\" *ngIf=\"!effectiveIsLoading\">{{ filteredRowCount }}</span>\n <div class=\"search-count-loader\" *ngIf=\"effectiveIsLoading\">\n <div class=\"spinner\"></div>\n </div>\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 <span class=\"view-menu-check\" aria-hidden=\"true\"></span>\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 <span class=\"view-menu-check\" aria-hidden=\"true\"></span>\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 <span class=\"view-menu-check\" aria-hidden=\"true\"></span>\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\" [class.is-loading]=\"effectiveIsLoading\" *ngIf=\"showPagination\">\n <mat-paginator\n [length]=\"totalRows\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"pageSizeOptions\"\n [showFirstLastButtons]=\"resolvedPaginationShowFirstLastButtons\"\n ></mat-paginator>\n <div class=\"paginator-range-loader\" *ngIf=\"effectiveIsLoading\">\n <div class=\"spinner\"></div>\n </div>\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\" *ngIf=\"!isDefaultGroupByActive\" (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\" *ngIf=\"!isDefaultGroupByActive\"></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-count-loader{flex-shrink:0;display:flex;align-items:center;justify-content:center;min-width:20px;height:20px}.spinner{width:14px;height:14px;border:2px solid #d1d5db;border-top-color:#374151;border-radius:50%;animation:spin .8s linear infinite}.search-count-loader .spinner{width:16px;height:16px;border:2.5px solid #e5e7eb;border-top-color:#a0a5ad}@keyframes spin{to{transform:rotate(360deg)}}.search-clear-btn{background:none;border:none;cursor:pointer;color:var(--jqt-text-muted);font-size:16px;padding:0;line-height:1;flex-shrink:0}.search-clear-btn:hover{color:var(--jqt-text-color)}.slice-expand-btn{flex-shrink:0;width:34px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--jqt-text-color);padding:0;line-height:1}.slice-expand-btn:hover{border-color:var(--jqt-text-muted)}.slice-expand-mat-icon{font-size:18px;width:18px;height:18px;line-height:1}.view-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.view-btn:hover{border-color:var(--jqt-text-muted)}.view-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.export-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.export-btn:hover{border-color:var(--jqt-text-muted)}.export-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.view-menu-export-divider{margin:4px 0!important}.view-menu-export-item{display:flex;align-items:center;gap:6px;width:100%;padding:0 16px;height:40px;border:none;background:transparent;cursor:pointer;font-size:13px;font-weight:500;color:var(--jqt-text-color);text-align:left;box-sizing:border-box}.view-menu-export-item:hover{background:#0000000a}.view-menu-export-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.edit-confirm-btn{position:absolute;top:10px;right:10px;z-index:200;display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:50%;background:#16a34a;cursor:pointer;box-shadow:0 2px 8px #00000040;transition:background .15s}.edit-confirm-btn mat-icon{color:#fff;font-size:22px;width:22px;height:22px}.edit-confirm-btn:hover{background:#15803d}.preferences-message{display:flex;align-items:center;gap:8px;margin:0 0 8px;padding:8px 12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;font-size:12px;color:#15803d}.preferences-message .preferences-message-icon{font-size:16px;width:16px;height:16px}:host ::ng-deep .edit-mode-header{background:#3b82f60a!important;cursor:grab!important;-webkit-user-select:none;user-select:none;overflow:visible!important}.col-resize-handle{position:absolute;top:0;right:0;width:6px;height:100%;cursor:col-resize!important;background:#3b82f640;z-index:20}.col-resize-handle:hover{background:#3b82f6b3}.col-resize-handle:active{background:#3b82f6e6}:host ::ng-deep th.mat-mdc-header-cell{position:relative}:host ::ng-deep th.mat-mdc-header-cell.drag-enabled{overflow:visible!important}:host ::ng-deep .edit-mode-row{pointer-events:none;-webkit-user-select:none;user-select:none;opacity:.8}.cell-edit-mode{pointer-events:none;-webkit-user-select:none;user-select:none;color:var(--jqt-text-muted)}.group-header-row{cursor:pointer}:host ::ng-deep .jquery-table{border-spacing:0}:host ::ng-deep .group-header-row td{padding:0!important;background:var(--jqt-group-bg);border-bottom:1px solid var(--jqt-group-border);border-top:1px solid var(--jqt-group-border);border-left:3px solid var(--jqt-group-accent);position:sticky;top:var(--actual-header-height, 56px);z-index:2;box-shadow:-4px 0 0 0 var(--jqt-group-bg)}:host ::ng-deep .group-header-row:hover td{background:var(--jqt-group-bg-hover);box-shadow:-4px 0 0 0 var(--jqt-group-bg-hover)}:host ::ng-deep .group-header-loading td,:host ::ng-deep .group-header-loading:hover td,:host ::ng-deep .group-header-too-many td,:host ::ng-deep .group-header-too-many:hover td{cursor:default;background:var(--jqt-surface-alt-color)}.group-too-many-label{font-size:12px;font-weight:500;color:var(--jqt-warn-color, #b45309);font-style:italic}.group-header-cell{padding:0!important}.group-header-label{display:flex;align-items:center;gap:6px;padding:6px 16px;font-size:12px;font-weight:700;color:var(--jqt-text-color);text-transform:uppercase;letter-spacing:.04em}.group-key-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-count-badge{flex-shrink:0;font-size:11px;line-height:1;font-weight:600;color:#fff;background:var(--jqt-text-secondary, #6b7280);border-radius:10px;padding:3px 7px;min-width:20px;text-align:center;text-transform:none;letter-spacing:0}.group-pagination{display:inline-flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.group-page-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:1px solid var(--jqt-border-color);border-radius:5px;background:var(--jqt-surface-color);cursor:pointer;font-size:14px;color:var(--jqt-text-color);line-height:1;padding:0 0 3px;text-transform:none;letter-spacing:0}.group-page-btn:hover:not(:disabled){border-color:var(--jqt-text-muted);background:var(--jqt-surface-alt-color)}.group-page-btn:disabled{opacity:.35;cursor:default}.group-page-info{font-size:11px;color:var(--jqt-text-secondary);min-width:36px;text-align:center;font-weight:500;text-transform:none;letter-spacing:0}.group-chevron{flex-shrink:0;font-size:14px;font-weight:700;padding-bottom:3px;transition:transform .15s ease;transform:rotate(90deg)}.group-chevron.collapsed{transform:rotate(0)}.no-data-row{cursor:default!important}.no-data-cell{padding:0!important;height:0!important;border:none!important}.jquery-table{width:100%;table-layout:fixed}:host ::ng-deep .mat-mdc-table{width:100%;background:transparent}:host ::ng-deep .mat-mdc-header-cell,:host ::ng-deep .mat-mdc-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host ::ng-deep .mat-mdc-header-row{background:var(--jqt-header-bg)}:host ::ng-deep .mat-mdc-header-cell{background:var(--jqt-header-bg);color:var(--jqt-header-text-color)}:host ::ng-deep .mat-mdc-row,:host ::ng-deep .mat-mdc-cell{background:var(--jqt-cell-bg)}:host ::ng-deep .mat-mdc-row{cursor:pointer;border-bottom-color:var(--jqt-border-color)}:host ::ng-deep .mat-mdc-row.no-pointer{cursor:auto}:host ::ng-deep .mat-mdc-row.no-data-row{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drag-enabled{-webkit-user-select:none;user-select:none}:host ::ng-deep .mat-mdc-header-cell.drag-enabled:active{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drop-target{background:var(--jqt-row-hover-bg)}.head-cell{display:flex;align-items:center;justify-content:space-between;gap:6px;min-width:0}.drag-handle{display:inline-flex;align-items:center;cursor:grab;color:var(--jqt-border-color);flex-shrink:0;padding:0 2px;border-radius:4px;line-height:1}.drag-handle:active{cursor:grabbing}.drag-handle:hover{color:var(--jqt-text-muted)}.drag-handle mat-icon{font-size:16px;width:16px;height:16px;line-height:1}.head-icon{font-size:14px;width:14px;height:14px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.head-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase;font-size:11px;font-weight:600;letter-spacing:.05em}.head-actions{display:inline-flex;align-items:center;gap:6px}.cell-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cell-empty{color:var(--jqt-empty-cell-color, #9ca3af);font-style:italic}.bottom-bar{display:flex;align-items:center;flex:0 0 auto;position:relative;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)}:host ::ng-deep .bottom-bar.is-loading .mat-mdc-paginator-range-label{visibility:hidden}.paginator-range-loader{position:absolute;right:0;top:50%;transform:translateY(-50%);margin-right:145px;display:flex;align-items:center;justify-content:center;width:auto;padding:0 16px;height:24px}.paginator-range-loader .spinner{width:16px;height:16px;border:2.5px solid #e5e7eb;border-top-color:#a0a5ad}.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 }); }
1803
+ 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", clearSearchInput: "clearSearchInput" }, 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 }], 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\">filter_list</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\" *ngIf=\"!effectiveIsLoading\">{{ filteredRowCount }}</span>\n <div class=\"search-count-loader\" *ngIf=\"effectiveIsLoading\">\n <div class=\"spinner\"></div>\n </div>\n </div>\n </div>\n <div *ngIf=\"showOrganizerButton\" class=\"table-toolbar-right\">\n <table-organizer-button\n [config]=\"organizerConfig\"\n [state]=\"organizerState\"\n (viewChange)=\"onOrganizerViewChange($event)\">\n </table-organizer-button>\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\" [class.is-loading]=\"effectiveIsLoading\" *ngIf=\"showPagination\">\n <mat-paginator\n [length]=\"totalRows\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"pageSizeOptions\"\n [showFirstLastButtons]=\"resolvedPaginationShowFirstLastButtons\"\n ></mat-paginator>\n <div class=\"paginator-range-loader\" \n [class.paginator-range-loader--no-first-last]=\"!resolvedPaginationShowFirstLastButtons\"\n *ngIf=\"effectiveIsLoading\">\n <div class=\"spinner\"></div>\n </div>\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-count-loader{flex-shrink:0;display:flex;align-items:center;justify-content:center;min-width:20px;height:20px}.spinner{width:14px;height:14px;border:2px solid #d1d5db;border-top-color:#374151;border-radius:50%;animation:spin .8s linear infinite}.search-count-loader .spinner{width:16px;height:16px;border:2.5px solid #e5e7eb;border-top-color:#a0a5ad}@keyframes spin{to{transform:rotate(360deg)}}.search-clear-btn{background:none;border:none;cursor:pointer;color:var(--jqt-text-muted);font-size:16px;padding:0;line-height:1;flex-shrink:0}.search-clear-btn:hover{color:var(--jqt-text-color)}.slice-expand-btn{flex-shrink:0;width:34px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--jqt-text-color);padding:0;line-height:1}.slice-expand-btn:hover{border-color:var(--jqt-text-muted)}.slice-expand-mat-icon{font-size:18px;width:18px;height:18px;line-height:1}.view-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.view-btn:hover{border-color:var(--jqt-text-muted)}.view-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.export-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.export-btn:hover{border-color:var(--jqt-text-muted)}.export-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.view-menu-export-divider{margin:4px 0!important}.view-menu-export-item{display:flex;align-items:center;gap:6px;width:100%;padding:0 16px;height:40px;border:none;background:transparent;cursor:pointer;font-size:13px;font-weight:500;color:var(--jqt-text-color);text-align:left;box-sizing:border-box}.view-menu-export-item:hover{background:#0000000a}.view-menu-export-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.edit-confirm-btn{position:absolute;top:10px;right:10px;z-index:200;display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:50%;background:#16a34a;cursor:pointer;box-shadow:0 2px 8px #00000040;transition:background .15s}.edit-confirm-btn mat-icon{color:#fff;font-size:22px;width:22px;height:22px}.edit-confirm-btn:hover{background:#15803d}.preferences-message{display:flex;align-items:center;gap:8px;margin:0 0 8px;padding:8px 12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;font-size:12px;color:#15803d}.preferences-message .preferences-message-icon{font-size:16px;width:16px;height:16px}:host ::ng-deep .edit-mode-header{background:#3b82f60a!important;cursor:grab!important;-webkit-user-select:none;user-select:none;overflow:visible!important}.col-resize-handle{position:absolute;top:0;right:0;width:6px;height:100%;cursor:col-resize!important;background:#3b82f640;z-index:20}.col-resize-handle:hover{background:#3b82f6b3}.col-resize-handle:active{background:#3b82f6e6}:host ::ng-deep th.mat-mdc-header-cell{position:relative}:host ::ng-deep th.mat-mdc-header-cell.drag-enabled{overflow:visible!important}:host ::ng-deep .edit-mode-row{pointer-events:none;-webkit-user-select:none;user-select:none;opacity:.8}.cell-edit-mode{pointer-events:none;-webkit-user-select:none;user-select:none;color:var(--jqt-text-muted)}.group-header-row{cursor:pointer}:host ::ng-deep .jquery-table{border-spacing:0}:host ::ng-deep .group-header-row td{padding:0!important;background:var(--jqt-group-bg);border-bottom:1px solid var(--jqt-group-border);border-top:1px solid var(--jqt-group-border);border-left:3px solid var(--jqt-group-accent);position:sticky;top:var(--actual-header-height, 56px);z-index:2;box-shadow:-4px 0 0 0 var(--jqt-group-bg)}:host ::ng-deep .group-header-row:hover td{background:var(--jqt-group-bg-hover);box-shadow:-4px 0 0 0 var(--jqt-group-bg-hover)}:host ::ng-deep .group-header-loading td,:host ::ng-deep .group-header-loading:hover td,:host ::ng-deep .group-header-too-many td,:host ::ng-deep .group-header-too-many:hover td{cursor:default;background:var(--jqt-surface-alt-color)}.group-too-many-label{font-size:12px;font-weight:500;color:var(--jqt-warn-color, #b45309);font-style:italic}.group-header-cell{padding:0!important}.group-header-label{display:flex;align-items:center;gap:6px;padding:6px 16px;font-size:12px;font-weight:700;color:var(--jqt-text-color);text-transform:uppercase;letter-spacing:.04em}.group-key-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-count-badge{flex-shrink:0;font-size:11px;line-height:1;font-weight:600;color:#fff;background:var(--jqt-text-secondary, #6b7280);border-radius:10px;padding:3px 7px;min-width:20px;text-align:center;text-transform:none;letter-spacing:0}.group-pagination{display:inline-flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.group-page-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:1px solid var(--jqt-border-color);border-radius:5px;background:var(--jqt-surface-color);cursor:pointer;font-size:14px;color:var(--jqt-text-color);line-height:1;padding:0 0 3px;text-transform:none;letter-spacing:0}.group-page-btn:hover:not(:disabled){border-color:var(--jqt-text-muted);background:var(--jqt-surface-alt-color)}.group-page-btn:disabled{opacity:.35;cursor:default}.group-page-info{font-size:11px;color:var(--jqt-text-secondary);min-width:36px;text-align:center;font-weight:500;text-transform:none;letter-spacing:0}.group-chevron{flex-shrink:0;font-size:14px;font-weight:700;padding-bottom:3px;transition:transform .15s ease;transform:rotate(90deg)}.group-chevron.collapsed{transform:rotate(0)}.no-data-row{cursor:default!important}.no-data-cell{padding:0!important;height:0!important;border:none!important}.jquery-table{width:100%;table-layout:fixed}:host ::ng-deep .mat-mdc-table{width:100%;background:transparent}:host ::ng-deep .mat-mdc-header-cell,:host ::ng-deep .mat-mdc-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host ::ng-deep .mat-mdc-header-row{background:var(--jqt-header-bg)}:host ::ng-deep .mat-mdc-header-cell{background:var(--jqt-header-bg);color:var(--jqt-header-text-color)}:host ::ng-deep .mat-mdc-row,:host ::ng-deep .mat-mdc-cell{background:var(--jqt-cell-bg)}:host ::ng-deep .mat-mdc-row{cursor:pointer;border-bottom-color:var(--jqt-border-color)}:host ::ng-deep .mat-mdc-row.no-pointer{cursor:auto}:host ::ng-deep .mat-mdc-row.no-data-row{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drag-enabled{-webkit-user-select:none;user-select:none}:host ::ng-deep .mat-mdc-header-cell.drag-enabled:active{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drop-target{background:var(--jqt-row-hover-bg)}.head-cell{display:flex;align-items:center;justify-content:space-between;gap:6px;min-width:0}.drag-handle{display:inline-flex;align-items:center;cursor:grab;color:var(--jqt-border-color);flex-shrink:0;padding:0 2px;border-radius:4px;line-height:1}.drag-handle:active{cursor:grabbing}.drag-handle:hover{color:var(--jqt-text-muted)}.drag-handle mat-icon{font-size:16px;width:16px;height:16px;line-height:1}.head-icon{font-size:14px;width:14px;height:14px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.head-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase;font-size:11px;font-weight:600;letter-spacing:.05em}.head-actions{display:inline-flex;align-items:center;gap:6px}.cell-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cell-empty{color:var(--jqt-empty-cell-color, #9ca3af);font-style:italic}.bottom-bar{display:flex;align-items:center;flex:0 0 auto;position:relative;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)}:host ::ng-deep .bottom-bar.is-loading .mat-mdc-paginator-range-label{visibility:hidden}.paginator-range-loader{position:absolute;right:0;top:50%;transform:translateY(-50%);margin-right:145px;display:flex;align-items:center;justify-content:center;width:auto;padding:0 16px;height:24px}.paginator-range-loader--no-first-last{margin-right:80px}.paginator-range-loader .spinner{width:16px;height:16px;border:2.5px solid #e5e7eb;border-top-color:#a0a5ad}.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.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.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.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: "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: "ngmodule", type: MatIconModule }, { kind: "component", type: i6.MatIcon, selector: "mat-icon", inputs: ["color", "inline", "svgIcon", "fontSet", "fontIcon"], exportAs: ["matIcon"] }, { kind: "ngmodule", type: MatSelectModule }, { kind: "component", type: i7.MatFormField, selector: "mat-form-field", inputs: ["hideRequiredMarker", "color", "floatLabel", "appearance", "subscriptSizing", "hintLabel"], exportAs: ["matFormField"] }, { kind: "component", type: i8.MatSelect, selector: "mat-select", inputs: ["disabled", "disableRipple", "tabIndex", "panelWidth", "hideSingleSelectionIndicator"], exportAs: ["matSelect"] }, { kind: "directive", type: i8.MatSelectTrigger, selector: "mat-select-trigger" }, { kind: "component", type: i9.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"] }, { kind: "component", type: OrganizerButtonWrapperComponent, selector: "table-organizer-button", inputs: ["config", "state", "hideMenuValues"], outputs: ["viewChange", "sliceStateChange"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
2648
1804
  }
2649
1805
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TableComponent, decorators: [{
2650
1806
  type: Component,
2651
- 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\">filter_list</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\" *ngIf=\"!effectiveIsLoading\">{{ filteredRowCount }}</span>\n <div class=\"search-count-loader\" *ngIf=\"effectiveIsLoading\">\n <div class=\"spinner\"></div>\n </div>\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 <span class=\"view-menu-check\" aria-hidden=\"true\"></span>\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 <span class=\"view-menu-check\" aria-hidden=\"true\"></span>\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 <span class=\"view-menu-check\" aria-hidden=\"true\"></span>\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\" [class.is-loading]=\"effectiveIsLoading\" *ngIf=\"showPagination\">\n <mat-paginator\n [length]=\"totalRows\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"pageSizeOptions\"\n [showFirstLastButtons]=\"resolvedPaginationShowFirstLastButtons\"\n ></mat-paginator>\n <div class=\"paginator-range-loader\" *ngIf=\"effectiveIsLoading\">\n <div class=\"spinner\"></div>\n </div>\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\" *ngIf=\"!isDefaultGroupByActive\" (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\" *ngIf=\"!isDefaultGroupByActive\"></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-count-loader{flex-shrink:0;display:flex;align-items:center;justify-content:center;min-width:20px;height:20px}.spinner{width:14px;height:14px;border:2px solid #d1d5db;border-top-color:#374151;border-radius:50%;animation:spin .8s linear infinite}.search-count-loader .spinner{width:16px;height:16px;border:2.5px solid #e5e7eb;border-top-color:#a0a5ad}@keyframes spin{to{transform:rotate(360deg)}}.search-clear-btn{background:none;border:none;cursor:pointer;color:var(--jqt-text-muted);font-size:16px;padding:0;line-height:1;flex-shrink:0}.search-clear-btn:hover{color:var(--jqt-text-color)}.slice-expand-btn{flex-shrink:0;width:34px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--jqt-text-color);padding:0;line-height:1}.slice-expand-btn:hover{border-color:var(--jqt-text-muted)}.slice-expand-mat-icon{font-size:18px;width:18px;height:18px;line-height:1}.view-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.view-btn:hover{border-color:var(--jqt-text-muted)}.view-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.export-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.export-btn:hover{border-color:var(--jqt-text-muted)}.export-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.view-menu-export-divider{margin:4px 0!important}.view-menu-export-item{display:flex;align-items:center;gap:6px;width:100%;padding:0 16px;height:40px;border:none;background:transparent;cursor:pointer;font-size:13px;font-weight:500;color:var(--jqt-text-color);text-align:left;box-sizing:border-box}.view-menu-export-item:hover{background:#0000000a}.view-menu-export-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.edit-confirm-btn{position:absolute;top:10px;right:10px;z-index:200;display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:50%;background:#16a34a;cursor:pointer;box-shadow:0 2px 8px #00000040;transition:background .15s}.edit-confirm-btn mat-icon{color:#fff;font-size:22px;width:22px;height:22px}.edit-confirm-btn:hover{background:#15803d}.preferences-message{display:flex;align-items:center;gap:8px;margin:0 0 8px;padding:8px 12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;font-size:12px;color:#15803d}.preferences-message .preferences-message-icon{font-size:16px;width:16px;height:16px}:host ::ng-deep .edit-mode-header{background:#3b82f60a!important;cursor:grab!important;-webkit-user-select:none;user-select:none;overflow:visible!important}.col-resize-handle{position:absolute;top:0;right:0;width:6px;height:100%;cursor:col-resize!important;background:#3b82f640;z-index:20}.col-resize-handle:hover{background:#3b82f6b3}.col-resize-handle:active{background:#3b82f6e6}:host ::ng-deep th.mat-mdc-header-cell{position:relative}:host ::ng-deep th.mat-mdc-header-cell.drag-enabled{overflow:visible!important}:host ::ng-deep .edit-mode-row{pointer-events:none;-webkit-user-select:none;user-select:none;opacity:.8}.cell-edit-mode{pointer-events:none;-webkit-user-select:none;user-select:none;color:var(--jqt-text-muted)}.group-header-row{cursor:pointer}:host ::ng-deep .jquery-table{border-spacing:0}:host ::ng-deep .group-header-row td{padding:0!important;background:var(--jqt-group-bg);border-bottom:1px solid var(--jqt-group-border);border-top:1px solid var(--jqt-group-border);border-left:3px solid var(--jqt-group-accent);position:sticky;top:var(--actual-header-height, 56px);z-index:2;box-shadow:-4px 0 0 0 var(--jqt-group-bg)}:host ::ng-deep .group-header-row:hover td{background:var(--jqt-group-bg-hover);box-shadow:-4px 0 0 0 var(--jqt-group-bg-hover)}:host ::ng-deep .group-header-loading td,:host ::ng-deep .group-header-loading:hover td,:host ::ng-deep .group-header-too-many td,:host ::ng-deep .group-header-too-many:hover td{cursor:default;background:var(--jqt-surface-alt-color)}.group-too-many-label{font-size:12px;font-weight:500;color:var(--jqt-warn-color, #b45309);font-style:italic}.group-header-cell{padding:0!important}.group-header-label{display:flex;align-items:center;gap:6px;padding:6px 16px;font-size:12px;font-weight:700;color:var(--jqt-text-color);text-transform:uppercase;letter-spacing:.04em}.group-key-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-count-badge{flex-shrink:0;font-size:11px;line-height:1;font-weight:600;color:#fff;background:var(--jqt-text-secondary, #6b7280);border-radius:10px;padding:3px 7px;min-width:20px;text-align:center;text-transform:none;letter-spacing:0}.group-pagination{display:inline-flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.group-page-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:1px solid var(--jqt-border-color);border-radius:5px;background:var(--jqt-surface-color);cursor:pointer;font-size:14px;color:var(--jqt-text-color);line-height:1;padding:0 0 3px;text-transform:none;letter-spacing:0}.group-page-btn:hover:not(:disabled){border-color:var(--jqt-text-muted);background:var(--jqt-surface-alt-color)}.group-page-btn:disabled{opacity:.35;cursor:default}.group-page-info{font-size:11px;color:var(--jqt-text-secondary);min-width:36px;text-align:center;font-weight:500;text-transform:none;letter-spacing:0}.group-chevron{flex-shrink:0;font-size:14px;font-weight:700;padding-bottom:3px;transition:transform .15s ease;transform:rotate(90deg)}.group-chevron.collapsed{transform:rotate(0)}.no-data-row{cursor:default!important}.no-data-cell{padding:0!important;height:0!important;border:none!important}.jquery-table{width:100%;table-layout:fixed}:host ::ng-deep .mat-mdc-table{width:100%;background:transparent}:host ::ng-deep .mat-mdc-header-cell,:host ::ng-deep .mat-mdc-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host ::ng-deep .mat-mdc-header-row{background:var(--jqt-header-bg)}:host ::ng-deep .mat-mdc-header-cell{background:var(--jqt-header-bg);color:var(--jqt-header-text-color)}:host ::ng-deep .mat-mdc-row,:host ::ng-deep .mat-mdc-cell{background:var(--jqt-cell-bg)}:host ::ng-deep .mat-mdc-row{cursor:pointer;border-bottom-color:var(--jqt-border-color)}:host ::ng-deep .mat-mdc-row.no-pointer{cursor:auto}:host ::ng-deep .mat-mdc-row.no-data-row{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drag-enabled{-webkit-user-select:none;user-select:none}:host ::ng-deep .mat-mdc-header-cell.drag-enabled:active{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drop-target{background:var(--jqt-row-hover-bg)}.head-cell{display:flex;align-items:center;justify-content:space-between;gap:6px;min-width:0}.drag-handle{display:inline-flex;align-items:center;cursor:grab;color:var(--jqt-border-color);flex-shrink:0;padding:0 2px;border-radius:4px;line-height:1}.drag-handle:active{cursor:grabbing}.drag-handle:hover{color:var(--jqt-text-muted)}.drag-handle mat-icon{font-size:16px;width:16px;height:16px;line-height:1}.head-icon{font-size:14px;width:14px;height:14px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.head-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase;font-size:11px;font-weight:600;letter-spacing:.05em}.head-actions{display:inline-flex;align-items:center;gap:6px}.cell-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cell-empty{color:var(--jqt-empty-cell-color, #9ca3af);font-style:italic}.bottom-bar{display:flex;align-items:center;flex:0 0 auto;position:relative;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)}:host ::ng-deep .bottom-bar.is-loading .mat-mdc-paginator-range-label{visibility:hidden}.paginator-range-loader{position:absolute;right:0;top:50%;transform:translateY(-50%);margin-right:145px;display:flex;align-items:center;justify-content:center;width:auto;padding:0 16px;height:24px}.paginator-range-loader .spinner{width:16px;height:16px;border:2.5px solid #e5e7eb;border-top-color:#a0a5ad}.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"] }]
1807
+ args: [{ standalone: true, selector: 'jquery-table', imports: [CommonModule, FormsModule, MatTableModule, MatButtonModule, MatDividerModule, MatPaginatorModule, MatSortModule, MatMenuModule, MatIconModule, MatSelectModule, SlicePanelComponent, OrganizerButtonWrapperComponent], 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\">filter_list</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\" *ngIf=\"!effectiveIsLoading\">{{ filteredRowCount }}</span>\n <div class=\"search-count-loader\" *ngIf=\"effectiveIsLoading\">\n <div class=\"spinner\"></div>\n </div>\n </div>\n </div>\n <div *ngIf=\"showOrganizerButton\" class=\"table-toolbar-right\">\n <table-organizer-button\n [config]=\"organizerConfig\"\n [state]=\"organizerState\"\n (viewChange)=\"onOrganizerViewChange($event)\">\n </table-organizer-button>\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\" [class.is-loading]=\"effectiveIsLoading\" *ngIf=\"showPagination\">\n <mat-paginator\n [length]=\"totalRows\"\n [pageSize]=\"pageSize\"\n [pageSizeOptions]=\"pageSizeOptions\"\n [showFirstLastButtons]=\"resolvedPaginationShowFirstLastButtons\"\n ></mat-paginator>\n <div class=\"paginator-range-loader\" \n [class.paginator-range-loader--no-first-last]=\"!resolvedPaginationShowFirstLastButtons\"\n *ngIf=\"effectiveIsLoading\">\n <div class=\"spinner\"></div>\n </div>\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-count-loader{flex-shrink:0;display:flex;align-items:center;justify-content:center;min-width:20px;height:20px}.spinner{width:14px;height:14px;border:2px solid #d1d5db;border-top-color:#374151;border-radius:50%;animation:spin .8s linear infinite}.search-count-loader .spinner{width:16px;height:16px;border:2.5px solid #e5e7eb;border-top-color:#a0a5ad}@keyframes spin{to{transform:rotate(360deg)}}.search-clear-btn{background:none;border:none;cursor:pointer;color:var(--jqt-text-muted);font-size:16px;padding:0;line-height:1;flex-shrink:0}.search-clear-btn:hover{color:var(--jqt-text-color)}.slice-expand-btn{flex-shrink:0;width:34px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);cursor:pointer;display:inline-flex;align-items:center;justify-content:center;color:var(--jqt-text-color);padding:0;line-height:1}.slice-expand-btn:hover{border-color:var(--jqt-text-muted)}.slice-expand-mat-icon{font-size:18px;width:18px;height:18px;line-height:1}.view-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.view-btn:hover{border-color:var(--jqt-text-muted)}.view-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.export-btn{display:inline-flex;align-items:center;gap:5px;height:34px;border:1px solid var(--jqt-border-color);border-radius:8px;background:var(--jqt-surface-color);padding:0 12px;font-size:13px;font-weight:500;color:var(--jqt-text-color);cursor:pointer;white-space:nowrap;box-sizing:border-box}.export-btn:hover{border-color:var(--jqt-text-muted)}.export-btn-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0}.view-menu-export-divider{margin:4px 0!important}.view-menu-export-item{display:flex;align-items:center;gap:6px;width:100%;padding:0 16px;height:40px;border:none;background:transparent;cursor:pointer;font-size:13px;font-weight:500;color:var(--jqt-text-color);text-align:left;box-sizing:border-box}.view-menu-export-item:hover{background:#0000000a}.view-menu-export-icon{font-size:16px;width:16px;height:16px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.edit-confirm-btn{position:absolute;top:10px;right:10px;z-index:200;display:flex;align-items:center;justify-content:center;width:36px;height:36px;border:none;border-radius:50%;background:#16a34a;cursor:pointer;box-shadow:0 2px 8px #00000040;transition:background .15s}.edit-confirm-btn mat-icon{color:#fff;font-size:22px;width:22px;height:22px}.edit-confirm-btn:hover{background:#15803d}.preferences-message{display:flex;align-items:center;gap:8px;margin:0 0 8px;padding:8px 12px;background:#f0fdf4;border:1px solid #bbf7d0;border-radius:6px;font-size:12px;color:#15803d}.preferences-message .preferences-message-icon{font-size:16px;width:16px;height:16px}:host ::ng-deep .edit-mode-header{background:#3b82f60a!important;cursor:grab!important;-webkit-user-select:none;user-select:none;overflow:visible!important}.col-resize-handle{position:absolute;top:0;right:0;width:6px;height:100%;cursor:col-resize!important;background:#3b82f640;z-index:20}.col-resize-handle:hover{background:#3b82f6b3}.col-resize-handle:active{background:#3b82f6e6}:host ::ng-deep th.mat-mdc-header-cell{position:relative}:host ::ng-deep th.mat-mdc-header-cell.drag-enabled{overflow:visible!important}:host ::ng-deep .edit-mode-row{pointer-events:none;-webkit-user-select:none;user-select:none;opacity:.8}.cell-edit-mode{pointer-events:none;-webkit-user-select:none;user-select:none;color:var(--jqt-text-muted)}.group-header-row{cursor:pointer}:host ::ng-deep .jquery-table{border-spacing:0}:host ::ng-deep .group-header-row td{padding:0!important;background:var(--jqt-group-bg);border-bottom:1px solid var(--jqt-group-border);border-top:1px solid var(--jqt-group-border);border-left:3px solid var(--jqt-group-accent);position:sticky;top:var(--actual-header-height, 56px);z-index:2;box-shadow:-4px 0 0 0 var(--jqt-group-bg)}:host ::ng-deep .group-header-row:hover td{background:var(--jqt-group-bg-hover);box-shadow:-4px 0 0 0 var(--jqt-group-bg-hover)}:host ::ng-deep .group-header-loading td,:host ::ng-deep .group-header-loading:hover td,:host ::ng-deep .group-header-too-many td,:host ::ng-deep .group-header-too-many:hover td{cursor:default;background:var(--jqt-surface-alt-color)}.group-too-many-label{font-size:12px;font-weight:500;color:var(--jqt-warn-color, #b45309);font-style:italic}.group-header-cell{padding:0!important}.group-header-label{display:flex;align-items:center;gap:6px;padding:6px 16px;font-size:12px;font-weight:700;color:var(--jqt-text-color);text-transform:uppercase;letter-spacing:.04em}.group-key-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.group-count-badge{flex-shrink:0;font-size:11px;line-height:1;font-weight:600;color:#fff;background:var(--jqt-text-secondary, #6b7280);border-radius:10px;padding:3px 7px;min-width:20px;text-align:center;text-transform:none;letter-spacing:0}.group-pagination{display:inline-flex;align-items:center;gap:4px;flex-shrink:0;margin-left:auto}.group-page-btn{display:inline-flex;align-items:center;justify-content:center;width:22px;height:22px;border:1px solid var(--jqt-border-color);border-radius:5px;background:var(--jqt-surface-color);cursor:pointer;font-size:14px;color:var(--jqt-text-color);line-height:1;padding:0 0 3px;text-transform:none;letter-spacing:0}.group-page-btn:hover:not(:disabled){border-color:var(--jqt-text-muted);background:var(--jqt-surface-alt-color)}.group-page-btn:disabled{opacity:.35;cursor:default}.group-page-info{font-size:11px;color:var(--jqt-text-secondary);min-width:36px;text-align:center;font-weight:500;text-transform:none;letter-spacing:0}.group-chevron{flex-shrink:0;font-size:14px;font-weight:700;padding-bottom:3px;transition:transform .15s ease;transform:rotate(90deg)}.group-chevron.collapsed{transform:rotate(0)}.no-data-row{cursor:default!important}.no-data-cell{padding:0!important;height:0!important;border:none!important}.jquery-table{width:100%;table-layout:fixed}:host ::ng-deep .mat-mdc-table{width:100%;background:transparent}:host ::ng-deep .mat-mdc-header-cell,:host ::ng-deep .mat-mdc-cell{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}:host ::ng-deep .mat-mdc-header-row{background:var(--jqt-header-bg)}:host ::ng-deep .mat-mdc-header-cell{background:var(--jqt-header-bg);color:var(--jqt-header-text-color)}:host ::ng-deep .mat-mdc-row,:host ::ng-deep .mat-mdc-cell{background:var(--jqt-cell-bg)}:host ::ng-deep .mat-mdc-row{cursor:pointer;border-bottom-color:var(--jqt-border-color)}:host ::ng-deep .mat-mdc-row.no-pointer{cursor:auto}:host ::ng-deep .mat-mdc-row.no-data-row{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drag-enabled{-webkit-user-select:none;user-select:none}:host ::ng-deep .mat-mdc-header-cell.drag-enabled:active{cursor:default}:host ::ng-deep .mat-mdc-header-cell.drop-target{background:var(--jqt-row-hover-bg)}.head-cell{display:flex;align-items:center;justify-content:space-between;gap:6px;min-width:0}.drag-handle{display:inline-flex;align-items:center;cursor:grab;color:var(--jqt-border-color);flex-shrink:0;padding:0 2px;border-radius:4px;line-height:1}.drag-handle:active{cursor:grabbing}.drag-handle:hover{color:var(--jqt-text-muted)}.drag-handle mat-icon{font-size:16px;width:16px;height:16px;line-height:1}.head-icon{font-size:14px;width:14px;height:14px;line-height:1;flex-shrink:0;color:var(--jqt-text-muted)}.head-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;text-transform:uppercase;font-size:11px;font-weight:600;letter-spacing:.05em}.head-actions{display:inline-flex;align-items:center;gap:6px}.cell-text{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.cell-empty{color:var(--jqt-empty-cell-color, #9ca3af);font-style:italic}.bottom-bar{display:flex;align-items:center;flex:0 0 auto;position:relative;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)}:host ::ng-deep .bottom-bar.is-loading .mat-mdc-paginator-range-label{visibility:hidden}.paginator-range-loader{position:absolute;right:0;top:50%;transform:translateY(-50%);margin-right:145px;display:flex;align-items:center;justify-content:center;width:auto;padding:0 16px;height:24px}.paginator-range-loader--no-first-last{margin-right:80px}.paginator-range-loader .spinner{width:16px;height:16px;border:2.5px solid #e5e7eb;border-top-color:#a0a5ad}.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"] }]
2652
1808
  }], propDecorators: { config: [{
2653
1809
  type: Input
2654
1810
  }], dataSource: [{
@@ -2699,9 +1855,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
2699
1855
  }], slicePanelRef: [{
2700
1856
  type: ViewChild,
2701
1857
  args: [SlicePanelComponent]
2702
- }], viewMenuTrigger: [{
2703
- type: ViewChild,
2704
- args: ['viewMenuTrigger']
2705
1858
  }], _cellDefs: [{
2706
1859
  type: ContentChildren,
2707
1860
  args: [JqtCellDefDirective]
@@ -2715,5 +1868,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
2715
1868
  * Generated bundle index. Do not edit.
2716
1869
  */
2717
1870
 
2718
- export { JQT_I18N, JQT_I18N_DEFAULTS, JqtCellDefDirective, SlicePanelComponent, TableComponent, col };
1871
+ export { JQT_I18N, JQT_I18N_DEFAULTS, JqtCellDefDirective, OrganizerButtonWrapperComponent, TableComponent, col };
2719
1872
  //# sourceMappingURL=oneteme-jquery-table.mjs.map