@oneteme/jquery-table 0.0.7 → 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/esm2022/lib/component/organizer-button/organizer-button.component.mjs +49 -0
- package/esm2022/lib/component/table.component.mjs +185 -114
- package/esm2022/lib/jqt-i18n.token.mjs +2 -2
- package/esm2022/lib/jquery-table.model.mjs +1 -1
- package/esm2022/public-api.mjs +4 -3
- package/fesm2022/oneteme-jquery-table.mjs +265 -1107
- package/fesm2022/oneteme-jquery-table.mjs.map +1 -1
- package/lib/component/organizer-button/organizer-button.component.d.ts +12 -0
- package/lib/component/table.component.d.ts +10 -8
- package/lib/jquery-table.model.d.ts +2 -2
- package/package.json +3 -2
- package/public-api.d.ts +3 -2
- package/esm2022/lib/component/group-by.manager.mjs +0 -99
- package/esm2022/lib/component/lazy-column-manager.mjs +0 -99
- package/esm2022/lib/component/slice-panel/slice-panel.component.mjs +0 -472
- package/esm2022/lib/component/slice-panel/slice-panel.model.mjs +0 -2
- package/esm2022/lib/component/view/view.facade.mjs +0 -304
- package/lib/component/group-by.manager.d.ts +0 -44
- package/lib/component/lazy-column-manager.d.ts +0 -26
- package/lib/component/slice-panel/slice-panel.component.d.ts +0 -81
- package/lib/component/slice-panel/slice-panel.model.d.ts +0 -69
- package/lib/component/view/view.facade.d.ts +0 -95
|
@@ -1,28 +1,27 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, Directive, Input, EventEmitter,
|
|
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
|
|
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
|
|
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
|
|
12
|
+
import * as i4 from '@angular/material/paginator';
|
|
15
13
|
import { MatPaginatorIntl, MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
|
|
16
|
-
import * as
|
|
14
|
+
import * as i8 from '@angular/material/select';
|
|
17
15
|
import { MatSelectModule } from '@angular/material/select';
|
|
18
|
-
import * as
|
|
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,
|
|
23
|
-
import '@oneteme/jquery-
|
|
24
|
-
|
|
25
|
-
import * as
|
|
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: '
|
|
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,471 +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
|
-
this._cdr.markForCheck();
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
// ── Computed
|
|
156
|
-
get showPanel() {
|
|
157
|
-
const hasCfg = (this.sliceConfigs || []).length > 0 || this._dynamicSlices.length > 0;
|
|
158
|
-
if (this.alwaysShow)
|
|
159
|
-
return hasCfg;
|
|
160
|
-
return hasCfg && (this.data?.length ?? 0) > 0;
|
|
161
|
-
}
|
|
162
|
-
get slices() {
|
|
163
|
-
return this._cachedSlices;
|
|
164
|
-
}
|
|
165
|
-
get staticSliceCount() {
|
|
166
|
-
return (this.sliceConfigs || []).length;
|
|
167
|
-
}
|
|
168
|
-
get staticSlicesForMenu() {
|
|
169
|
-
return (this.sliceConfigs || [])
|
|
170
|
-
.filter((s) => !!s.columnKey)
|
|
171
|
-
.map((s) => ({ key: s.columnKey, title: s.title || s.columnKey }));
|
|
172
|
-
}
|
|
173
|
-
get activeDynamicSliceColumns() {
|
|
174
|
-
return this._dynamicSlices
|
|
175
|
-
.map((d) => (this.columns || []).find((c) => c.key === d.key))
|
|
176
|
-
.filter((c) => !!c);
|
|
177
|
-
}
|
|
178
|
-
get availableColumnsForDynamicSlice() {
|
|
179
|
-
const dynamicKeys = new Set(this._dynamicSlices.map((d) => d.key));
|
|
180
|
-
const staticKeys = new Set((this.sliceConfigs || []).map((s) => s.columnKey).filter((k) => !!k));
|
|
181
|
-
const existingKeys = new Set([...dynamicKeys, ...staticKeys]);
|
|
182
|
-
const seen = new Set();
|
|
183
|
-
return (this.columns || []).filter((c) => {
|
|
184
|
-
if (existingKeys.has(c.key) || seen.has(c.key))
|
|
185
|
-
return false;
|
|
186
|
-
seen.add(c.key);
|
|
187
|
-
return true;
|
|
188
|
-
});
|
|
189
|
-
}
|
|
190
|
-
get activeSliceByLabel() {
|
|
191
|
-
const staticActiveCount = (this.sliceConfigs || []).length;
|
|
192
|
-
const total = staticActiveCount + this._dynamicSlices.length;
|
|
193
|
-
if (total === 0)
|
|
194
|
-
return 'Aucun';
|
|
195
|
-
if (total === 1) {
|
|
196
|
-
const first = (this.sliceConfigs || [])[0];
|
|
197
|
-
if (first)
|
|
198
|
-
return first.title || first.columnKey || '1';
|
|
199
|
-
return this.activeDynamicSliceColumns[0]?.header || '1';
|
|
200
|
-
}
|
|
201
|
-
return `${total} actifs`;
|
|
202
|
-
}
|
|
203
|
-
// ── UI state
|
|
204
|
-
isSliceCollapsed(sliceIndex) {
|
|
205
|
-
if (this._cachedSlices.length === 1) {
|
|
206
|
-
return this._expandedSlices.has(-1 - sliceIndex);
|
|
207
|
-
}
|
|
208
|
-
return !this._expandedSlices.has(sliceIndex);
|
|
209
|
-
}
|
|
210
|
-
toggleSliceCollapse(sliceIndex) {
|
|
211
|
-
if (this.slices.length === 1) {
|
|
212
|
-
const closedKey = -1 - sliceIndex;
|
|
213
|
-
if (this._expandedSlices.has(closedKey)) {
|
|
214
|
-
this._expandedSlices.delete(closedKey);
|
|
215
|
-
}
|
|
216
|
-
else {
|
|
217
|
-
this._expandedSlices.add(closedKey);
|
|
218
|
-
this._scrollSliceTitleIntoView(sliceIndex);
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
if (this._expandedSlices.has(sliceIndex)) {
|
|
223
|
-
this._expandedSlices.delete(sliceIndex);
|
|
224
|
-
}
|
|
225
|
-
else {
|
|
226
|
-
this._expandedSlices.clear();
|
|
227
|
-
this._expandedSlices.add(sliceIndex);
|
|
228
|
-
this._scrollSliceTitleIntoView(sliceIndex);
|
|
229
|
-
}
|
|
230
|
-
}
|
|
231
|
-
this._cdr.markForCheck();
|
|
232
|
-
}
|
|
233
|
-
togglePanel() {
|
|
234
|
-
this.isCollapsed = !this.isCollapsed;
|
|
235
|
-
this.collapsedChange.emit(this.isCollapsed);
|
|
236
|
-
this._cdr.markForCheck();
|
|
237
|
-
}
|
|
238
|
-
isSliceDynamic(sliceIndex) {
|
|
239
|
-
return sliceIndex >= this.staticSliceCount;
|
|
240
|
-
}
|
|
241
|
-
isSliceLoading(sliceIndex) {
|
|
242
|
-
const staticSlice = (this.sliceConfigs || [])[sliceIndex];
|
|
243
|
-
if (staticSlice?.columnKey) {
|
|
244
|
-
const colDef = (this.columns || []).find(c => c.key === staticSlice.columnKey);
|
|
245
|
-
if (colDef?.lazy) {
|
|
246
|
-
const status = this.lazyStatus.get(staticSlice.columnKey) ?? 'idle';
|
|
247
|
-
return status === 'loading' || status === 'idle';
|
|
248
|
-
}
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
const key = this._getDynamicSliceColumnKey(sliceIndex);
|
|
252
|
-
if (!key)
|
|
253
|
-
return false;
|
|
254
|
-
const colDef = (this.columns || []).find(c => c.key === key);
|
|
255
|
-
if (!colDef?.lazy)
|
|
256
|
-
return false;
|
|
257
|
-
const status = this.lazyStatus.get(key) ?? 'idle';
|
|
258
|
-
return status === 'loading' || status === 'idle';
|
|
259
|
-
}
|
|
260
|
-
isStaticSliceVisible(_columnKey) {
|
|
261
|
-
return true;
|
|
262
|
-
}
|
|
263
|
-
toggleStaticSlice(_columnKey) {
|
|
264
|
-
// Délégué à table.component via _staticSliceHiddenKeys — ne rien faire ici.
|
|
265
|
-
}
|
|
266
|
-
// ── Selection
|
|
267
|
-
getSliceIcon(slice) {
|
|
268
|
-
if (slice.icon)
|
|
269
|
-
return slice.icon;
|
|
270
|
-
if (slice.columnKey) {
|
|
271
|
-
return (this.columns || []).find(c => c.key === slice.columnKey)?.icon;
|
|
272
|
-
}
|
|
273
|
-
return undefined;
|
|
274
|
-
}
|
|
275
|
-
isSliceCategoryActive(sliceIndex, categoryKey) {
|
|
276
|
-
return this.activeKeysBySlice.get(sliceIndex)?.has(categoryKey) ?? false;
|
|
277
|
-
}
|
|
278
|
-
isSliceAllActive(sliceIndex) {
|
|
279
|
-
return !this.activeKeysBySlice.has(sliceIndex) || this.activeKeysBySlice.get(sliceIndex).size === 0;
|
|
280
|
-
}
|
|
281
|
-
sliceCategoryCount(sliceIndex, categoryKey) {
|
|
282
|
-
const cacheKey = `${sliceIndex}:${categoryKey}`;
|
|
283
|
-
if (this._countCache.has(cacheKey)) {
|
|
284
|
-
return this._countCache.get(cacheKey);
|
|
285
|
-
}
|
|
286
|
-
const slice = this._cachedSlices[sliceIndex];
|
|
287
|
-
if (!slice)
|
|
288
|
-
return 0;
|
|
289
|
-
const category = (slice.categories || []).find((c) => c.key === categoryKey);
|
|
290
|
-
const count = category ? (this.data || []).filter((row) => category.filter(row)).length : 0;
|
|
291
|
-
this._countCache.set(cacheKey, count);
|
|
292
|
-
return count;
|
|
293
|
-
}
|
|
294
|
-
isSliceEmpty(sliceIndex) {
|
|
295
|
-
const slice = this._cachedSlices[sliceIndex];
|
|
296
|
-
if (!slice)
|
|
297
|
-
return true;
|
|
298
|
-
return (slice.categories || []).every(c => this.sliceCategoryCount(sliceIndex, c.key) === 0);
|
|
299
|
-
}
|
|
300
|
-
onSliceCategorySelected(sliceIndex, categoryKey) {
|
|
301
|
-
const slice = this.slices[sliceIndex];
|
|
302
|
-
if (!slice)
|
|
303
|
-
return;
|
|
304
|
-
if (!this.activeKeysBySlice.has(sliceIndex)) {
|
|
305
|
-
this.activeKeysBySlice.set(sliceIndex, new Set());
|
|
306
|
-
}
|
|
307
|
-
const activeKeys = this.activeKeysBySlice.get(sliceIndex);
|
|
308
|
-
if (slice.multiSelect === false) {
|
|
309
|
-
if (activeKeys.has(categoryKey)) {
|
|
310
|
-
activeKeys.clear();
|
|
311
|
-
}
|
|
312
|
-
else {
|
|
313
|
-
activeKeys.clear();
|
|
314
|
-
activeKeys.add(categoryKey);
|
|
315
|
-
}
|
|
316
|
-
}
|
|
317
|
-
else {
|
|
318
|
-
if (activeKeys.has(categoryKey)) {
|
|
319
|
-
activeKeys.delete(categoryKey);
|
|
320
|
-
}
|
|
321
|
-
else {
|
|
322
|
-
activeKeys.add(categoryKey);
|
|
323
|
-
}
|
|
324
|
-
}
|
|
325
|
-
this._emitFilter();
|
|
326
|
-
this._cdr.markForCheck();
|
|
327
|
-
}
|
|
328
|
-
onSliceAllSelected(sliceIndex) {
|
|
329
|
-
if (this.activeKeysBySlice.has(sliceIndex)) {
|
|
330
|
-
this.activeKeysBySlice.get(sliceIndex).clear();
|
|
331
|
-
}
|
|
332
|
-
this._emitFilter();
|
|
333
|
-
this._cdr.markForCheck();
|
|
334
|
-
}
|
|
335
|
-
/** Retourne l'état des filtres actifs sous forme sérialisable (sliceIndex → tableau de clés).
|
|
336
|
-
* Inclut les slices sans sélection (tableau vide) pour distinguer "aucune tranche sélectionnée"
|
|
337
|
-
* (= filtre ouvert, rien coché) de "slice absente". */
|
|
338
|
-
getActiveFilters() {
|
|
339
|
-
const result = {};
|
|
340
|
-
this._cachedSlices.forEach((_, idx) => {
|
|
341
|
-
const keys = this.activeKeysBySlice.get(idx);
|
|
342
|
-
result[idx] = keys ? [...keys] : [];
|
|
343
|
-
});
|
|
344
|
-
return result;
|
|
345
|
-
}
|
|
346
|
-
/** Restaure les filtres actifs depuis un snapshot sérialisé et réémet le filtre. */
|
|
347
|
-
restoreFilters(filters) {
|
|
348
|
-
this.activeKeysBySlice = new Map(Object.entries(filters).map(([idx, keys]) => [Number(idx), new Set(keys)]));
|
|
349
|
-
this._emitFilter();
|
|
350
|
-
this._cdr.markForCheck();
|
|
351
|
-
}
|
|
352
|
-
// ── Dynamic slice management
|
|
353
|
-
addDynamicSlice(column) {
|
|
354
|
-
const distinctValues = this._computeDistinctValues(column.key);
|
|
355
|
-
const newSlice = {
|
|
356
|
-
title: column.header,
|
|
357
|
-
columnKey: column.key,
|
|
358
|
-
categories: distinctValues.map((v) => this._buildCategoryFromValue(column.key, v)),
|
|
359
|
-
};
|
|
360
|
-
this._dynamicSlices = [...this._dynamicSlices, { key: column.key, slice: newSlice }];
|
|
361
|
-
this._rebuildCache();
|
|
362
|
-
this.dynamicSliceKeysChange.emit(this._dynamicSlices.map(d => d.key));
|
|
363
|
-
this._emitFilter();
|
|
364
|
-
this._cdr.markForCheck();
|
|
365
|
-
}
|
|
366
|
-
removeDynamicSlice(sliceIndex) {
|
|
367
|
-
const dynamicIndex = sliceIndex - this.staticSliceCount;
|
|
368
|
-
if (dynamicIndex < 0)
|
|
369
|
-
return;
|
|
370
|
-
const newActiveKeys = new Map();
|
|
371
|
-
this.activeKeysBySlice.forEach((keys, idx) => {
|
|
372
|
-
if (idx < sliceIndex)
|
|
373
|
-
newActiveKeys.set(idx, keys);
|
|
374
|
-
else if (idx > sliceIndex)
|
|
375
|
-
newActiveKeys.set(idx - 1, keys);
|
|
376
|
-
});
|
|
377
|
-
this.activeKeysBySlice = newActiveKeys;
|
|
378
|
-
this._dynamicSlices = this._dynamicSlices.filter((_, i) => i !== dynamicIndex);
|
|
379
|
-
this._rebuildCache();
|
|
380
|
-
this.dynamicSliceKeysChange.emit(this._dynamicSlices.map(d => d.key));
|
|
381
|
-
this._emitFilter();
|
|
382
|
-
this._cdr.markForCheck();
|
|
383
|
-
}
|
|
384
|
-
removeDynamicSliceByKey(key) {
|
|
385
|
-
const dynamicIndex = this._dynamicSlices.findIndex((d) => d.key === key);
|
|
386
|
-
if (dynamicIndex < 0)
|
|
387
|
-
return;
|
|
388
|
-
const sliceIndex = this.staticSliceCount + dynamicIndex;
|
|
389
|
-
this.removeDynamicSlice(sliceIndex);
|
|
390
|
-
}
|
|
391
|
-
// ── Private helpers
|
|
392
|
-
_emitFilter() {
|
|
393
|
-
const slices = this.slices;
|
|
394
|
-
const activeKeysBySlice = this.activeKeysBySlice;
|
|
395
|
-
this.activeKeysChange.emit(slices.map((_, i) => {
|
|
396
|
-
const keys = activeKeysBySlice.get(i);
|
|
397
|
-
return keys ? [...keys] : [];
|
|
398
|
-
}));
|
|
399
|
-
const pred = (row) => slices.every((slice, sliceIndex) => {
|
|
400
|
-
const activeKeys = activeKeysBySlice.get(sliceIndex);
|
|
401
|
-
if (!activeKeys || activeKeys.size === 0)
|
|
402
|
-
return true;
|
|
403
|
-
return [...activeKeys].some((key) => {
|
|
404
|
-
const category = (slice.categories || []).find((c) => c.key === key);
|
|
405
|
-
if (!category)
|
|
406
|
-
return false;
|
|
407
|
-
// Si pas de filter défini (usage chart), on ne filtre pas en mémoire
|
|
408
|
-
if (!category?.filter)
|
|
409
|
-
return true;
|
|
410
|
-
return category.filter(row);
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
this.filterChange.emit(pred);
|
|
414
|
-
}
|
|
415
|
-
_getDynamicSliceColumnKey(sliceIndex) {
|
|
416
|
-
const dynamicIndex = sliceIndex - this.staticSliceCount;
|
|
417
|
-
return this._dynamicSlices[dynamicIndex]?.key ?? null;
|
|
418
|
-
}
|
|
419
|
-
_getSliceIdentity(slice, fallbackIndex) {
|
|
420
|
-
if (slice.columnKey) {
|
|
421
|
-
return `column:${slice.columnKey}`;
|
|
422
|
-
}
|
|
423
|
-
if (slice.title) {
|
|
424
|
-
return `title:${slice.title}`;
|
|
425
|
-
}
|
|
426
|
-
const categoryKeys = (slice.categories || []).map((category) => category.key).join('|');
|
|
427
|
-
return `index:${fallbackIndex}|categories:${categoryKeys}`;
|
|
428
|
-
}
|
|
429
|
-
_materializeSlice(slice) {
|
|
430
|
-
if (!slice.columnKey || (slice.categories && slice.categories.length > 0)) {
|
|
431
|
-
return slice;
|
|
432
|
-
}
|
|
433
|
-
const col = (this.columns || []).find(c => c.key === slice.columnKey);
|
|
434
|
-
const isLazy = !!col?.lazy;
|
|
435
|
-
if (isLazy && this.lazyStatus.get(slice.columnKey) !== 'loaded') {
|
|
436
|
-
return { ...slice, categories: [] };
|
|
437
|
-
}
|
|
438
|
-
if (slice.bucket) {
|
|
439
|
-
const distinctValues = [...new Set((this.data || []).map(slice.bucket))].sort();
|
|
440
|
-
return {
|
|
441
|
-
...slice,
|
|
442
|
-
categories: distinctValues.map((v) => ({
|
|
443
|
-
key: v,
|
|
444
|
-
label: v,
|
|
445
|
-
filter: (row) => slice.bucket(row) === v,
|
|
446
|
-
})),
|
|
447
|
-
};
|
|
448
|
-
}
|
|
449
|
-
const distinctValues = this._computeDistinctValues(slice.columnKey);
|
|
450
|
-
return {
|
|
451
|
-
...slice,
|
|
452
|
-
categories: distinctValues.map((v) => this._buildCategoryFromValue(slice.columnKey, v)),
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
_buildCategoryFromValue(columnKey, v) {
|
|
456
|
-
if (v === EMPTY_CATEGORY_KEY) {
|
|
457
|
-
return {
|
|
458
|
-
key: EMPTY_CATEGORY_KEY,
|
|
459
|
-
label: EMPTY_CATEGORY_LABEL,
|
|
460
|
-
filter: (row) => {
|
|
461
|
-
const val = this._getLazyAwareValue(columnKey, row);
|
|
462
|
-
return val == null || val === '';
|
|
463
|
-
},
|
|
464
|
-
};
|
|
465
|
-
}
|
|
466
|
-
return {
|
|
467
|
-
key: v,
|
|
468
|
-
label: v,
|
|
469
|
-
filter: (row) => String(this._getLazyAwareValue(columnKey, row) ?? '') === v,
|
|
470
|
-
};
|
|
471
|
-
}
|
|
472
|
-
_computeDistinctValues(columnKey) {
|
|
473
|
-
const rawValues = (this.data || []).map((row) => {
|
|
474
|
-
const val = this._getLazyAwareValue(columnKey, row);
|
|
475
|
-
return val != null && val !== '' ? String(val) : null;
|
|
476
|
-
});
|
|
477
|
-
const distinct = [...new Set(rawValues.filter((v) => v !== null))].sort();
|
|
478
|
-
if (rawValues.some(v => v === null)) {
|
|
479
|
-
distinct.push(EMPTY_CATEGORY_KEY);
|
|
480
|
-
}
|
|
481
|
-
return distinct;
|
|
482
|
-
}
|
|
483
|
-
_getLazyAwareValue(columnKey, row) {
|
|
484
|
-
const colDef = (this.columns || []).find(c => c.key === columnKey);
|
|
485
|
-
if (colDef?.lazy) {
|
|
486
|
-
return this.lazyData.get(columnKey)?.get(row) ?? null;
|
|
487
|
-
}
|
|
488
|
-
if (colDef?.value) {
|
|
489
|
-
return colDef.value(row, 0);
|
|
490
|
-
}
|
|
491
|
-
return row[columnKey];
|
|
492
|
-
}
|
|
493
|
-
_rebuildCache() {
|
|
494
|
-
const previousSlices = this._cachedSlices;
|
|
495
|
-
const previousActiveKeys = this.activeKeysBySlice;
|
|
496
|
-
const allSlices = [...(this.sliceConfigs || []), ...this._dynamicSlices.map((d) => d.slice)];
|
|
497
|
-
this._cachedSlices = allSlices.map((slice) => this._materializeSlice(slice));
|
|
498
|
-
const previousKeysByIdentity = new Map();
|
|
499
|
-
previousSlices.forEach((slice, index) => {
|
|
500
|
-
const activeKeys = previousActiveKeys.get(index);
|
|
501
|
-
if (!activeKeys || activeKeys.size === 0)
|
|
502
|
-
return;
|
|
503
|
-
previousKeysByIdentity.set(this._getSliceIdentity(slice, index), new Set(activeKeys));
|
|
504
|
-
});
|
|
505
|
-
const nextActiveKeys = new Map();
|
|
506
|
-
this._cachedSlices.forEach((slice, index) => {
|
|
507
|
-
const previousKeys = previousKeysByIdentity.get(this._getSliceIdentity(slice, index));
|
|
508
|
-
if (!previousKeys || previousKeys.size === 0)
|
|
509
|
-
return;
|
|
510
|
-
const availableKeys = new Set((slice.categories || []).map((category) => category.key));
|
|
511
|
-
const matchingKeys = [...previousKeys].filter((key) => availableKeys.has(key));
|
|
512
|
-
if (matchingKeys.length > 0) {
|
|
513
|
-
nextActiveKeys.set(index, new Set(matchingKeys));
|
|
514
|
-
}
|
|
515
|
-
});
|
|
516
|
-
this.activeKeysBySlice = nextActiveKeys;
|
|
517
|
-
this._countCache.clear();
|
|
518
|
-
}
|
|
519
|
-
_refreshDynamicSlices() {
|
|
520
|
-
this._dynamicSlices = this._dynamicSlices.map(({ key, slice }) => {
|
|
521
|
-
const distinctValues = this._computeDistinctValues(key);
|
|
522
|
-
return {
|
|
523
|
-
key,
|
|
524
|
-
slice: {
|
|
525
|
-
...slice,
|
|
526
|
-
categories: distinctValues.map((v) => this._buildCategoryFromValue(key, v)),
|
|
527
|
-
},
|
|
528
|
-
};
|
|
529
|
-
});
|
|
530
|
-
}
|
|
531
|
-
_scrollSliceTitleIntoView(sliceIndex) {
|
|
532
|
-
setTimeout(() => {
|
|
533
|
-
const container = this.slicePanelBodyRef?.nativeElement;
|
|
534
|
-
if (!container)
|
|
535
|
-
return;
|
|
536
|
-
const row = container.querySelector(`[data-slice-index="${sliceIndex}"]`);
|
|
537
|
-
if (!row)
|
|
538
|
-
return;
|
|
539
|
-
row.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
540
|
-
}, 0);
|
|
541
|
-
}
|
|
542
|
-
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SlicePanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
543
|
-
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 }); }
|
|
544
|
-
}
|
|
545
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SlicePanelComponent, decorators: [{
|
|
546
|
-
type: Component,
|
|
547
|
-
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"] }]
|
|
548
|
-
}], propDecorators: { sliceConfigs: [{
|
|
549
|
-
type: Input
|
|
550
|
-
}], columns: [{
|
|
551
|
-
type: Input
|
|
552
|
-
}], data: [{
|
|
553
|
-
type: Input
|
|
554
|
-
}], lazyData: [{
|
|
555
|
-
type: Input
|
|
556
|
-
}], lazyStatus: [{
|
|
557
|
-
type: Input
|
|
558
|
-
}], showToggle: [{
|
|
559
|
-
type: Input
|
|
560
|
-
}], showCounts: [{
|
|
561
|
-
type: Input
|
|
562
|
-
}], collapsedByDefault: [{
|
|
563
|
-
type: Input
|
|
564
|
-
}], alwaysShow: [{
|
|
565
|
-
type: Input
|
|
566
|
-
}], filterChange: [{
|
|
567
|
-
type: Output
|
|
568
|
-
}], activeKeysChange: [{
|
|
569
|
-
type: Output
|
|
570
|
-
}], dynamicSliceKeysChange: [{
|
|
571
|
-
type: Output
|
|
572
|
-
}], collapsedChange: [{
|
|
573
|
-
type: Output
|
|
574
|
-
}], slicePanelBodyRef: [{
|
|
575
|
-
type: ViewChild,
|
|
576
|
-
args: ['slicePanelBody']
|
|
577
|
-
}] } });
|
|
578
|
-
|
|
579
113
|
/** Traductions françaises du paginator Material. */
|
|
580
114
|
function getFrenchPaginatorIntl() {
|
|
581
115
|
const intl = new MatPaginatorIntl();
|
|
@@ -634,435 +168,6 @@ function humanizeKey(key) {
|
|
|
634
168
|
.replace(/^./, (value) => value.toUpperCase());
|
|
635
169
|
}
|
|
636
170
|
|
|
637
|
-
// ── Facade
|
|
638
|
-
/**
|
|
639
|
-
* Centralise l'état et les actions du panneau View (Champs / Group by / Slice by).
|
|
640
|
-
* Pas de dépendance Angular/DOM : peut être extraite vers jquery-core à terme.
|
|
641
|
-
*
|
|
642
|
-
* Usage :
|
|
643
|
-
* this._view = new ViewFacade();
|
|
644
|
-
* this._view.events$.subscribe(e => { ... });
|
|
645
|
-
* this._view.update({ config, columns, sliceConfigs });
|
|
646
|
-
*/
|
|
647
|
-
class ViewFacade {
|
|
648
|
-
constructor() {
|
|
649
|
-
// ── State
|
|
650
|
-
this.fields = {
|
|
651
|
-
activeColumns: [],
|
|
652
|
-
userCustomized: false,
|
|
653
|
-
};
|
|
654
|
-
this.groupBy = {
|
|
655
|
-
activeKey: null,
|
|
656
|
-
userCustomized: false,
|
|
657
|
-
};
|
|
658
|
-
this.sliceBy = {
|
|
659
|
-
activeDynamicColumns: [],
|
|
660
|
-
availableForDynamic: [],
|
|
661
|
-
allDynamicColumns: [],
|
|
662
|
-
activeLabel: 'Aucun',
|
|
663
|
-
};
|
|
664
|
-
this._panelConfig = { columns: [], sliceConfigs: [] };
|
|
665
|
-
this._hiddenStaticKeys = new Set();
|
|
666
|
-
// ── Events
|
|
667
|
-
this._events$ = new Subject();
|
|
668
|
-
this.events$ = this._events$.asObservable();
|
|
669
|
-
}
|
|
670
|
-
// ── Computed flags
|
|
671
|
-
get enabled() {
|
|
672
|
-
return this._panelConfig.config?.enabled === true;
|
|
673
|
-
}
|
|
674
|
-
get showFields() {
|
|
675
|
-
return this.enabled;
|
|
676
|
-
}
|
|
677
|
-
get showGroupBySection() {
|
|
678
|
-
return this.enabled && this.groupByColumns.length > 0;
|
|
679
|
-
}
|
|
680
|
-
get showSliceBySection() {
|
|
681
|
-
return this.enabled && ((this._panelConfig.sliceConfigs?.length ?? 0) > 0 ||
|
|
682
|
-
this.sliceBy.allDynamicColumns.length > 0);
|
|
683
|
-
}
|
|
684
|
-
get allowColumnRemoval() {
|
|
685
|
-
return this._panelConfig.config?.enableColumnRemoval !== false;
|
|
686
|
-
}
|
|
687
|
-
get isColumnDragDropEnabled() {
|
|
688
|
-
return this._panelConfig.config?.enableColumnDragDrop === true;
|
|
689
|
-
}
|
|
690
|
-
get menuBaseColumns() {
|
|
691
|
-
return this._uniqueByKey((this._panelConfig.columns || []).filter(c => !c.optional));
|
|
692
|
-
}
|
|
693
|
-
get menuOptionalColumns() {
|
|
694
|
-
return this._uniqueByKey((this._panelConfig.columns || []).filter(c => c.optional));
|
|
695
|
-
}
|
|
696
|
-
get totalColumnCount() {
|
|
697
|
-
return new Set((this._panelConfig.columns || []).map(c => c.key)).size;
|
|
698
|
-
}
|
|
699
|
-
get groupByColumns() {
|
|
700
|
-
const seen = new Set();
|
|
701
|
-
return (this._panelConfig.columns || []).filter(c => {
|
|
702
|
-
if (c.groupable === false || seen.has(c.key))
|
|
703
|
-
return false;
|
|
704
|
-
if (!c.header && c.groupable !== true)
|
|
705
|
-
return false;
|
|
706
|
-
seen.add(c.key);
|
|
707
|
-
return true;
|
|
708
|
-
});
|
|
709
|
-
}
|
|
710
|
-
get activeGroupByLabel() {
|
|
711
|
-
if (!this.groupBy.activeKey)
|
|
712
|
-
return this._panelConfig.noneLabel ?? 'Aucun';
|
|
713
|
-
const col = this.groupByColumns.find(c => c.key === this.groupBy.activeKey);
|
|
714
|
-
return col?.header || this.groupBy.activeKey;
|
|
715
|
-
}
|
|
716
|
-
get staticSlicesForMenu() {
|
|
717
|
-
return (this._panelConfig.sliceConfigs ?? [])
|
|
718
|
-
.filter(s => !!s.columnKey)
|
|
719
|
-
.map(s => {
|
|
720
|
-
const colIcon = (this._panelConfig.columns ?? []).find(c => c.key === s.columnKey)?.icon;
|
|
721
|
-
return { key: s.columnKey, title: s.title || humanizeKey(s.columnKey), icon: s.icon ?? colIcon };
|
|
722
|
-
});
|
|
723
|
-
}
|
|
724
|
-
colLabel(col) {
|
|
725
|
-
return col.header || humanizeKey(col.key);
|
|
726
|
-
}
|
|
727
|
-
// ── Mise à jour de la config (appelée depuis refreshViewModel)
|
|
728
|
-
update(panelConfig) {
|
|
729
|
-
this._panelConfig = panelConfig;
|
|
730
|
-
this._refreshDefaultColumns();
|
|
731
|
-
this._refreshDynamicSliceMeta();
|
|
732
|
-
if (!this.groupBy.userCustomized) {
|
|
733
|
-
this.groupBy.activeKey = panelConfig.defaultGroupBy ?? null;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
// ── Actions Fields
|
|
737
|
-
setActiveColumns(columns) {
|
|
738
|
-
this.fields.activeColumns = columns;
|
|
739
|
-
this.fields.userCustomized = true;
|
|
740
|
-
}
|
|
741
|
-
setUserCustomized(value) {
|
|
742
|
-
this.fields.userCustomized = value;
|
|
743
|
-
}
|
|
744
|
-
/** Remet la vue dans son état initial : colonnes par défaut, groupBy par défaut, pas de dynamic slices. */
|
|
745
|
-
resetToDefaults() {
|
|
746
|
-
this.fields.userCustomized = false;
|
|
747
|
-
this.groupBy.userCustomized = false;
|
|
748
|
-
this._refreshDefaultColumns();
|
|
749
|
-
this.groupBy.activeKey = this._panelConfig.defaultGroupBy ?? null;
|
|
750
|
-
this.sliceBy.activeDynamicColumns = [];
|
|
751
|
-
this._refreshDynamicSliceMeta();
|
|
752
|
-
}
|
|
753
|
-
addColumn(column) {
|
|
754
|
-
const configCols = this._panelConfig.columns ?? [];
|
|
755
|
-
const configIndex = configCols.findIndex(c => c.key === column.key);
|
|
756
|
-
if (configIndex === -1) {
|
|
757
|
-
this.fields.activeColumns = [...this.fields.activeColumns, column];
|
|
758
|
-
}
|
|
759
|
-
else {
|
|
760
|
-
const insertBefore = this.fields.activeColumns.findIndex(c => {
|
|
761
|
-
const ci = configCols.findIndex(cc => cc.key === c.key);
|
|
762
|
-
return ci !== -1 && ci > configIndex;
|
|
763
|
-
});
|
|
764
|
-
if (insertBefore === -1) {
|
|
765
|
-
this.fields.activeColumns = [...this.fields.activeColumns, column];
|
|
766
|
-
}
|
|
767
|
-
else {
|
|
768
|
-
const copy = [...this.fields.activeColumns];
|
|
769
|
-
copy.splice(insertBefore, 0, column);
|
|
770
|
-
this.fields.activeColumns = copy;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
this.fields.userCustomized = true;
|
|
774
|
-
this._events$.next({ type: 'fieldsChanged', fieldIds: this.fields.activeColumns.map(c => c.key) });
|
|
775
|
-
}
|
|
776
|
-
removeColumn(columnKey) {
|
|
777
|
-
const col = this.fields.activeColumns.find(c => c.key === columnKey);
|
|
778
|
-
if (!col)
|
|
779
|
-
return false;
|
|
780
|
-
if (!col.optional && !this.allowColumnRemoval)
|
|
781
|
-
return false;
|
|
782
|
-
if (this.fields.activeColumns.length <= 1)
|
|
783
|
-
return false;
|
|
784
|
-
this.fields.activeColumns = this.fields.activeColumns.filter(c => c.key !== columnKey);
|
|
785
|
-
this.fields.userCustomized = true;
|
|
786
|
-
this._events$.next({ type: 'fieldsChanged', fieldIds: this.fields.activeColumns.map(c => c.key) });
|
|
787
|
-
return true;
|
|
788
|
-
}
|
|
789
|
-
toggleColumnVisibility(column, visible) {
|
|
790
|
-
const isVisible = this.isColumnVisible(column.key);
|
|
791
|
-
if (visible && !isVisible) {
|
|
792
|
-
this.addColumn(column);
|
|
793
|
-
}
|
|
794
|
-
else if (!visible && isVisible) {
|
|
795
|
-
this.removeColumn(column.key);
|
|
796
|
-
}
|
|
797
|
-
}
|
|
798
|
-
reorderColumns(fromKey, toKey) {
|
|
799
|
-
const prev = this.fields.activeColumns.findIndex(c => c.key === fromKey);
|
|
800
|
-
const curr = this.fields.activeColumns.findIndex(c => c.key === toKey);
|
|
801
|
-
if (prev === -1 || curr === -1 || prev === curr)
|
|
802
|
-
return false;
|
|
803
|
-
const reordered = [...this.fields.activeColumns];
|
|
804
|
-
const [moved] = reordered.splice(prev, 1);
|
|
805
|
-
reordered.splice(curr, 0, moved);
|
|
806
|
-
this.fields.activeColumns = reordered;
|
|
807
|
-
this.fields.userCustomized = true;
|
|
808
|
-
this._events$.next({ type: 'fieldsChanged', fieldIds: this.fields.activeColumns.map(c => c.key) });
|
|
809
|
-
return true;
|
|
810
|
-
}
|
|
811
|
-
isColumnVisible(key) {
|
|
812
|
-
return this.fields.activeColumns.some(c => c.key === key);
|
|
813
|
-
}
|
|
814
|
-
// ── Actions Group by
|
|
815
|
-
setGroupBy(key) {
|
|
816
|
-
this.groupBy.activeKey = key;
|
|
817
|
-
this.groupBy.userCustomized = true;
|
|
818
|
-
this._events$.next({ type: 'groupByChanged', key });
|
|
819
|
-
}
|
|
820
|
-
// ── Actions Slice by (dynamique)
|
|
821
|
-
/** Appelée quand le SlicePanelComponent émet dynamicSliceKeysChange */
|
|
822
|
-
onDynamicSliceKeysChange(keys) {
|
|
823
|
-
const allCols = this._panelConfig.columns ?? [];
|
|
824
|
-
const staticKeys = new Set((this._panelConfig.sliceConfigs ?? []).map(s => s.columnKey).filter((k) => !!k));
|
|
825
|
-
const dynamicKeySet = new Set(keys);
|
|
826
|
-
const existingKeys = new Set([...staticKeys, ...dynamicKeySet]);
|
|
827
|
-
this.sliceBy.activeDynamicColumns = keys
|
|
828
|
-
.map(k => allCols.find(c => c.key === k))
|
|
829
|
-
.filter((c) => !!c);
|
|
830
|
-
const seen = new Set();
|
|
831
|
-
this.sliceBy.availableForDynamic = allCols.filter(c => {
|
|
832
|
-
if (c.sliceable === false || existingKeys.has(c.key) || seen.has(c.key))
|
|
833
|
-
return false;
|
|
834
|
-
seen.add(c.key);
|
|
835
|
-
return true;
|
|
836
|
-
});
|
|
837
|
-
this._computeSliceLabel();
|
|
838
|
-
this._events$.next({ type: 'dynamicSlicesChanged', keys });
|
|
839
|
-
}
|
|
840
|
-
isDynamicSliceActive(key) {
|
|
841
|
-
return this.sliceBy.activeDynamicColumns.some(c => c.key === key);
|
|
842
|
-
}
|
|
843
|
-
// ── Contrat core
|
|
844
|
-
/** Retourne un snapshot ViewState (contrat core) de l'état courant. */
|
|
845
|
-
toViewState() {
|
|
846
|
-
return {
|
|
847
|
-
selectedFieldIds: this.fields.activeColumns.map(c => c.key),
|
|
848
|
-
groupByKey: this.groupBy.activeKey,
|
|
849
|
-
dynamicSliceKeys: this.sliceBy.activeDynamicColumns.map(c => c.key),
|
|
850
|
-
};
|
|
851
|
-
}
|
|
852
|
-
/** Convertit une TableColumnProvider en ViewField (contrat core). */
|
|
853
|
-
static toViewField(col) {
|
|
854
|
-
return {
|
|
855
|
-
id: col.key,
|
|
856
|
-
label: col.header,
|
|
857
|
-
icon: col.icon,
|
|
858
|
-
groupable: col.groupable,
|
|
859
|
-
sliceable: col.sliceable,
|
|
860
|
-
optional: col.optional,
|
|
861
|
-
};
|
|
862
|
-
}
|
|
863
|
-
// ── Destructor
|
|
864
|
-
destroy() {
|
|
865
|
-
this._events$.complete();
|
|
866
|
-
}
|
|
867
|
-
// ── Helpers privés
|
|
868
|
-
_refreshDefaultColumns() {
|
|
869
|
-
const defaultCols = (this._panelConfig.columns || []).filter(c => !c.optional);
|
|
870
|
-
if (!this.fields.userCustomized || this.fields.activeColumns.length === 0) {
|
|
871
|
-
this.fields.activeColumns = [...defaultCols];
|
|
872
|
-
}
|
|
873
|
-
else {
|
|
874
|
-
const allowedKeys = new Set((this._panelConfig.columns || []).map(c => c.key));
|
|
875
|
-
this.fields.activeColumns = this.fields.activeColumns.filter(c => allowedKeys.has(c.key));
|
|
876
|
-
}
|
|
877
|
-
}
|
|
878
|
-
_refreshDynamicSliceMeta() {
|
|
879
|
-
const allCols = this._panelConfig.columns ?? [];
|
|
880
|
-
const staticKeys = new Set((this._panelConfig.sliceConfigs ?? []).map(s => s.columnKey).filter((k) => !!k));
|
|
881
|
-
const activeDynKeys = new Set(this.sliceBy.activeDynamicColumns.map(c => c.key));
|
|
882
|
-
const seen1 = new Set();
|
|
883
|
-
this.sliceBy.availableForDynamic = allCols.filter(c => {
|
|
884
|
-
if (c.sliceable === false || staticKeys.has(c.key) || activeDynKeys.has(c.key) || seen1.has(c.key))
|
|
885
|
-
return false;
|
|
886
|
-
if (!c.header && c.sliceable !== true)
|
|
887
|
-
return false;
|
|
888
|
-
seen1.add(c.key);
|
|
889
|
-
return true;
|
|
890
|
-
});
|
|
891
|
-
const seen2 = new Set();
|
|
892
|
-
this.sliceBy.allDynamicColumns = allCols.filter(c => {
|
|
893
|
-
if (c.sliceable === false || staticKeys.has(c.key) || seen2.has(c.key))
|
|
894
|
-
return false;
|
|
895
|
-
if (!c.header && c.sliceable !== true)
|
|
896
|
-
return false;
|
|
897
|
-
seen2.add(c.key);
|
|
898
|
-
return true;
|
|
899
|
-
});
|
|
900
|
-
this._computeSliceLabel();
|
|
901
|
-
}
|
|
902
|
-
/** Notifie la facade des slices statiques actuellement cachées (géré par table.component).
|
|
903
|
-
* Recalcule le label slice afin qu'il reflète uniquement les slices visibles. */
|
|
904
|
-
updateHiddenStaticKeys(keys) {
|
|
905
|
-
this._hiddenStaticKeys = keys;
|
|
906
|
-
this._computeSliceLabel();
|
|
907
|
-
}
|
|
908
|
-
_computeSliceLabel() {
|
|
909
|
-
const visibleStatics = (this._panelConfig.sliceConfigs ?? []).filter(s => !s.columnKey || !this._hiddenStaticKeys.has(s.columnKey));
|
|
910
|
-
const visibleStaticCount = visibleStatics.length;
|
|
911
|
-
const total = visibleStaticCount + this.sliceBy.activeDynamicColumns.length;
|
|
912
|
-
if (total === 0) {
|
|
913
|
-
this.sliceBy.activeLabel = this._panelConfig.noneLabel ?? 'Aucun';
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
if (total === 1) {
|
|
917
|
-
if (visibleStaticCount === 1) {
|
|
918
|
-
const first = visibleStatics[0];
|
|
919
|
-
this.sliceBy.activeLabel = first?.title || first?.columnKey || '1';
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
this.sliceBy.activeLabel = this.sliceBy.activeDynamicColumns[0]?.header || '1';
|
|
923
|
-
return;
|
|
924
|
-
}
|
|
925
|
-
this.sliceBy.activeLabel = `${total} actifs`;
|
|
926
|
-
}
|
|
927
|
-
_uniqueByKey(cols) {
|
|
928
|
-
const seen = new Set();
|
|
929
|
-
return cols.filter(c => {
|
|
930
|
-
if (seen.has(c.key))
|
|
931
|
-
return false;
|
|
932
|
-
seen.add(c.key);
|
|
933
|
-
return true;
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
}
|
|
937
|
-
|
|
938
|
-
/**
|
|
939
|
-
* Constantes sentinel utilisées pour les identifiants de colonnes et les valeurs
|
|
940
|
-
* projetées dans le datasource interne de la table.
|
|
941
|
-
*
|
|
942
|
-
* Centraliser ici évite les typos silencieuses et facilite les tests unitaires.
|
|
943
|
-
*/
|
|
944
|
-
/** Identifiant de la colonne virtuelle rendue sur toute la largeur pour les en-têtes de groupe. */
|
|
945
|
-
const GROUP_ROW_COLUMN = '__group_row';
|
|
946
|
-
/** Marqueur posé sur les objets de ligne pour signaler une ligne d'en-tête de groupe. */
|
|
947
|
-
const GROUP_HEADER_MARKER = '__groupHeader';
|
|
948
|
-
/** Clé du groupe spéciale « chargement lazy en cours ». */
|
|
949
|
-
const GROUP_KEY_LAZY_LOADING = '__lazy_loading__';
|
|
950
|
-
/** Clé du groupe spéciale « trop de groupes ». */
|
|
951
|
-
const GROUP_KEY_TOO_MANY = '__too_many_groups__';
|
|
952
|
-
/** Propriété injectée dans chaque ligne projetée : référence vers la ligne brute d'origine. */
|
|
953
|
-
const ROW_RAW_KEY = '__raw';
|
|
954
|
-
/** Propriété injectée dans chaque ligne projetée : index global de la ligne dans le jeu de données filtré. */
|
|
955
|
-
const ROW_INDEX_KEY = '__index';
|
|
956
|
-
/** Propriété injectée dans chaque ligne projetée : clé du groupe auquel appartient la ligne d'en-tête. */
|
|
957
|
-
const ROW_GROUP_KEY = '__groupKey';
|
|
958
|
-
/** Propriété injectée dans chaque ligne projetée : nombre de lignes dans le groupe. */
|
|
959
|
-
const ROW_GROUP_COUNT = '__groupCount';
|
|
960
|
-
/** Propriété injectée dans chaque ligne projetée : page courante du groupe. */
|
|
961
|
-
const ROW_GROUP_PAGE = '__groupPage';
|
|
962
|
-
/** Propriété injectée dans chaque ligne projetée : nombre de pages du groupe. */
|
|
963
|
-
const ROW_GROUP_PAGE_COUNT = '__groupPageCount';
|
|
964
|
-
/** Valeur sentinelle posée dans une cellule lazy en cours de chargement. */
|
|
965
|
-
const LAZY_LOADING_VALUE = '__lazy_loading__';
|
|
966
|
-
/** Valeur sentinelle posée dans une cellule lazy en erreur. */
|
|
967
|
-
const LAZY_ERROR_VALUE = '__lazy_error__';
|
|
968
|
-
|
|
969
|
-
/**
|
|
970
|
-
* Gère le cycle de vie des colonnes à chargement différé (lazy) :
|
|
971
|
-
* statuts, données, annulation des requêtes en cours.
|
|
972
|
-
*
|
|
973
|
-
* Pas de dépendance Angular/DOM — peut être instancié manuellement dans le composant.
|
|
974
|
-
*/
|
|
975
|
-
class LazyColumnManager {
|
|
976
|
-
constructor() {
|
|
977
|
-
this._status = new Map();
|
|
978
|
-
this._data = new Map();
|
|
979
|
-
this._cancels = new Map();
|
|
980
|
-
}
|
|
981
|
-
get status() {
|
|
982
|
-
return this._status;
|
|
983
|
-
}
|
|
984
|
-
get data() {
|
|
985
|
-
return this._data;
|
|
986
|
-
}
|
|
987
|
-
getStatus(key) {
|
|
988
|
-
return this._status.get(key) ?? 'idle';
|
|
989
|
-
}
|
|
990
|
-
getRenderType(rowValue) {
|
|
991
|
-
if (rowValue === LAZY_LOADING_VALUE)
|
|
992
|
-
return 'loading';
|
|
993
|
-
if (rowValue === LAZY_ERROR_VALUE)
|
|
994
|
-
return 'error';
|
|
995
|
-
return 'value';
|
|
996
|
-
}
|
|
997
|
-
/** Annule tous les fetches en cours et réinitialise l'état (appeler lors d'un changement de données). */
|
|
998
|
-
cancelAll() {
|
|
999
|
-
this._cancels.forEach(s => { s.next(); s.complete(); });
|
|
1000
|
-
this._cancels.clear();
|
|
1001
|
-
this._status = new Map();
|
|
1002
|
-
this._data = new Map();
|
|
1003
|
-
}
|
|
1004
|
-
/** Réinitialise une colonne et relance son fetch. */
|
|
1005
|
-
retry(column, resolvedData, callbacks) {
|
|
1006
|
-
const k = column.key;
|
|
1007
|
-
const prev = this._cancels.get(k);
|
|
1008
|
-
if (prev) {
|
|
1009
|
-
prev.next();
|
|
1010
|
-
prev.complete();
|
|
1011
|
-
}
|
|
1012
|
-
this._cancels.delete(k);
|
|
1013
|
-
const newStatus = new Map(this._status);
|
|
1014
|
-
newStatus.delete(k);
|
|
1015
|
-
this._status = newStatus;
|
|
1016
|
-
const newData = new Map(this._data);
|
|
1017
|
-
newData.delete(k);
|
|
1018
|
-
this._data = newData;
|
|
1019
|
-
this.fetch(column, resolvedData, callbacks);
|
|
1020
|
-
}
|
|
1021
|
-
/** Lance le chargement différé d'une colonne. No-op si déjà en cours. */
|
|
1022
|
-
fetch(column, resolvedData, callbacks) {
|
|
1023
|
-
if (!column.lazy)
|
|
1024
|
-
return;
|
|
1025
|
-
const k = column.key;
|
|
1026
|
-
if (this._status.get(k) === 'loading')
|
|
1027
|
-
return;
|
|
1028
|
-
const prev = this._cancels.get(k);
|
|
1029
|
-
if (prev) {
|
|
1030
|
-
prev.next();
|
|
1031
|
-
prev.complete();
|
|
1032
|
-
}
|
|
1033
|
-
const cancel$ = new Subject();
|
|
1034
|
-
this._cancels.set(k, cancel$);
|
|
1035
|
-
const newStatus = new Map(this._status);
|
|
1036
|
-
newStatus.set(k, 'loading');
|
|
1037
|
-
this._status = newStatus;
|
|
1038
|
-
callbacks.onRefresh();
|
|
1039
|
-
column.lazy.fetchFn().pipe(take(1), takeUntil(cancel$)).subscribe({
|
|
1040
|
-
next: (values) => {
|
|
1041
|
-
const rowMap = new Map();
|
|
1042
|
-
values.forEach((value, i) => {
|
|
1043
|
-
if (i < resolvedData.length)
|
|
1044
|
-
rowMap.set(resolvedData[i], value);
|
|
1045
|
-
});
|
|
1046
|
-
const newData = new Map(this._data);
|
|
1047
|
-
newData.set(k, rowMap);
|
|
1048
|
-
this._data = newData;
|
|
1049
|
-
const newStatus2 = new Map(this._status);
|
|
1050
|
-
newStatus2.set(k, 'loaded');
|
|
1051
|
-
this._status = newStatus2;
|
|
1052
|
-
callbacks.onRefresh();
|
|
1053
|
-
callbacks.onMarkForCheck();
|
|
1054
|
-
},
|
|
1055
|
-
error: () => {
|
|
1056
|
-
const newStatusErr = new Map(this._status);
|
|
1057
|
-
newStatusErr.set(k, 'error');
|
|
1058
|
-
this._status = newStatusErr;
|
|
1059
|
-
callbacks.onRefresh();
|
|
1060
|
-
callbacks.onMarkForCheck();
|
|
1061
|
-
},
|
|
1062
|
-
});
|
|
1063
|
-
}
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
171
|
const STORAGE_PREFIX = 'jqt_prefs_';
|
|
1067
172
|
/**
|
|
1068
173
|
* Gère la persistance de la configuration d'un tableau dans le localStorage.
|
|
@@ -1101,105 +206,6 @@ class TablePreferencesManager {
|
|
|
1101
206
|
}
|
|
1102
207
|
}
|
|
1103
208
|
|
|
1104
|
-
/**
|
|
1105
|
-
* GroupByManager
|
|
1106
|
-
*
|
|
1107
|
-
* Encapsule tout l'état et la logique liés au mode Group By :
|
|
1108
|
-
* - état collapsed / expanded des groupes
|
|
1109
|
-
* - pagination interne par groupe
|
|
1110
|
-
* - tri des groupes (asc / desc)
|
|
1111
|
-
* - scroll vers un groupe
|
|
1112
|
-
*
|
|
1113
|
-
* Le composant TableComponent l'instancie et lui passe deux callbacks :
|
|
1114
|
-
* - `onNeedProjection` : demande une reprojection légère de la page courante
|
|
1115
|
-
* - `onNeedRender` : demande un rebuild complet (tri changé etc.)
|
|
1116
|
-
*/
|
|
1117
|
-
class GroupByManager {
|
|
1118
|
-
constructor(onNeedProjection, onNeedRender) {
|
|
1119
|
-
this.onNeedProjection = onNeedProjection;
|
|
1120
|
-
this.onNeedRender = onNeedRender;
|
|
1121
|
-
/** Groupes actuellement ouverts (mode default-collapsed) ou fermés (mode default-expanded). */
|
|
1122
|
-
this._collapsedGroups = new Set();
|
|
1123
|
-
/** Page courante par clé de groupe. */
|
|
1124
|
-
this._groupPages = new Map();
|
|
1125
|
-
/** Taille de page pour la pagination interne des groupes. */
|
|
1126
|
-
this.groupPageSize = 5;
|
|
1127
|
-
/** Ordre de tri des groupes ('asc' | 'desc'). */
|
|
1128
|
-
this.groupSortOrder = 'asc';
|
|
1129
|
-
/**
|
|
1130
|
-
* Quand `true`, tous les groupes sont collapsés par défaut.
|
|
1131
|
-
* `_collapsedGroups` contient alors le seul groupe ouvert.
|
|
1132
|
-
*/
|
|
1133
|
-
this._defaultGroupCollapsed = false;
|
|
1134
|
-
}
|
|
1135
|
-
// ── API publique
|
|
1136
|
-
/** Initialise le mode par défaut selon qu'un groupBy vient d'être activé. */
|
|
1137
|
-
setDefaultCollapsed(collapsed) {
|
|
1138
|
-
this._defaultGroupCollapsed = collapsed;
|
|
1139
|
-
}
|
|
1140
|
-
/** Réinitialise l'état (à appeler quand le groupBy change de colonne). */
|
|
1141
|
-
reset() {
|
|
1142
|
-
this._collapsedGroups.clear();
|
|
1143
|
-
this._groupPages.clear();
|
|
1144
|
-
}
|
|
1145
|
-
isGroupCollapsed(groupKey) {
|
|
1146
|
-
return this._defaultGroupCollapsed
|
|
1147
|
-
? !this._collapsedGroups.has(groupKey)
|
|
1148
|
-
: this._collapsedGroups.has(groupKey);
|
|
1149
|
-
}
|
|
1150
|
-
toggleGroupCollapse(groupKey, scrollTo) {
|
|
1151
|
-
if (this._collapsedGroups.has(groupKey)) {
|
|
1152
|
-
this._collapsedGroups.delete(groupKey);
|
|
1153
|
-
}
|
|
1154
|
-
else {
|
|
1155
|
-
this._collapsedGroups.clear();
|
|
1156
|
-
this._collapsedGroups.add(groupKey);
|
|
1157
|
-
scrollTo(groupKey);
|
|
1158
|
-
}
|
|
1159
|
-
this.onNeedProjection();
|
|
1160
|
-
}
|
|
1161
|
-
/** Retourne le groupe actuellement ouvert (seulement en mode default-collapsed avec exactement 1 ouvert). */
|
|
1162
|
-
getSingleOpenGroup() {
|
|
1163
|
-
return this._defaultGroupCollapsed && this._collapsedGroups.size === 1
|
|
1164
|
-
? [...this._collapsedGroups][0]
|
|
1165
|
-
: null;
|
|
1166
|
-
}
|
|
1167
|
-
// ── Pagination par groupe
|
|
1168
|
-
getPage(groupKey) {
|
|
1169
|
-
return this._groupPages.get(groupKey) ?? 0;
|
|
1170
|
-
}
|
|
1171
|
-
groupPageBack(groupKey) {
|
|
1172
|
-
const current = this._groupPages.get(groupKey) ?? 0;
|
|
1173
|
-
if (current > 0) {
|
|
1174
|
-
this._groupPages.set(groupKey, current - 1);
|
|
1175
|
-
this.onNeedProjection();
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
groupPageForward(groupKey, totalCount) {
|
|
1179
|
-
const maxPage = Math.ceil(totalCount / this.groupPageSize) - 1;
|
|
1180
|
-
const current = this._groupPages.get(groupKey) ?? 0;
|
|
1181
|
-
if (current < maxPage) {
|
|
1182
|
-
this._groupPages.set(groupKey, current + 1);
|
|
1183
|
-
this.onNeedProjection();
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
onGroupPageSizeChange(size) {
|
|
1187
|
-
this.groupPageSize = size;
|
|
1188
|
-
this._groupPages.clear();
|
|
1189
|
-
this.onNeedRender();
|
|
1190
|
-
}
|
|
1191
|
-
onGroupSortToggle(dir, scrollTo) {
|
|
1192
|
-
if (this.groupSortOrder === dir)
|
|
1193
|
-
return;
|
|
1194
|
-
this.groupSortOrder = dir;
|
|
1195
|
-
this._groupPages.clear();
|
|
1196
|
-
this.onNeedRender();
|
|
1197
|
-
const openGroup = this.getSingleOpenGroup();
|
|
1198
|
-
if (openGroup)
|
|
1199
|
-
scrollTo(openGroup);
|
|
1200
|
-
}
|
|
1201
|
-
}
|
|
1202
|
-
|
|
1203
209
|
/**
|
|
1204
210
|
* ExportManager
|
|
1205
211
|
*
|
|
@@ -1245,6 +251,83 @@ class ExportManager {
|
|
|
1245
251
|
}
|
|
1246
252
|
}
|
|
1247
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
|
+
|
|
1248
331
|
class TableComponent {
|
|
1249
332
|
constructor() {
|
|
1250
333
|
this.isLoading = false;
|
|
@@ -1277,12 +360,12 @@ class TableComponent {
|
|
|
1277
360
|
this._ROW_GROUP_PAGE = ROW_GROUP_PAGE;
|
|
1278
361
|
this._ROW_GROUP_PAGE_COUNT = ROW_GROUP_PAGE_COUNT;
|
|
1279
362
|
/** Facade View : état et actions centralisés (Champs / Group by / Slice by) */
|
|
1280
|
-
this.
|
|
363
|
+
this._organizer = new OrganizerFacade();
|
|
1281
364
|
/**
|
|
1282
|
-
* Snapshot stable synchronisé depuis
|
|
365
|
+
* Snapshot stable synchronisé depuis _organizer.fields.activeFields dans refreshViewModel().
|
|
1283
366
|
* Propriété (non getter) pour qu'Angular puisse la suivre sans NG0100.
|
|
1284
367
|
*/
|
|
1285
|
-
this.
|
|
368
|
+
this.activeFields = [];
|
|
1286
369
|
this.activeSliceFilter = () => true;
|
|
1287
370
|
// Propriétés stables pour les bindings du template (contre erreur NG0100)
|
|
1288
371
|
this._sliceConfigs = [];
|
|
@@ -1304,7 +387,7 @@ class TableComponent {
|
|
|
1304
387
|
/** Gestionnaire du mode Group By (état collapsed, pagination, tri des groupes). */
|
|
1305
388
|
this._groupBy = new GroupByManager(() => { this._projectCurrentPage(); this._cdr.markForCheck(); }, () => this.refreshViewModel());
|
|
1306
389
|
/** Gestionnaire du cycle de vie des colonnes lazy (fetch, annulation, statuts, données). */
|
|
1307
|
-
this._lazy = new
|
|
390
|
+
this._lazy = new LazyFieldManager();
|
|
1308
391
|
this._cdr = inject(ChangeDetectorRef);
|
|
1309
392
|
this._el = inject((ElementRef));
|
|
1310
393
|
this._ngZone = inject(NgZone);
|
|
@@ -1313,7 +396,7 @@ class TableComponent {
|
|
|
1313
396
|
this.i18n = { ...JQT_I18N_DEFAULTS, ...(this._i18nRaw ?? {}) };
|
|
1314
397
|
this.isGroupHeader = (_, row) => row[GROUP_HEADER_MARKER] === true;
|
|
1315
398
|
/** Gestionnaire export CSV. */
|
|
1316
|
-
this._export = new ExportManager(() => this._allFilteredRows, () => this.
|
|
399
|
+
this._export = new ExportManager(() => this._allFilteredRows, () => this.activeFields, () => this.resolvedConfig.export, (col, row, idx) => this.resolveCellValue(col, row, idx));
|
|
1317
400
|
// ── Lifecycle
|
|
1318
401
|
this.pageSize = 5;
|
|
1319
402
|
this.pageSizeOptions = [5, 10, 20];
|
|
@@ -1351,6 +434,7 @@ class TableComponent {
|
|
|
1351
434
|
this._configHiddenKeys = new Set();
|
|
1352
435
|
/** Clés des colonnes visibles du dernier émission columnsChange (pour éviter les émissions dupliquement). */
|
|
1353
436
|
this._prevColumnKeys = [];
|
|
437
|
+
this._organizerConfigCache = null;
|
|
1354
438
|
this.trackRowFn = (_index, row) => row[GROUP_HEADER_MARKER] ? `__group_${row[ROW_GROUP_KEY]}` : row[ROW_INDEX_KEY] ?? _index;
|
|
1355
439
|
this._preferencesMessageTimer = null;
|
|
1356
440
|
}
|
|
@@ -1370,12 +454,12 @@ class TableComponent {
|
|
|
1370
454
|
}
|
|
1371
455
|
get paginator() { return this._paginator; }
|
|
1372
456
|
// Délégués vers _view (conservés pour compatibilité template sans refactoring HTML)
|
|
1373
|
-
get _staticSlicesForMenu() { return this.
|
|
1374
|
-
get _activeDynamicSliceColumns() { return this.
|
|
1375
|
-
get _availableColumnsForDynamicSlice() { return this.
|
|
1376
|
-
get _allDynamicSliceColumns() { return this.
|
|
1377
|
-
get _activeSliceByLabel() { return this.
|
|
1378
|
-
get activeGroupByKey() { return this.
|
|
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; }
|
|
1379
463
|
/** Délégués vers GroupByManager pour la compatibilité des templates. */
|
|
1380
464
|
get groupPageSize() { return this._groupBy.groupPageSize; }
|
|
1381
465
|
set groupPageSize(v) { this._groupBy.groupPageSize = v; }
|
|
@@ -1408,9 +492,10 @@ class TableComponent {
|
|
|
1408
492
|
if (changes['config']) {
|
|
1409
493
|
this.applyConfigSearchReset();
|
|
1410
494
|
this.applyInitialSearchQuery();
|
|
495
|
+
this._invalidateOrganizerConfig(); // les colonnes ont pu changer
|
|
1411
496
|
}
|
|
1412
497
|
if (dataChanged) {
|
|
1413
|
-
this.
|
|
498
|
+
this.activeFields.filter(c => c.lazy).forEach(c => this.triggerLazyFetch(c));
|
|
1414
499
|
}
|
|
1415
500
|
}
|
|
1416
501
|
ngAfterContentInit() {
|
|
@@ -1436,25 +521,24 @@ class TableComponent {
|
|
|
1436
521
|
const filters = this._pendingSliceFilters ? { ...this._pendingSliceFilters } : null;
|
|
1437
522
|
this._pendingDynamicSliceKeys = null;
|
|
1438
523
|
this._pendingSliceFilters = null;
|
|
1439
|
-
// Tout est différé dans le même setTimeout pour :
|
|
1440
|
-
// 1. Éviter NG0100 (modifications après vérification dans ngAfterViewInit)
|
|
1441
|
-
// 2. Garantir que les dynamic slices sont ajoutées AVANT la restauration des filtres
|
|
1442
|
-
// (les indices dans sliceFilters dépendent de l'ordre final de _cachedSlices)
|
|
1443
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 });
|
|
1444
534
|
keys.forEach(key => {
|
|
1445
535
|
const col = allCols.find(c => c.key === key);
|
|
1446
536
|
if (col)
|
|
1447
537
|
this.addDynamicSlice(col);
|
|
1448
538
|
});
|
|
1449
|
-
if (filters) {
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
}
|
|
1453
|
-
else {
|
|
1454
|
-
// slicePanelRef toujours null (données pas encore arrivées) :
|
|
1455
|
-
// remettre en attente pour que _recomputeShowSlicePanel les applique
|
|
1456
|
-
this._pendingSliceFilters = filters;
|
|
1457
|
-
}
|
|
539
|
+
if (filters && this.slicePanelRef) {
|
|
540
|
+
console.log('[SliceRestore] restoreFilters depuis ngAfterViewInit', filters);
|
|
541
|
+
this.slicePanelRef.restoreFilters(filters);
|
|
1458
542
|
}
|
|
1459
543
|
this._cdr.markForCheck();
|
|
1460
544
|
});
|
|
@@ -1470,7 +554,7 @@ class TableComponent {
|
|
|
1470
554
|
this._resizeState = null;
|
|
1471
555
|
this._destroy$.next();
|
|
1472
556
|
this._destroy$.complete();
|
|
1473
|
-
this.
|
|
557
|
+
this._organizer.destroy();
|
|
1474
558
|
}
|
|
1475
559
|
_measureHeaderHeight() {
|
|
1476
560
|
const measure = () => {
|
|
@@ -1487,7 +571,73 @@ class TableComponent {
|
|
|
1487
571
|
};
|
|
1488
572
|
this._ngZone.runOutsideAngular(() => requestAnimationFrame(() => measure()));
|
|
1489
573
|
}
|
|
1490
|
-
|
|
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
|
+
}
|
|
1491
641
|
get title() {
|
|
1492
642
|
return this.resolvedConfig.title ?? '';
|
|
1493
643
|
}
|
|
@@ -1495,10 +645,11 @@ class TableComponent {
|
|
|
1495
645
|
return !!this.resolvedConfig.title;
|
|
1496
646
|
}
|
|
1497
647
|
get visibleColumns() {
|
|
1498
|
-
return this.
|
|
648
|
+
return this.activeFields;
|
|
1499
649
|
}
|
|
1500
650
|
get showSearchBar() { return this.resolvedConfig.search?.enabled === true; }
|
|
1501
|
-
get showViewButton() { return this.
|
|
651
|
+
get showViewButton() { return this._organizer.enabled || this.showExportButton || this.showPreferencesMenu; }
|
|
652
|
+
get showOrganizerButton() { return this._organizer.enabled || this.showExportButton || this.showPreferencesMenu; }
|
|
1502
653
|
get showSliceExpandBtn() { return this._showSlicePanel && this._slicePanelCollapsed && this._sliceShowToggle; }
|
|
1503
654
|
get showExportButton() {
|
|
1504
655
|
return this.resolvedConfig.export?.enabled === true;
|
|
@@ -1516,21 +667,20 @@ class TableComponent {
|
|
|
1516
667
|
expandSlicePanel() {
|
|
1517
668
|
this.slicePanelRef?.togglePanel();
|
|
1518
669
|
}
|
|
1519
|
-
get showFields() { return this.
|
|
1520
|
-
get showGroupBySection() { return this.
|
|
670
|
+
get showFields() { return this._organizer.showFields; }
|
|
671
|
+
get showGroupBySection() { return this._organizer.showGroupBySection; }
|
|
1521
672
|
get filteredRowCount() {
|
|
1522
673
|
return this.activeGroupByKey ? this._totalFilteredCount : this._allFilteredRows.length;
|
|
1523
674
|
}
|
|
1524
|
-
get activeGroupByLabel() { return this.
|
|
1525
|
-
get totalColumnCount() { return this.
|
|
1526
|
-
get activeSliceByLabel() { return this.
|
|
1527
|
-
get activeDynamicSliceColumns() { return this.
|
|
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; }
|
|
1528
679
|
removeDynamicSliceByKey(key) {
|
|
1529
680
|
this.slicePanelRef?.removeDynamicSliceByKey(key);
|
|
1530
681
|
}
|
|
1531
|
-
get groupByColumns() { return this.
|
|
1532
|
-
colLabel(col) { return this.
|
|
1533
|
-
// ── Actions utilisateur
|
|
682
|
+
get groupByColumns() { return this._organizer.groupByFields; }
|
|
683
|
+
colLabel(col) { return this._organizer.fieldLabel(col); }
|
|
1534
684
|
onSearchChange() {
|
|
1535
685
|
this._preservePageIndex = 0;
|
|
1536
686
|
this.refreshViewModel();
|
|
@@ -1538,7 +688,7 @@ class TableComponent {
|
|
|
1538
688
|
this.searchChange.emit(this.searchQuery);
|
|
1539
689
|
}
|
|
1540
690
|
onGroupByChange(key) {
|
|
1541
|
-
this.
|
|
691
|
+
this._organizer.setGroupBy(key);
|
|
1542
692
|
this._groupBy.setDefaultCollapsed(key !== null);
|
|
1543
693
|
this._groupBy.reset();
|
|
1544
694
|
this._groupBy.groupPageSize = this.resolveDefaultGroupPageSize();
|
|
@@ -1546,7 +696,6 @@ class TableComponent {
|
|
|
1546
696
|
this.paginator?.firstPage();
|
|
1547
697
|
this.groupByChange.emit(key);
|
|
1548
698
|
if (key) {
|
|
1549
|
-
this.viewMenuTrigger?.closeMenu();
|
|
1550
699
|
const colDef = (this.resolvedConfig.columns || []).find(c => c.key === key);
|
|
1551
700
|
if (colDef?.lazy && this._lazyColumnStatus.get(key) !== 'loaded') {
|
|
1552
701
|
this.triggerLazyFetch(colDef);
|
|
@@ -1566,7 +715,6 @@ class TableComponent {
|
|
|
1566
715
|
this._ngZone.runOutsideAngular(() => setTimeout(() => this.scrollToGroupHeader(key), 0));
|
|
1567
716
|
});
|
|
1568
717
|
}
|
|
1569
|
-
// ── Lazy loading
|
|
1570
718
|
getLazyColumnStatus(key) {
|
|
1571
719
|
return this._lazy.getStatus(key);
|
|
1572
720
|
}
|
|
@@ -1585,7 +733,6 @@ class TableComponent {
|
|
|
1585
733
|
onMarkForCheck: () => this._cdr.markForCheck(),
|
|
1586
734
|
};
|
|
1587
735
|
}
|
|
1588
|
-
// ── Groupement
|
|
1589
736
|
toggleGroupCollapse(groupKey) {
|
|
1590
737
|
this._groupBy.toggleGroupCollapse(groupKey, (key) => {
|
|
1591
738
|
this._ngZone.runOutsideAngular(() => setTimeout(() => this.scrollToGroupHeader(key), 0));
|
|
@@ -1614,24 +761,22 @@ class TableComponent {
|
|
|
1614
761
|
groupPageForward(groupKey, totalCount) {
|
|
1615
762
|
this._groupBy.groupPageForward(groupKey, totalCount);
|
|
1616
763
|
}
|
|
1617
|
-
// ── Colonnes
|
|
1618
764
|
get hasAddableColumns() {
|
|
1619
|
-
return this.
|
|
765
|
+
return this._organizer.menuBaseFields.length > 0 || this._organizer.menuOptionalFields.length > 0;
|
|
1620
766
|
}
|
|
1621
767
|
get menuBaseColumns() {
|
|
1622
|
-
return this.
|
|
768
|
+
return this._organizer.menuBaseFields;
|
|
1623
769
|
}
|
|
1624
770
|
get menuOptionalColumns() {
|
|
1625
|
-
return this.
|
|
771
|
+
return this._organizer.menuOptionalFields;
|
|
1626
772
|
}
|
|
1627
773
|
get availableColumnsToAdd() {
|
|
1628
|
-
const activeKeys = new Set(this.
|
|
774
|
+
const activeKeys = new Set(this.activeFields.map((column) => column.key));
|
|
1629
775
|
return (this.resolvedConfig.columns || []).filter((c) => c.optional && !activeKeys.has(c.key));
|
|
1630
776
|
}
|
|
1631
777
|
get allowColumnRemoval() {
|
|
1632
|
-
return this.
|
|
778
|
+
return this._organizer.allowFieldRemoval;
|
|
1633
779
|
}
|
|
1634
|
-
// ── Slices
|
|
1635
780
|
get hasSliceConfig() {
|
|
1636
781
|
return (this.resolvedConfig.slices?.length ?? 0) > 0;
|
|
1637
782
|
}
|
|
@@ -1643,28 +788,43 @@ class TableComponent {
|
|
|
1643
788
|
this._cdr.markForCheck();
|
|
1644
789
|
}
|
|
1645
790
|
_recomputeShowSlicePanel() {
|
|
1646
|
-
const hasCfg = this._sliceConfigs.length > 0 || this.
|
|
791
|
+
const hasCfg = this._sliceConfigs.length > 0 || this._organizer.sliceBy.activeDynamicFields.length > 0;
|
|
1647
792
|
const wasVisible = this._showSlicePanel;
|
|
1648
793
|
this._showSlicePanel = hasCfg && this._resolvedData.length > 0;
|
|
1649
|
-
//
|
|
1650
|
-
//
|
|
1651
|
-
|
|
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)) {
|
|
1652
798
|
const filters = this._pendingSliceFilters;
|
|
799
|
+
const dynamicKeys = this._pendingDynamicSliceKeys;
|
|
1653
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 });
|
|
1654
804
|
setTimeout(() => {
|
|
1655
|
-
|
|
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
|
+
}
|
|
1656
816
|
this._cdr.markForCheck();
|
|
1657
817
|
});
|
|
1658
818
|
}
|
|
1659
819
|
}
|
|
1660
820
|
get showAddSliceButton() {
|
|
1661
|
-
return this.
|
|
821
|
+
return this._organizer.showSliceBySection;
|
|
1662
822
|
}
|
|
1663
823
|
get staticSlicesForMenu() {
|
|
1664
|
-
return this.
|
|
824
|
+
return this._organizer.staticSlicesForMenu;
|
|
1665
825
|
}
|
|
1666
826
|
get availableColumnsForDynamicSlice() {
|
|
1667
|
-
return this.
|
|
827
|
+
return this._organizer.sliceBy.availableForDynamic;
|
|
1668
828
|
}
|
|
1669
829
|
isStaticSliceVisible(columnKey) {
|
|
1670
830
|
return !this._staticSliceHiddenKeys.has(columnKey);
|
|
@@ -1676,7 +836,7 @@ class TableComponent {
|
|
|
1676
836
|
else
|
|
1677
837
|
next.add(columnKey);
|
|
1678
838
|
this._staticSliceHiddenKeys = next;
|
|
1679
|
-
this.
|
|
839
|
+
this._organizer.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
|
|
1680
840
|
this._sliceConfigs = this._computeSliceConfigs();
|
|
1681
841
|
// Si une slice vient d'être cachée, réinitialise le filtre actif pour éviter
|
|
1682
842
|
// un filtre orphelin (la slice n'est plus visible mais son filtre resterait actif)
|
|
@@ -1697,7 +857,7 @@ class TableComponent {
|
|
|
1697
857
|
return this._allDynamicSliceColumns;
|
|
1698
858
|
}
|
|
1699
859
|
isDynamicSliceActive(key) {
|
|
1700
|
-
return this.
|
|
860
|
+
return this._organizer.isDynamicSliceActive(key);
|
|
1701
861
|
}
|
|
1702
862
|
toggleDynamicSlice(col) {
|
|
1703
863
|
if (this.isDynamicSliceActive(col.key)) {
|
|
@@ -1714,7 +874,7 @@ class TableComponent {
|
|
|
1714
874
|
this.paginator?.firstPage();
|
|
1715
875
|
}
|
|
1716
876
|
onDynamicSliceKeysChange(keys) {
|
|
1717
|
-
this.
|
|
877
|
+
this._organizer.onDynamicSliceKeysChange(keys);
|
|
1718
878
|
this._recomputeShowSlicePanel();
|
|
1719
879
|
this._cdr.markForCheck();
|
|
1720
880
|
}
|
|
@@ -1740,7 +900,7 @@ class TableComponent {
|
|
|
1740
900
|
return this.resolvedConfig.pagination?.showFirstLastButtons === true;
|
|
1741
901
|
}
|
|
1742
902
|
get isColumnDragDropEnabled() {
|
|
1743
|
-
return this.
|
|
903
|
+
return this._organizer.isFieldDragDropEnabled;
|
|
1744
904
|
}
|
|
1745
905
|
get totalRows() {
|
|
1746
906
|
return this.activeGroupByKey
|
|
@@ -1749,7 +909,7 @@ class TableComponent {
|
|
|
1749
909
|
}
|
|
1750
910
|
// ── Gestion des colonnes
|
|
1751
911
|
onAddColumn(column) {
|
|
1752
|
-
this.
|
|
912
|
+
this._organizer.addField(column);
|
|
1753
913
|
this._preservePageIndex = this.paginator?.pageIndex ?? null;
|
|
1754
914
|
this.refreshViewModel();
|
|
1755
915
|
if (column.lazy) {
|
|
@@ -1759,13 +919,13 @@ class TableComponent {
|
|
|
1759
919
|
this.columnAdded.emit(column);
|
|
1760
920
|
}
|
|
1761
921
|
onColumnVisibilityChange(column, checked) {
|
|
1762
|
-
const isVisible = this.
|
|
922
|
+
const isVisible = this._organizer.isFieldVisible(column.key);
|
|
1763
923
|
if (checked && !isVisible) {
|
|
1764
924
|
this.onAddColumn(column);
|
|
1765
925
|
return;
|
|
1766
926
|
}
|
|
1767
927
|
if (!checked && isVisible) {
|
|
1768
|
-
const removed = this.
|
|
928
|
+
const removed = this._organizer.removeField(column.key);
|
|
1769
929
|
if (!removed)
|
|
1770
930
|
return;
|
|
1771
931
|
this._preservePageIndex = this.paginator?.pageIndex ?? null;
|
|
@@ -1774,14 +934,14 @@ class TableComponent {
|
|
|
1774
934
|
}
|
|
1775
935
|
}
|
|
1776
936
|
isColumnVisible(columnKey) {
|
|
1777
|
-
return this.
|
|
937
|
+
return this.activeFields.some(c => c.key === columnKey);
|
|
1778
938
|
}
|
|
1779
939
|
onRemoveColumn(columnKey, event) {
|
|
1780
940
|
event?.stopPropagation();
|
|
1781
|
-
const column = this.
|
|
941
|
+
const column = this.activeFields.find(item => item.key === columnKey);
|
|
1782
942
|
if (!column)
|
|
1783
943
|
return;
|
|
1784
|
-
const removed = this.
|
|
944
|
+
const removed = this._organizer.removeField(columnKey);
|
|
1785
945
|
if (!removed)
|
|
1786
946
|
return;
|
|
1787
947
|
this.refreshViewModel();
|
|
@@ -1805,11 +965,11 @@ class TableComponent {
|
|
|
1805
965
|
* Seules les valeurs explicitement en `px` sont sommables ; les pourcentages ou autres unités sont ignorés. */
|
|
1806
966
|
get tableMinWidth() {
|
|
1807
967
|
const hasResized = Object.keys(this._columnWidths).length > 0;
|
|
1808
|
-
const hasSomeExplicitWidth = hasResized || this.
|
|
968
|
+
const hasSomeExplicitWidth = hasResized || this.activeFields.some(c => c.width);
|
|
1809
969
|
if (!hasSomeExplicitWidth)
|
|
1810
970
|
return null;
|
|
1811
971
|
let sum = 0;
|
|
1812
|
-
for (const col of this.
|
|
972
|
+
for (const col of this.activeFields) {
|
|
1813
973
|
if (this._columnWidths[col.key] != null) {
|
|
1814
974
|
sum += this._columnWidths[col.key];
|
|
1815
975
|
}
|
|
@@ -1868,7 +1028,7 @@ class TableComponent {
|
|
|
1868
1028
|
return;
|
|
1869
1029
|
}
|
|
1870
1030
|
event.preventDefault();
|
|
1871
|
-
const moved = this.
|
|
1031
|
+
const moved = this._organizer.reorderFields(this.draggedColumnKey, targetColumnKey);
|
|
1872
1032
|
if (!moved) {
|
|
1873
1033
|
this.onHeaderDragEnd();
|
|
1874
1034
|
return;
|
|
@@ -1950,12 +1110,12 @@ class TableComponent {
|
|
|
1950
1110
|
savePreferences() {
|
|
1951
1111
|
if (!this._preferencesManager)
|
|
1952
1112
|
return;
|
|
1953
|
-
const dynamicSliceKeys = this.
|
|
1113
|
+
const dynamicSliceKeys = this._organizer.sliceBy.activeDynamicFields.map(c => c.key);
|
|
1954
1114
|
const config = {
|
|
1955
1115
|
search: this.searchQuery || undefined,
|
|
1956
1116
|
groupBy: this.activeGroupByKey,
|
|
1957
|
-
columnOrder: this.
|
|
1958
|
-
visibleColumns: this.
|
|
1117
|
+
columnOrder: this.activeFields.map(c => c.key),
|
|
1118
|
+
visibleColumns: this.activeFields.map(c => c.key),
|
|
1959
1119
|
columnWidths: Object.keys(this._columnWidths).length ? { ...this._columnWidths } : undefined,
|
|
1960
1120
|
slicePanelCollapsed: this._slicePanelCollapsed,
|
|
1961
1121
|
hiddenSliceKeys: [...this._staticSliceHiddenKeys],
|
|
@@ -1972,7 +1132,7 @@ class TableComponent {
|
|
|
1972
1132
|
this._preferencesManager.clear();
|
|
1973
1133
|
this._hasSavedConfig = false;
|
|
1974
1134
|
// Capturer les dynamic slices actives AVANT le reset (resetToDefaults() les vide)
|
|
1975
|
-
const dynamicSlicesToRemove = this.
|
|
1135
|
+
const dynamicSlicesToRemove = this._organizer.sliceBy.activeDynamicFields.slice();
|
|
1976
1136
|
// Reset complet de l'état vers les valeurs par défaut de la config
|
|
1977
1137
|
this.searchQuery = '';
|
|
1978
1138
|
this._columnWidths = {};
|
|
@@ -1987,9 +1147,9 @@ class TableComponent {
|
|
|
1987
1147
|
this._groupBy.setDefaultCollapsed(false);
|
|
1988
1148
|
// Slices statiques : revenir aux seules celles cachées par la config (pas par l'utilisateur)
|
|
1989
1149
|
this._staticSliceHiddenKeys = new Set(this._configHiddenKeys);
|
|
1990
|
-
this.
|
|
1150
|
+
this._organizer.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
|
|
1991
1151
|
// Colonnes et groupBy : reset via la facade (retire les optionnelles, remet l'ordre config)
|
|
1992
|
-
this.
|
|
1152
|
+
this._organizer.resetToDefaults();
|
|
1993
1153
|
this._defaultGroupByApplied = false;
|
|
1994
1154
|
// Dynamic slices : les retirer du slice panel avec la liste capturée avant le reset
|
|
1995
1155
|
dynamicSlicesToRemove.forEach(col => {
|
|
@@ -2000,8 +1160,8 @@ class TableComponent {
|
|
|
2000
1160
|
if (this.slicePanelRef)
|
|
2001
1161
|
this.slicePanelRef.restoreFilters({});
|
|
2002
1162
|
// Rafraîchit toute la vue
|
|
2003
|
-
this.
|
|
2004
|
-
this.renderedColumns = this.
|
|
1163
|
+
this.activeFields = this._organizer.fields.activeFields;
|
|
1164
|
+
this.renderedColumns = this.activeFields.map(c => c.key);
|
|
2005
1165
|
this._pendingDynamicSliceKeys = null;
|
|
2006
1166
|
this._pendingSliceFilters = null;
|
|
2007
1167
|
this.refreshViewModel();
|
|
@@ -2029,7 +1189,7 @@ class TableComponent {
|
|
|
2029
1189
|
this._columnWidths = { ...saved.columnWidths };
|
|
2030
1190
|
// Group by
|
|
2031
1191
|
if (saved.groupBy !== undefined) {
|
|
2032
|
-
this.
|
|
1192
|
+
this._organizer.setGroupBy(saved.groupBy);
|
|
2033
1193
|
if (saved.groupBy) {
|
|
2034
1194
|
this._groupBy.setDefaultCollapsed(true);
|
|
2035
1195
|
this._groupBy.reset();
|
|
@@ -2041,9 +1201,9 @@ class TableComponent {
|
|
|
2041
1201
|
// Slices statiques cachées — on utilise la valeur exacte sauvegardée (pas les defaults de config)
|
|
2042
1202
|
if (saved.hiddenSliceKeys !== undefined) {
|
|
2043
1203
|
this._staticSliceHiddenKeys = new Set(saved.hiddenSliceKeys);
|
|
2044
|
-
this.
|
|
1204
|
+
this._organizer.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
|
|
2045
1205
|
}
|
|
2046
|
-
// Colonnes visibles + ordre — reconstruit
|
|
1206
|
+
// Colonnes visibles + ordre — reconstruit activeFields en une seule passe.
|
|
2047
1207
|
// On part de saved.columnOrder (ordre sauvegardé) si disponible, sinon saved.visibleColumns.
|
|
2048
1208
|
// Cela gère à la fois : colonnes optionnelles ajoutées, colonnes retirées, et réordonnancement.
|
|
2049
1209
|
const targetKeys = saved.columnOrder ?? saved.visibleColumns;
|
|
@@ -2056,7 +1216,7 @@ class TableComponent {
|
|
|
2056
1216
|
// Colonnes actives actuelllement non présentes dans targetKeys (non-optional passées sous silence)
|
|
2057
1217
|
// → on les exclut : l'utilisateur avait explicitement choisi ce sous-ensemble
|
|
2058
1218
|
if (ordered.length > 0) {
|
|
2059
|
-
this.
|
|
1219
|
+
this._organizer.setActiveFields(ordered);
|
|
2060
1220
|
}
|
|
2061
1221
|
}
|
|
2062
1222
|
// Dynamic slices — différé : slicePanelRef pas encore disponible au moment de ngOnChanges
|
|
@@ -2072,9 +1232,9 @@ class TableComponent {
|
|
|
2072
1232
|
}
|
|
2073
1233
|
}
|
|
2074
1234
|
canRemoveColumn(column) {
|
|
2075
|
-
return (this.
|
|
1235
|
+
return (this._organizer.allowFieldRemoval &&
|
|
2076
1236
|
column.removable !== false &&
|
|
2077
|
-
this.
|
|
1237
|
+
this.activeFields.length > 1);
|
|
2078
1238
|
}
|
|
2079
1239
|
isCellEmpty(value) {
|
|
2080
1240
|
return value === '' || value == null;
|
|
@@ -2099,7 +1259,7 @@ class TableComponent {
|
|
|
2099
1259
|
? this.resolvedConfig.preferences.tableId
|
|
2100
1260
|
: null;
|
|
2101
1261
|
const currentId = prefsId ?? null;
|
|
2102
|
-
// Préférences à appliquer après
|
|
1262
|
+
// Préférences à appliquer après _organizer.update() (quand activeFields est déjà peuplé)
|
|
2103
1263
|
let pendingPrefs = null;
|
|
2104
1264
|
if (currentId && this._preferencesManager?.key !== ('jqt_prefs_' + currentId)) {
|
|
2105
1265
|
this._preferencesManager = new TablePreferencesManager(currentId);
|
|
@@ -2111,21 +1271,21 @@ class TableComponent {
|
|
|
2111
1271
|
this._hasSavedConfig = false;
|
|
2112
1272
|
}
|
|
2113
1273
|
// Délègue à la facade : mise à jour de l'état View (colonnes, groupBy meta, sliceBy meta)
|
|
2114
|
-
this.
|
|
1274
|
+
this._organizer.update({
|
|
2115
1275
|
config: this.resolvedConfig.view,
|
|
2116
|
-
|
|
1276
|
+
fields: this.resolvedConfig.columns ?? [],
|
|
2117
1277
|
sliceConfigs: this.resolvedConfig.slices ?? [],
|
|
2118
1278
|
defaultGroupBy: this.resolvedConfig.defaultGroupBy,
|
|
2119
1279
|
});
|
|
2120
|
-
if (!pendingPrefs && !this._defaultGroupByApplied && !this.
|
|
1280
|
+
if (!pendingPrefs && !this._defaultGroupByApplied && !this._organizer.groupBy.userCustomized && this._organizer.groupBy.activeKey != null) {
|
|
2121
1281
|
this._defaultGroupByApplied = true;
|
|
2122
1282
|
this._groupBy.setDefaultCollapsed(true);
|
|
2123
1283
|
this._groupBy.reset();
|
|
2124
1284
|
this._groupBy.groupPageSize = this.resolveDefaultGroupPageSize();
|
|
2125
1285
|
}
|
|
2126
1286
|
// Synchronise la propriété stable (évite NG0100 — la facade est source de vérité)
|
|
2127
|
-
this.
|
|
2128
|
-
this.renderedColumns = this.
|
|
1287
|
+
this.activeFields = this._organizer.fields.activeFields;
|
|
1288
|
+
this.renderedColumns = this.activeFields.map(c => c.key);
|
|
2129
1289
|
this.pageSize = this.paginator?.pageSize || this.resolvePageSize();
|
|
2130
1290
|
this.pageSizeOptions = this.resolvePageSizeOptions();
|
|
2131
1291
|
this.pageSizeOptionsGroupBy = this.resolvePageSizeOptionsGroupBy();
|
|
@@ -2143,10 +1303,10 @@ class TableComponent {
|
|
|
2143
1303
|
// (pendingPrefs les gèrera lui-même via _applyPreferences → hiddenSliceKeys)
|
|
2144
1304
|
if (!pendingPrefs) {
|
|
2145
1305
|
this._staticSliceHiddenKeys = new Set(newConfigHidden);
|
|
2146
|
-
this.
|
|
1306
|
+
this._organizer.updateHiddenStaticKeys(this._staticSliceHiddenKeys);
|
|
2147
1307
|
}
|
|
2148
1308
|
}
|
|
2149
|
-
// Applique les préférences ICI, APRÈS
|
|
1309
|
+
// Applique les préférences ICI, APRÈS _organizer.update() ET après _configHiddenKeys mis à jour,
|
|
2150
1310
|
// mais avant _computeSliceConfigs() pour que hiddenSliceKeys soit déjà restauré.
|
|
2151
1311
|
if (pendingPrefs) {
|
|
2152
1312
|
this._applyPreferences(pendingPrefs);
|
|
@@ -2204,6 +1364,7 @@ class TableComponent {
|
|
|
2204
1364
|
}
|
|
2205
1365
|
}
|
|
2206
1366
|
this._sliceByMenuItems = items;
|
|
1367
|
+
this._invalidateOrganizerConfig(); // les slices ont changé
|
|
2207
1368
|
}
|
|
2208
1369
|
get sliceByMenuItems() { return this._sliceByMenuItems; }
|
|
2209
1370
|
onSliceMenuItemClick(item) {
|
|
@@ -2264,7 +1425,7 @@ class TableComponent {
|
|
|
2264
1425
|
pageRows.forEach((row, i) => {
|
|
2265
1426
|
const idx = start + i;
|
|
2266
1427
|
const p = { [ROW_RAW_KEY]: row, [ROW_INDEX_KEY]: idx };
|
|
2267
|
-
this.
|
|
1428
|
+
this.activeFields.forEach((col) => {
|
|
2268
1429
|
p[col.key] = this.resolveCellValue(col, row, idx);
|
|
2269
1430
|
});
|
|
2270
1431
|
projected.push(p);
|
|
@@ -2377,7 +1538,7 @@ class TableComponent {
|
|
|
2377
1538
|
const result = [];
|
|
2378
1539
|
rows.forEach((row, index) => {
|
|
2379
1540
|
const projected = { [ROW_RAW_KEY]: row, [ROW_INDEX_KEY]: index };
|
|
2380
|
-
this.
|
|
1541
|
+
this.activeFields.forEach((col) => {
|
|
2381
1542
|
projected[col.key] = this.resolveCellValue(col, row, index);
|
|
2382
1543
|
});
|
|
2383
1544
|
result.push(projected);
|
|
@@ -2485,7 +1646,7 @@ class TableComponent {
|
|
|
2485
1646
|
groupRows.slice(start, end).forEach((row, i) => {
|
|
2486
1647
|
const idx = start + i;
|
|
2487
1648
|
const projected = { [ROW_RAW_KEY]: row, [ROW_INDEX_KEY]: idx };
|
|
2488
|
-
this.
|
|
1649
|
+
this.activeFields.forEach((col) => {
|
|
2489
1650
|
projected[col.key] = this.resolveCellValue(col, row, idx);
|
|
2490
1651
|
});
|
|
2491
1652
|
result.push(projected);
|
|
@@ -2639,11 +1800,11 @@ class TableComponent {
|
|
|
2639
1800
|
}
|
|
2640
1801
|
}
|
|
2641
1802
|
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TableComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
|
|
2642
|
-
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 ></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\" (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 .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 }); }
|
|
2643
1804
|
}
|
|
2644
1805
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: TableComponent, decorators: [{
|
|
2645
1806
|
type: Component,
|
|
2646
|
-
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 ></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\" (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 .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"] }]
|
|
2647
1808
|
}], propDecorators: { config: [{
|
|
2648
1809
|
type: Input
|
|
2649
1810
|
}], dataSource: [{
|
|
@@ -2694,9 +1855,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
|
|
|
2694
1855
|
}], slicePanelRef: [{
|
|
2695
1856
|
type: ViewChild,
|
|
2696
1857
|
args: [SlicePanelComponent]
|
|
2697
|
-
}], viewMenuTrigger: [{
|
|
2698
|
-
type: ViewChild,
|
|
2699
|
-
args: ['viewMenuTrigger']
|
|
2700
1858
|
}], _cellDefs: [{
|
|
2701
1859
|
type: ContentChildren,
|
|
2702
1860
|
args: [JqtCellDefDirective]
|
|
@@ -2710,5 +1868,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImpo
|
|
|
2710
1868
|
* Generated bundle index. Do not edit.
|
|
2711
1869
|
*/
|
|
2712
1870
|
|
|
2713
|
-
export { JQT_I18N, JQT_I18N_DEFAULTS, JqtCellDefDirective,
|
|
1871
|
+
export { JQT_I18N, JQT_I18N_DEFAULTS, JqtCellDefDirective, OrganizerButtonWrapperComponent, TableComponent, col };
|
|
2714
1872
|
//# sourceMappingURL=oneteme-jquery-table.mjs.map
|