@massimo-cassandro/minimo 1.29.0 → 1.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1177 +0,0 @@
1
- // @ts-nocheck
2
-
3
- /* global process */
4
-
5
- import { DataTable } from 'simple-datatables';
6
-
7
- import { parseCols } from './src/parse-cols.js';
8
- import { makeSearchMethod } from './src/search-method.js';
9
-
10
- import * as styles from './s-datatable-component.module.css';
11
-
12
- import caretLeftIcon from '../../icons/caret-left.svg?inline';
13
- import caretRightIcon from '../../icons/caret-right.svg?inline';
14
-
15
-
16
- /**
17
- * Implementazione di simple-datatable (https://fiduswriter.github.io/simple-datatables/documentation/)
18
- * per generare una tabella da dati json e da un oggetto `cols` di configurazione
19
- *
20
- * @deprecated Componente destinato alla dismissione
21
- *
22
- * ─────────────────────────────────────────────────────────────────────────────
23
- * PARAMETRI
24
- * ─────────────────────────────────────────────────────────────────────────────
25
- * Tutti i parametri sono leggibili sia da attributo HTML che da script (init).
26
- * Ordine di precedenza: script > attributo HTML > default interno.
27
- *
28
- * @typedef {object} ColDefinition
29
- * @property {string} _heading Intestazione della colonna (obbligatorio).
30
- * @property {string} _field Chiave del campo nell'oggetto dato (obbligatorio).
31
- * @property {string|Function} [_headingTitle] Testo del tooltip sull'intestazione (`<abbr title="…">`).
32
- * Accetta:
33
- * - stringa semplice (tooltip statico)
34
- * - stringa mustache-like con segnaposto `[[key]]`
35
- * (interpolati sui dati della prima riga disponibile;
36
- * utile solo se il titolo varia per colonna, non per riga)
37
- * - funzione `(row) => string` (row è null per le intestazioni)
38
- * @property {string|Function} [_cellTitle] Testo del tooltip delle celle (attributo `title`).
39
- * Accetta:
40
- * - stringa semplice (tooltip statico uguale per tutte le righe)
41
- * - stringa mustache-like con segnaposto `[[key]]`
42
- * (i segnaposto vengono sostituiti con i valori della riga corrente)
43
- * - funzione `(row) => string` (riceve l'oggetto riga corrente)
44
- * @property {string|Function} [_cellRender] stringa o funzione per renderizzare il contenuto della cella.
45
- * Accetta:
46
- * - stringa mustache-like con segnaposto `[[key]]`
47
- * (sostituiti con i valori dell'oggetto riga;
48
- * i segnaposto non trovati usano `_renderNullAs`)
49
- * - funzione `(row) => string`
50
- * - se sono impostate sia `_cellRender` che l'opzione `render` di simple-datatable
51
- * quest'ultima viene annullata per evitare potenziali conflitti
52
- * @property {string|Function} [_footerRender] Contenuto della cella <td> nel <tfoot> per questa colonna.
53
- * Accetta:
54
- * - funzione `(data) => string` dove `data` è l'array dei
55
- * record visibili (filtrati dalla ricerca corrente, ma non
56
- * limitati alla pagina attuale): ideale per somme, medie, conteggi.
57
- * - stringa statica (es. etichetta fissa "Totale:")
58
- * Le colonne senza `_footerRender` producono una <td> vuota.
59
- * Il <tfoot> viene aggiunto solo se almeno una colonna lo definisce.
60
- * La cella eredita le classi CSS (allineamento) della colonna.
61
- *
62
- * @property {string|boolean} [_collapseKey] Percorso (notazione punto) del campo da usare come chiave
63
- * di raggruppamento per collassare valori ripetuti su righe
64
- * consecutive. `true` = riusa `_field`. Colonne diverse con lo
65
- * stesso `_collapseKey` collassano insieme come un unico gruppo
66
- * logico. La prima riga di ogni pagina mostra sempre il valore
67
- * pieno, anche se continua un gruppo iniziato nella pagina precedente.
68
- * @property {string|Function} [_collapsePlaceholder] Testo da mostrare al posto del valore ripetuto
69
- * quando la colonna collassa. Accetta lo stesso ventaglio di
70
- * `_cellRender`/`_cellTitle` (via `resolveTemplate`): stringa
71
- * semplice, stringa mustache-like con segnaposto `[[key]]`
72
- * (interpolati sulla riga corrente), o funzione `(row) => string`.
73
- * Default `null` (anche se la funzione ritorna null/undefined) =
74
- * il testo della cella resta invariato (utile insieme alla sola
75
- * `_collapseClass`). (default: null)
76
- * @property {string} [_collapseClass] Classe CSS da assegnare alla cella quando collassata. Nessuna
77
- * classe di default: il trattamento visivo è a discrezione del
78
- * consumer. (default: null)
79
- *
80
- * @property {string|Function} [_sortValue] Percorso del campo da usare per l'ORDINAMENTO al posto
81
- * del contenuto visualizzato, oppure una funzione
82
- * `(row) => string|number` per casi complessi
83
- * (es. booleani, valori calcolati).
84
- * Stringa: percorso con notazione punto (es. 'owner.cognome').
85
- * Funzione: riceve l'oggetto riga originale.
86
- * @property {string|Function} [_searchValue] Percorso/i del campo da usare per la RICERCA al posto
87
- * del contenuto visualizzato, oppure una funzione
88
- * `(row) => string` per casi complessi (es. booleani).
89
- * Stringa: uno o più percorsi separati da spazio.
90
- * Funzione: riceve l'oggetto riga originale.
91
- * @property {string} [_renderNullAs] Stringa da mostrare al posto di valori null/undefined.
92
- * Default: `'&mdash;'` (trattino em).
93
- * Applicato al valore della cella e ai segnaposto
94
- * di `_cellRender` che si risolvono a null. (default: '&mdash;')
95
- * @property {'id'|'email'|'sf_datetime'} [_renderMode]
96
- * Modalità di visualizzazione predefinita:
97
- * - `id` – colonna numerica, non ricercabile, allineata a destra
98
- * - `email` – inserisce andate a capo prima e dopo la `@`
99
- * - `sf_datetime` – data e ora da oggetto Symfony (con pre-elaborazione in `_load`)
100
- * Per i booleani usare `type: 'boolean'` (proprietà nativa
101
- * simple-datatables): parseCols aggiunge automaticamente
102
- * l'icona spunta/croce con colore semantico.
103
- *
104
- * @param {string} json URL da cui recuperare i dati (struttura attesa: `{ data: [...] }`).
105
- * Ignorato se `data` è presente.
106
- * Eventuali altre chiavi aggiuntive (oltre a `data`) vengono ignorate
107
- *
108
- * @param {Array<object>} data Dati inline, come array di oggetti plain.
109
- * Ha precedenza su `json` se entrambi sono specificati.
110
- * Come attributo HTML è utile principalmente in contesti SSR
111
- * (Twig, Blade, ecc.) dove il JSON viene serializzato server-side;
112
- * per grandi dataset è preferibile `json`.
113
- *
114
- * @param {ColDefinition[]} cols Definizione delle colonne. Le chiavi con prefisso `_` sono
115
- * proprietà di questo componente; le restanti vengono passate
116
- * direttamente a simple-datatables (es. `sortable`, `searchable`).
117
- * Default: `[]` (default: [])
118
- *
119
- * @param {number} perPage Numero di righe per pagina.
120
- * Default: `25` (default: 25)
121
- *
122
- * @param {string} renderNullAs Stringa globale da mostrare al posto di valori null/undefined
123
- * in tutte le colonne. Può essere sovrascritto per singola
124
- * colonna con `_renderNullAs`.
125
- * Default: `\u2014` (em dash) (default: '\u2014')
126
- *
127
- * @param {boolean} updateFooterOnPageChange Se true, la callback `_footerRender` riceve i soli record
128
- * della pagina corrente (subtotale di pagina). Se false
129
- * (default), riceve l'intero set filtrato indipendentemente
130
- * dalla paginazione (totale complessivo).
131
- * Quando è false il footer si aggiorna solo al cambio di
132
- * filtro, non al cambio pagina. (default: false)
133
- *
134
- * @param {string[]} refs Elenco di percorsi URL da cui, se si proviene,
135
- * la pagina corrente viene ripristinata dal cookie
136
- * di sessione `sd-pag`. Se il referrer non è tra
137
- * quelli indicati, il cookie viene eliminato.
138
- * Confronto su `pathname` (query string e hash ignorati).
139
- * Esempio: `['/utenti']`
140
- * NB: il match è positivo anche se il referrer è parte di una delle stringhe permesse
141
- * quindi, ad esempio, `['/utenti']` intercetta sia `/utenti` che `/utenti/1234`
142
- *
143
-
144
- ------------ DA VALUTARE NON IMPLEMENTATO -----------
145
- * @param {string[]} refsCallback callback da eseguire in seguito al match di refs.
146
- * È invocato con l'argomento isRef (true | false) che indica se la pagina referrer
147
- * è tra quelle mappate in refs, quindi `['/utenti']`.
148
- * Utilizzabile solo nella configurazione via script
149
- */
150
-
151
-
152
-
153
- /**
154
- * getNestedValue
155
- *
156
- * Legge un valore da un oggetto tramite un percorso di chiavi separate da punto.
157
- * Supporta sia chiavi semplici ('commessa') che composte ('owner.nome').
158
- * Restituisce undefined se una chiave intermedia non esiste.
159
- *
160
- * Esempi:
161
- * getNestedValue(row, 'commessa') → row.commessa
162
- * getNestedValue(row, 'owner.nome') → row.owner.nome
163
- * getNestedValue(row, 'ciclo.ciclo') → row.ciclo.ciclo
164
- */
165
- function getNestedValue(obj, path) {
166
- return path?.split('.')?.reduce((acc, key) => acc?.[key], obj) ?? null;
167
- }
168
-
169
-
170
- /**
171
- * resolveTemplate
172
- *
173
- * Risolve un template in base al suo tipo:
174
- * - Elemento DOM: restituisce `outerHTML` dell'elemento.
175
- * - Funzione: invocata con `row`, può restituire una stringa o un elemento DOM
176
- * (in tal caso viene serializzato con `outerHTML`).
177
- * - Stringa mustache-like ([[key]]): i segnaposto vengono sostituiti con i
178
- * valori corrispondenti dell'oggetto riga.
179
- * - Stringa semplice: restituita così com'è (utile per tooltip statici).
180
- *
181
- * Quando `row` è null/undefined (caso intestazioni) le funzioni vengono
182
- * comunque chiamate con null; la stringa mustache-like viene restituita
183
- * letteralmente perché non ci sono dati da interpolare.
184
- *
185
- * @param {string|Function|Node} tpl Template da risolvere.
186
- * @param {object|null} row Oggetto riga corrente (null per le intestazioni).
187
- * @param {object} [opts] Opzioni aggiuntive. (default: {})
188
- * @param {string} [opts.nullAs] Stringa da usare come fallback per i segnaposto
189
- * che si risolvono a null (default: `''`).
190
- * Se non fornita i segnaposto non trovati diventano ''.
191
- * @param {number} [opts.warnColIdx] Se fornito, emette console.error quando una
192
- * chiave foglia non esiste sul padre (indica un errore
193
- * di configurazione, non un dato assente).
194
- * @returns {string|null} Stringa risolta, oppure null se `tpl` è falsy.
195
- */
196
- function resolveTemplate(tpl, row, { nullAs = '', warnColIdx } = {}) {
197
- if (!tpl) return null;
198
-
199
- if (tpl instanceof Node) {
200
- return tpl.outerHTML;
201
- }
202
-
203
- if (typeof tpl === 'function') {
204
- const result = tpl(row);
205
- if (result instanceof Node) return result.outerHTML;
206
- return result ?? null;
207
- }
208
-
209
- // Stringa mustache-like: interpola solo se row è disponibile
210
- if (row != null && /\[\[.*?\]\]/.test(tpl)) {
211
- return decodeURIComponent(tpl).replace(
212
- /\[\[(.*?)\]\]/g,
213
- (_, key) => {
214
- const resolved = getNestedValue(row, key);
215
- if (warnColIdx !== undefined) {
216
- const pathParts = key.split('.');
217
- const parentObj = pathParts.length > 1
218
- ? getNestedValue(row, pathParts.slice(0, -1).join('.'))
219
- : row;
220
- if (parentObj != null && !Object.prototype.hasOwnProperty.call(parentObj, pathParts.at(-1))) {
221
- // eslint-disable-next-line no-console
222
- console.error(`[s-datatable] Chiave "${key}" non trovata nella struttura dati (colonna ${warnColIdx})`);
223
- }
224
- }
225
- return resolved ?? nullAs;
226
- }
227
- );
228
- }
229
-
230
- return tpl;
231
- }
232
-
233
- let _deprecationWarned = false;
234
-
235
- /**
236
- * @deprecated Componente destinato alla dismissione, verrà sostituito da `json-table`.
237
- * Vedi la documentazione all'inizio del file per i dettagli.
238
- */
239
- class SimpleDatatableAdapter extends HTMLElement {
240
- constructor() {
241
- super();
242
- // this.attachShadow({ mode: 'open' }); // commentare per light dom
243
-
244
- if (!_deprecationWarned && process.env.NODE_ENV === 'development') {
245
- _deprecationWarned = true;
246
- // eslint-disable-next-line no-console
247
- console.warn('[s-datatable] componente deprecato');
248
- }
249
-
250
- this._config = null;
251
- this._initCalledProgrammatically = false;
252
- this._isConnected = false;
253
- this._loadStarted = false;
254
-
255
- /** Riferimento all'istanza DataTable di simple-datatables */
256
- this._dt = null;
257
-
258
- /** Filtro da applicare dopo il prossimo reload (vedi reload()) */
259
- this._pendingSearch = null;
260
- this._loadGeneration = 0;
261
-
262
- /**
263
- * Conteggio centralizzato delle righe attualmente visibili (post-filtro).
264
- * Viene inizializzato al totale in datatable.init e aggiornato dall'evento
265
- * datatable.search tramite dt.data?.data.length.
266
- * È l'unica fonte di verità per _renderTfoot().
267
- */
268
- this._filteredCount = 0;
269
-
270
- /**
271
- * Array di record raw attualmente visibili (post-filtro, pre-paginazione).
272
- * Popolato da onFilter al momento dell'evento datatable.search/multisearch,
273
- * quando dt.searchData è garantito aggiornato.
274
- * null = nessun filtro attivo → _renderTfoot usa rawData direttamente.
275
- */
276
- this._filteredData = null;
277
-
278
- /**
279
- * Cache del contenuto "vero" (pre-collapse) delle celle con `_collapseKey`,
280
- * chiavata per `${jidx}:${colIdx}` — non per nodo DOM: simple-datatables
281
- * riusa/sposta i nodi <td> tra un render e l'altro (il suo motore di diff
282
- * confronta due virtual-dom interni, non il DOM reale), quindi lo stesso
283
- * nodo fisico può ospitare righe diverse dopo un sort. Popolata al primo
284
- * incontro di ogni cella (prima di un'eventuale collapse) in _applyCollapse,
285
- * svuotata ad ogni nuovo caricamento dati in _load().
286
- */
287
- this._collapseOrigCache = new Map();
288
-
289
- // Handler bound per poterli rimuovere in disconnectedCallback
290
- this._onSearch = this._handleSearch.bind(this);
291
- this._onMultiSearch = this._handleMultiSearch.bind(this);
292
- this._onReload = this._handleReload.bind(this);
293
- }
294
-
295
-
296
- // ─── Lifecycle ──────────────────────────────────────────────────────────────
297
-
298
- async connectedCallback() {
299
- this._isConnected = true;
300
-
301
- // Registra i listener sugli eventi globali
302
- document.addEventListener('datatable:search', this._onSearch);
303
- document.addEventListener('datatable:multisearch', this._onMultiSearch);
304
- document.addEventListener('datatable:reload', this._onReload);
305
-
306
- if (this._initCalledProgrammatically) {
307
- if (!this._loadStarted) this._load();
308
- return;
309
- }
310
-
311
- this._load();
312
- }
313
-
314
- disconnectedCallback() {
315
- document.removeEventListener('datatable:search', this._onSearch);
316
- document.removeEventListener('datatable:multisearch', this._onMultiSearch);
317
- document.removeEventListener('datatable:reload', this._onReload);
318
- }
319
-
320
-
321
- // ─── API pubblica ────────────────────────────────────────────────────────────
322
-
323
- /**
324
- * Avvia il componente via script, passando la configurazione come oggetto.
325
- * Può essere chiamato sia prima che dopo l'inserimento nel DOM.
326
- *
327
- * I parametri non inclusi in `config` vengono letti dall'attributo HTML
328
- * corrispondente, se presente — consentendo un mix dei due approcci:
329
- *
330
- * <s-datatable
331
- * cols='[{"_heading":"Nome","_field":"name"}]'
332
- * ></s-datatable>
333
- *
334
- * el.init({ json: '/api/data.json', perPage: 20 });
335
- * // → json e perPage da script, cols dall'attributo
336
- *
337
- * Per ignorare esplicitamente un attributo esistente e forzare il default
338
- * interno, passare il valore `null` nel config:
339
- *
340
- * el.init({ json: '/api/data.json', cols: null });
341
- * // → cols = null (l'attributo viene ignorato)
342
- */
343
- init(config = {}) {
344
- this._config = config;
345
- this._initCalledProgrammatically = true;
346
-
347
- // Azzera il flag di caricamento: init() chiamato esplicitamente deve
348
- // sempre ripartire, anche se connectedCallback aveva già tentato un
349
- // _load() prematuro (es. quando domBuilder appende l'elemento al DOM
350
- // prima che la configurazione sia disponibile).
351
- this._loadStarted = false;
352
- this._loadGeneration++;
353
-
354
- if (this._isConnected) {
355
- this._load();
356
- }
357
- }
358
-
359
- /**
360
- * Distrugge l'istanza DataTable interna e svuota il componente.
361
- * Equivale a datatable.destroy() di simple-datatables.
362
- * Dopo la chiamata è possibile richiamare init() per reinizializzare
363
- * la tabella con nuovi dati o colonne, senza ricreare l'elemento.
364
- */
365
- destroy() {
366
- if (this._dt) {
367
- this._dt.destroy();
368
- this._dt = null;
369
- }
370
- this._loadStarted = false;
371
- this._loadGeneration++;
372
- this.innerHTML = '';
373
- }
374
-
375
- /**
376
- * Restituisce l'istanza DataTable interna di simple-datatables.
377
- * Utile per operazioni non coperte dall'API del componente.
378
- * Restituisce null se la tabella non è ancora stata inizializzata.
379
- *
380
- * Esempio:
381
- * const instance = dt.getDataTable();
382
- * instance.page(2);
383
- * instance.on('datatable.sort', (col, dir) => console.log(col, dir));
384
- */
385
- getDataTable() {
386
- return this._dt ?? null;
387
- }
388
-
389
- /**
390
- * Ricerca testuale su una o più colonne.
391
- * @param {string} term
392
- * @param {number[]} [columns] indici di colonna; se omesso cerca su tutto
393
- */
394
- search(term, columns) {
395
- if (!this._dt) return;
396
- this._dt.search(term, columns);
397
- }
398
-
399
- /**
400
- * Ricerca multi-colonna con query indipendenti.
401
- * @param {Array<{ terms: string[], columns?: number[] }>} queries
402
- */
403
- multiSearch(queries) {
404
- if (!this._dt) return;
405
- this._dt.multiSearch(queries);
406
- }
407
-
408
- /**
409
- * Ricarica i dati (eventualmente da un nuovo URL e/o con nuove colonne),
410
- * con possibilità di applicare un filtro contestuale al termine del fetch.
411
- *
412
- * @param {object} [overrides] (default: {})
413
- * @param {string} [overrides.json] Nuovo URL dati (ignorato se `data` è presente)
414
- * @param {Array} [overrides.data] Dati inline (array di oggetti); ha precedenza su `json`
415
- * @param {Array} [overrides.cols] Nuove colonne
416
- * @param {object} [overrides.search] Filtro da applicare dopo il caricamento.
417
- * Formati supportati:
418
- * - { term, columns? } → ricerca semplice (search)
419
- * - { queries: [{terms, columns?}, …] } → ricerca multipla (multiSearch)
420
- * I due si escludono: se presente `queries` → multiSearch, altrimenti search.
421
- */
422
- async reload(overrides = {}) {
423
- if (overrides.json) this._setParam('json', overrides.json);
424
- if (overrides.data !== undefined) this._setParam('data', overrides.data);
425
- if (overrides.cols) this._setParam('cols', overrides.cols);
426
-
427
- // Memorizza il filtro pendente: verrà applicato dentro datatable.init,
428
- // dopo che simple-datatables ha terminato di renderizzare i nuovi dati.
429
- this._pendingSearch = overrides.search ?? null;
430
-
431
- this._loadStarted = false;
432
- this._loadGeneration++;
433
- await this._load();
434
- }
435
-
436
-
437
- // ─── Helpers interni ─────────────────────────────────────────────────────────
438
-
439
- /**
440
- * Risolve il valore di un parametro fondendo le tre sorgenti possibili.
441
- *
442
- * Ordine di precedenza (dal più al meno prioritario):
443
- * 1. _config – valore passato via init() o _setParam().
444
- * Se presente e !== undefined sovrascrive sempre l'attributo.
445
- * Passare esplicitamente `null` per ignorare l'attributo e
446
- * ricevere il defaultValue.
447
- * 2. Attributo HTML – se il valore inizia con [ o { viene auto-parsato
448
- * da JSON (utile per cols, che può stare nell'attributo).
449
- * 3. defaultValue – fallback finale.
450
- *
451
- * Esempi con config = { json: '/api/x.json' } e attr cols='[…]':
452
- * _getParam('json') → '/api/x.json' (da config)
453
- * _getParam('cols') → [… parsed …] (da attributo, auto-JSON)
454
- * _getParam('perPage', 25)→ 25 (default)
455
- *
456
- * Per ignorare esplicitamente un attributo, passare null nel config:
457
- * init({ cols: null }) → _getParam('cols') restituisce null,
458
- * l'attributo viene saltato.
459
- */
460
- _getParam(name, defaultValue = null) {
461
- // 1. Config da script (undefined = chiave assente → scendi di livello)
462
- if (this._config && this._config[name] !== undefined) {
463
- return this._config[name];
464
- }
465
-
466
- // 2. Attributo HTML
467
- const attr = this.getAttribute(name);
468
- if (attr !== null) {
469
- // Auto-parse booleani solo quando il parametro è dichiaratamente booleano
470
- // (defaultValue === true | false). Copre tutti i formati HTML idiomatici:
471
- // updateFooterOnPageChange (attributo senza valore → '') → true
472
- // updateFooterOnPageChange="true" / updateFooterOnPageChange="1" → true
473
- // updateFooterOnPageChange="false" / updateFooterOnPageChange="0" → false
474
- // I valori "1" e "0" NON vengono intercettati qui se defaultValue non è
475
- // boolean, per non interferire con parametri numerici (es. perPage="1").
476
- const trimmed = attr.trim();
477
- if (typeof defaultValue === 'boolean') {
478
- const lower = trimmed.toLowerCase();
479
- // Stringa vuota '' = attributo presente senza valore → true (standard HTML)
480
- return lower !== 'false' && lower !== '0';
481
- }
482
-
483
- // Auto-parse JSON per array e oggetti (es. cols='[…]')
484
- if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
485
- try { return JSON.parse(trimmed); } catch { /* restituisce la stringa grezza */ }
486
- }
487
- return attr;
488
- }
489
-
490
- // 3. Default interno
491
- return defaultValue;
492
- }
493
-
494
- _setParam(name, value) {
495
- if (!this._config) this._config = {};
496
- this._config[name] = value;
497
- }
498
-
499
- /**
500
- * Restituisce true se il referrer corrisponde a uno dei percorsi in `refs`.
501
- * Il confronto avviene su `pathname` per ignorare query string e hash.
502
- */
503
- _isReferrer() {
504
- let allowed = this._getParam('refs', []);
505
-
506
- if (typeof allowed === 'string') {
507
- try {
508
- allowed = JSON.parse(allowed);
509
- } catch (e) {
510
- // eslint-disable-next-line no-console
511
- console.error('[s-datatable] Errore nel parsing di \'refs\':', e);
512
- return false;
513
- }
514
- }
515
- if (!Array.isArray(allowed) || !allowed.length || !document.referrer) {
516
- return false;
517
- }
518
- const referrerPath = new URL(document.referrer).pathname;
519
-
520
- // Ritorna true se il path del referrer è contenuto in una delle stringhe di 'allowed'
521
- return allowed.some(allowedUrl =>
522
- (typeof allowedUrl === 'string') && (referrerPath.includes(allowedUrl))
523
- );
524
- }
525
-
526
-
527
- /** Salva la pagina corrente nel cookie di sessione `sd-pag`. */
528
- _savePageCookie(page) {
529
- document.cookie = `sd-pag=${page}; path=${location.pathname}`;
530
- }
531
-
532
- /** Legge la pagina dal cookie `sd-pag`. Restituisce null se assente. */
533
- _readPageCookie() {
534
- const match = document.cookie.match(/(?:^|;\s*)sd-pag=(\d+)/);
535
- return match ? parseInt(match[1], 10) : null;
536
- }
537
-
538
- /** Elimina il cookie `sd-pag`. */
539
- _deletePageCookie() {
540
- document.cookie = `sd-pag=; path=${location.pathname}; max-age=0`;
541
- }
542
-
543
-
544
- /**
545
- * Ritorna true se questo elemento è il target dell'evento.
546
- * Se detail.target è assente, risponde sempre;
547
- * altrimenti solo se corrisponde all'id dell'elemento.
548
- */
549
- _isTarget(detail) {
550
- return !detail.target || detail.target === this.id;
551
- }
552
-
553
-
554
- // ─── Handler eventi globali ──────────────────────────────────────────────────
555
-
556
- _handleSearch(e) {
557
- if (!this._isTarget(e.detail)) return;
558
- const { term = '', columns } = e.detail;
559
- this.search(term, columns);
560
- }
561
-
562
- _handleMultiSearch(e) {
563
- if (!this._isTarget(e.detail)) return;
564
- const { queries = [] } = e.detail;
565
- this.multiSearch(queries);
566
- }
567
-
568
- _handleReload(e) {
569
- if (!this._isTarget(e.detail)) return;
570
- // Propaga `data` solo se esplicitamente presente nel detail
571
- const overrides = { ...e.detail };
572
- this.reload(overrides);
573
- }
574
-
575
-
576
- // ─── Caricamento dati ────────────────────────────────────────────────────────
577
-
578
- async _load() {
579
- if (this._loadStarted) return;
580
- this._loadStarted = true;
581
- const generation = this._loadGeneration;
582
-
583
- this.innerHTML = '<div class="spinner"><span class="visually-hidden">Caricamento dati...</span></div>';
584
-
585
- const inlineData = this._getParam('data');
586
- const jsonUrl = this._getParam('json');
587
- const cols = this._getParam('cols', []);
588
- const globalNullAs = this._getParam('renderNullAs', '\u2014');
589
-
590
- if (inlineData == null && !jsonUrl) {
591
- if (this._initCalledProgrammatically) {
592
- // eslint-disable-next-line no-console
593
- console.error('[s-datatable] Nessuna sorgente dati: specificare `data` o `json`.');
594
- }
595
- this.innerHTML = '';
596
- return;
597
- }
598
-
599
- try {
600
- // Risolvi i dati grezzi: inline `data` ha precedenza su `json`
601
- let rawData;
602
- if (inlineData != null) {
603
- rawData = Array.isArray(inlineData) ? inlineData : [];
604
- } else {
605
- const response = await fetch(jsonUrl);
606
- const parsed = await response.json();
607
- rawData = parsed.data;
608
- }
609
-
610
- this.headings = cols.map(item => {
611
- const title = resolveTemplate(item._headingTitle, null);
612
- return title
613
- ? `<abbr title="${title}">${item._heading}</abbr>`
614
- : item._heading;
615
- });
616
-
617
- this.data = rawData.map((row, idx) => ({
618
- attributes: { 'data-jidx': idx },
619
-
620
- cells: cols.map((col_item, col_idx) => {
621
- let value = getNestedValue(row, col_item._field);
622
-
623
- // _renderNullAs: stringa da mostrare quando il valore è null/undefined.
624
- // Precedenza: _renderNullAs di colonna > renderNullAs globale > '&mdash;'
625
- const nullAs = col_item._renderNullAs !== undefined
626
- ? col_item._renderNullAs
627
- : globalNullAs;
628
-
629
- // _sortValue: valore alternativo per l'ordinamento.
630
- // Accetta un percorso stringa o una funzione (row) => string|number.
631
- // Quando col_item.type è 'string' (default), simple-datatables confronta
632
- // `order` come stringa: se il valore letto è un numero (es. un id numerico
633
- // usato come chiave di lookup) viene convertito a stringa per garantire
634
- // che il comparatore string di simple-datatables operi correttamente.
635
- // Per colonne con type 'number' o 'date' il valore viene lasciato invariato.
636
- const sortValue = col_item._sortValue
637
- ? (() => {
638
- const raw = typeof col_item._sortValue === 'function'
639
- ? (col_item._sortValue(row) ?? '')
640
- : (getNestedValue(row, col_item._sortValue) ?? '');
641
- const colType = col_item.type ?? 'string';
642
- return (colType === 'string' && typeof raw === 'number')
643
- ? String(raw)
644
- : raw;
645
- })()
646
- : null;
647
-
648
-
649
- // _cellRender: template per il rendering del valore.
650
- // Accetta stringa mustache-like ([[key]]), funzione (row) => string|Node,
651
- // oppure un elemento DOM direttamente (serializzato via outerHTML).
652
- // Delega a resolveTemplate, passando nullAs e l'indice di colonna
653
- // per i warning di chiavi mancanti.
654
- if (col_item._cellRender) {
655
- col_item.render = null; // se presente lo annulla ad evitare conflitti
656
- value = resolveTemplate(col_item._cellRender, row, { nullAs, warnColIdx: col_idx });
657
-
658
- // sf_date o sf_datetime -> si prende il solo valore di date
659
- } else if ((col_item._renderMode === 'sf_datetime' || col_item._renderMode === 'sf_date') && value != null) {
660
- value = value.date.replace(' ', 'T') +
661
- (value.timezone === 'UTC' ? 'Z' : '');
662
- }
663
-
664
- // TODO rendere più snella la gestione di questi casi (???)
665
- // TODO passare il valore `nullAs` al render predefinito
666
- // Applica _renderNullAs al valore della cella se null/undefined
667
- // e la cella non ha già un template che gestisce la visualizzazione.
668
- // se _renderMode è impostato, il valore null è gestito direttamente dal gestore predefinito
669
- if (value == null && col_item._cellRender == null && col_item._renderMode == null) {
670
- value = nullAs;
671
- }
672
-
673
- // _searchValue: valore alternativo per la ricerca.
674
- // Viene scritto in `attributes['data-search']` della cella: la
675
- // searchMethod di colonna lo legge dall'oggetto dati interno di
676
- // simple-datatables (cellType), non dal DOM.
677
- // Accetta un percorso (o più percorsi separati da spazio) oppure una
678
- // funzione (row) => string|number: il valore viene normalizzato con
679
- // String() al momento del confronto.
680
- const searchValue = col_item._searchValue
681
- ? typeof col_item._searchValue === 'function'
682
- ? (col_item._searchValue(row) ?? '')
683
- : col_item._searchValue.split(' ')
684
- .map(path => getNestedValue(row, path.trim()) ?? '')
685
- .filter(Boolean)
686
- .join(' ')
687
- : null;
688
-
689
- // Se sono definiti _sortValue o _searchValue, restituisce un
690
- // oggetto cellType secondo le API di simple-datatables:
691
- // { data, order?, attributes? }
692
- // - `data` → contenuto visualizzato (obbligatorio)
693
- // - `order` → valore alternativo per l'ordinamento (campo nativo)
694
- // - `attributes['data-search']` → stringa su cui opera searchMethod
695
- // - `attributes['title']` → tooltip della cella (_cellTitle)
696
-
697
- // Risolvi _cellTitle: stringa semplice, mustache-like o funzione(row)
698
- const cellTitle = resolveTemplate(col_item._cellTitle, row);
699
-
700
- if (sortValue !== null || searchValue !== null || cellTitle !== null) {
701
- const cellObj = { data: value ?? '' };
702
- if (sortValue !== null) cellObj.order = sortValue;
703
-
704
- // Unifica data-search e title in un unico oggetto attributes
705
- const attrs = {};
706
- if (searchValue !== null) attrs['data-search'] = searchValue;
707
- if (cellTitle !== null) attrs['title'] = cellTitle;
708
- if (Object.keys(attrs).length) cellObj.attributes = attrs;
709
-
710
- return cellObj;
711
- }
712
-
713
- return value;
714
- })
715
- }));
716
-
717
- // Conserva i dati grezzi e le definizioni di colonna per il tfoot
718
- this._rawData = rawData;
719
- this._footerCols = cols;
720
- this._collapseOrigCache.clear(); // i jidx ripartono da zero, la cache precedente non è più valida
721
-
722
- this.columns = cols.map((col_settings, idx) => {
723
- col_settings = parseCols(col_settings);
724
-
725
- const colObj = {
726
- ...(Object.fromEntries(
727
- Object.entries(col_settings).filter(([key]) => !key.startsWith('_'))
728
- )),
729
- select: idx,
730
- type: col_settings.type ?? 'string',
731
- searchable: col_settings.searchable ?? true,
732
- sortable: col_settings.sortable ?? true,
733
- };
734
-
735
- // _searchValue: funzione searchMethod personalizzata che opera su
736
- // data-search invece che sul testo visualizzato.
737
- // L'API simple-datatables prevede: (terms, cell, row, colIdx, source)
738
- // - terms: array di stringhe (uno per parola cercata)
739
- // - cell: oggetto dati interno `cellType` ({ data, text?, order?, attributes? }),
740
- // NON il nodo DOM <td>: la libreria passa `searchRow[index]`
741
- // (vedi `multiSearch` in src/datatable.ts).
742
- // La funzione deve restituire true se la riga è da includere.
743
- // Vedi ./src/search-method.js per la lettura del valore e la
744
- // normalizzazione (allineata a quella che la libreria applica ai termini).
745
- if (col_settings._searchValue) {
746
- colObj.searchMethod = makeSearchMethod(col_settings);
747
- }
748
-
749
- return colObj;
750
- });
751
-
752
- } catch (err) {
753
- /* eslint-disable no-console */
754
- if (jsonUrl) console.error(jsonUrl);
755
- console.error(err);
756
- /* eslint-enable no-console */
757
- }
758
-
759
- if (generation !== this._loadGeneration) return;
760
- this._render();
761
- }
762
-
763
-
764
- // ─── Render ──────────────────────────────────────────────────────────────────
765
-
766
- _render() {
767
- // Distrugge l'istanza precedente prima di ricrearla (es. dopo reload)
768
- if (this._dt) {
769
- this._dt.destroy();
770
- this._dt = null;
771
- }
772
-
773
- this.innerHTML = '';
774
- const table = document.createElement('table');
775
- this.appendChild(table);
776
-
777
- const perPage = +this._getParam('perPage', 25);
778
-
779
- this._dt = new DataTable(table, {
780
- locale: 'it',
781
- searchable: true,
782
- sortable: true,
783
- fixedHeight: false,
784
- fixedColumns: false,
785
- paging: true,
786
- perPage,
787
- pagerDelta: 2,
788
- perPageSelect: false,
789
-
790
- labels: {
791
- placeholder: 'Cerca...',
792
- searchTitle: 'Cerca nella tabella',
793
- pageTitle: 'Pagina {page}',
794
- perPage: 'Righe per pagina',
795
- noRows: 'Nessun record trovato',
796
- info: 'Stai visualizzando le righe da {start} a {end}, su un totale di {rows} record trovati',
797
- noResults: 'Nessun risultato per la ricerca',
798
- },
799
-
800
- template: (options, dom) => `<div class='${options.classes.top}'>`+
801
- `<div class='${options.classes.info}'></div>` +
802
- (options.searchable
803
- ? `<div class='${options.classes.search}'>` +
804
- `<input class='${options.classes.input}' placeholder='${options.labels.placeholder}' type='search' title='${options.labels.searchTitle}'${dom.id ? ` aria-controls="${dom.id}"` : ''}>` +
805
- '</div>'
806
- : '') +
807
- '</div>' +
808
- `<div class='${options.classes.container}'${options.scrollY.length ? ` style='height: ${options.scrollY}; overflow-Y: auto;'` : ""}></div>`+
809
- `<div class='${options.classes.bottom}'>`+
810
- `<nav class='${options.classes.pagination}'></nav>` +
811
- '</div>'
812
- ,
813
-
814
- classes: {
815
- wrapper : styles.datatableWrapper,
816
- container : 'table-responsive',
817
- loading : 'datatable-loading', // ??
818
-
819
- // barra superiore
820
- top : styles.topArea,
821
- search : styles.searchWrapper,
822
- input : 'form-control form-control-sm',
823
- // dropdown : 'datatable-dropdown', // selettore righe per pagina, disattivato
824
-
825
- empty : styles.empty,
826
-
827
- // table
828
- table : `table table-bordered ${styles.table}`,
829
- // buttons in thead per il sort
830
- sorter : `btn-reset ${styles.sortBtn}`,
831
- ascending : styles.sortAscending,
832
- descending : styles.sortDescending,
833
-
834
- // area inferiore
835
- bottom : styles.bottomArea,
836
- // info text
837
- info : styles.info,
838
-
839
- // pagination
840
- // pagination : styles.paginationNav,
841
- paginationList : styles.paginationList,
842
- paginationListItem : styles.paginationListItem,
843
- paginationListItemLink : styles.paginationListItemBtn,
844
- // hidden : 'datatable-hidden',
845
- disabled : styles.paginationDisabled,
846
- active : styles.paginationActive,
847
-
848
- // cursor: 'datatable-cursor',
849
- // dropdown: 'datatable-dropdown',
850
- // ellipsis: 'datatable-ellipsis',
851
- // filter: 'datatable-filter',
852
- // filterActive: 'datatable-filter-active',
853
- // headercontainer: 'datatable-headercontainer',
854
- // selector: 'datatable-selector',
855
- },
856
-
857
- columns: this.columns,
858
- data: {
859
- headings: this.headings,
860
- data: this.data
861
- }
862
- });
863
-
864
- this._dt.on('datatable.init', () => {
865
- // Inizializza il conteggio filtrato al totale: al primo render non c'è
866
- // ricerca attiva, quindi tutti i record sono visibili.
867
- this._filteredCount = this._rawData?.length ?? 0;
868
- // ── Icone paginazione ─────────────────────────────────────────────────
869
- const pagination = this.querySelector(`.${styles.paginationList}`);
870
- if (pagination) {
871
- const items = pagination.querySelectorAll(`.${styles.paginationListItem}`);
872
- if (items.length) {
873
- pagination.querySelector(
874
- `.${styles.paginationListItem}:first-child .${styles.paginationListItemBtn}`
875
- ).innerHTML = caretLeftIcon;
876
-
877
- pagination.querySelector(
878
- `.${styles.paginationListItem}:last-child .${styles.paginationListItemBtn}`
879
- ).innerHTML = caretRightIcon;
880
-
881
- pagination.querySelectorAll(`.${styles.paginationListItemBtn}:has(svg)`)
882
- .forEach(btn => btn.classList.add(styles.hasSvg));
883
- }
884
- }
885
-
886
- // ── Ripristino pagina dal cookie di sessione (se referrer autorizzato) ─
887
- // const isRefUrl = this._isReferrer(), refsCallback = this._getParam('refsCallback', null);
888
- // if (isRefUrl) {
889
- if (this._isReferrer()) {
890
- const savedPage = this._readPageCookie();
891
- if (savedPage !== null) {
892
- this._dt.page(savedPage);
893
- }
894
- } else {
895
- this._deletePageCookie();
896
- }
897
-
898
- // if(refsCallback && typeof refsCallback === 'function') {
899
- // refsCallback(isRefUrl);
900
- // }
901
-
902
- // ── Filtro contestuale post-reload ────────────────────────────────────
903
- // Applica eventuale filtro contestuale passato via reload({ search: … })
904
- // Lo facciamo qui, dentro datatable.init, per essere certi che
905
- // simple-datatables abbia completato il render dei nuovi dati.
906
- if (this._pendingSearch) {
907
- const ps = this._pendingSearch;
908
- this._pendingSearch = null;
909
-
910
- if (ps.queries) {
911
- this._dt.multiSearch(ps.queries);
912
- } else if (ps.term !== undefined) {
913
- this._dt.search(ps.term, ps.columns);
914
- }
915
- }
916
-
917
- // ── Tfoot ─────────────────────────────────────────────────────────────
918
- // Eseguito sempre, indipendentemente dalla presenza della paginazione.
919
- this._renderTfoot();
920
-
921
- // ── Collapse valori ripetuti ────────────────────────────────────────
922
- this._applyCollapse();
923
- });
924
-
925
- // Salva la pagina corrente nel cookie ad ogni cambio di pagina;
926
- // aggiorna il tfoot solo se updateFooterOnPageChange è attivo.
927
- // _applyCollapse va invece ricalcolato sempre: ogni cambio pagina
928
- // rirenderizza righe nuove, la cui adiacenza va rivalutata.
929
- this._dt.on('datatable.page', page => {
930
- this._savePageCookie(page);
931
- if (this._getParam('updateFooterOnPageChange', false)) this._renderTfoot();
932
- this._applyCollapse();
933
- });
934
-
935
- // Un cambio di ordinamento altera l'adiacenza delle righe: ricalcola il collapse.
936
- this._dt.on('datatable.sort', () => {
937
- this._applyCollapse();
938
- });
939
-
940
- // Aggiorna il tfoot ad ogni cambio di filtro (ricerca / multisearch).
941
- // L'evento datatable.search è la fonte di verità centralizzata per il conteggio
942
- // delle righe filtrate: _filteredCount viene aggiornato qui e poi consumato da
943
- // _renderTfoot(), eliminando letture inconsistenti di dt.data?.data.
944
- // Il sort non altera il set filtrato: il footer non deve cambiare.
945
- // datatable.search emette (term, searchData)
946
- // datatable.multisearch emette (queries, searchData)
947
- // searchData è l'array degli indici in dt.data.data (post-sort), NON in rawData.
948
- // Per risalire ai record raw si usa l'attributo 'data-jidx' scritto su ogni riga
949
- // in fase di caricamento (_load), che preserva la corrispondenza con rawData
950
- // anche dopo che simple-datatables ha riordinato dt.data.data internamente.
951
- // Quando term === '' (reset), searchData è [] ma va usato rawData completo.
952
- const onFilter = (termOrQueries, searchData) => {
953
- const rawData = this._rawData ?? [];
954
- const dt = this._dt;
955
- const isReset = termOrQueries === '' ||
956
- (Array.isArray(termOrQueries) && termOrQueries.length === 0);
957
-
958
- if (isReset) {
959
- this._filteredData = null; // null → _renderTfoot usa rawData
960
- } else {
961
- // Mappa l'indice dt.data.data → indice rawData tramite data-jidx
962
- this._filteredData = searchData
963
- .map(dtIdx => {
964
- const jidx = dt.data?.data[dtIdx]?.attributes?.['data-jidx'];
965
- return jidx != null ? rawData[jidx] : undefined;
966
- })
967
- .filter(Boolean);
968
- }
969
-
970
- this._filteredCount = this._filteredData?.length ?? rawData.length;
971
- this._renderTfoot();
972
- this._applyCollapse();
973
- };
974
- this._dt.on('datatable.search', onFilter);
975
- this._dt.on('datatable.multisearch', onFilter);
976
-
977
- } // end _render
978
-
979
-
980
- // ─── Footer (tfoot) ──────────────────────────────────────────────────────────
981
-
982
- /**
983
- * Costruisce o aggiorna la riga <tfoot> della tabella.
984
- *
985
- * Chiamato dopo il render iniziale, ad ogni cambio di filtro (ricerca /
986
- * multisearch) e — se `updateFooterOnPageChange` è true — anche ad ogni cambio pagina.
987
- *
988
- * Il conteggio delle righe filtrate viene letto da `this._filteredCount`,
989
- * aggiornato centralmente dall'handler di `datatable.search` / `datatable.multisearch`.
990
- * Il tfoot viene rimosso quando non ci sono righe filtrate.
991
- *
992
- * Dataset passato alla callback `_footerRender(data)`:
993
- *
994
- * updateFooterOnPageChange: false (default)
995
- * → l'intero set filtrato, indipendente dalla pagina corrente.
996
- * I totali non cambiano navigando tra le pagine.
997
- * Se non c'è ricerca attiva, viene passato il dataset completo.
998
- * I record filtrati vengono letti da `this._filteredData`, popolato
999
- * in onFilter dagli argomenti dell'evento datatable.search/multisearch,
1000
- * che passano `searchData` (array di indici) direttamente senza dover
1001
- * leggere lo stato interno di simple-datatables.
1002
- * null = reset o nessun filtro → rawData completo.
1003
- *
1004
- * updateFooterOnPageChange: true
1005
- * → solo i record della pagina corrente.
1006
- * Utile per subtotali di pagina.
1007
- *
1008
- * Quando la ricerca non trova nulla, `_filteredData` è `[]`:
1009
- * in quel caso la callback riceve un array vuoto, non il dataset completo.
1010
- */
1011
- _renderTfoot() {
1012
- const cols = this._footerCols;
1013
- const rawData = this._rawData;
1014
-
1015
- if (!cols?.length || !rawData) return;
1016
-
1017
- const hasFooter = cols.some(c => c._footerRender != null);
1018
- if (!hasFooter) return;
1019
-
1020
- const table = this.querySelector('table');
1021
- if (!table) return;
1022
-
1023
- const dt = this._dt;
1024
- const filteredCount = this._filteredCount;
1025
-
1026
- // Rimuove il tfoot quando non ci sono righe filtrate
1027
- if (filteredCount === 0) {
1028
- table.querySelector('tfoot')?.remove();
1029
- return;
1030
- }
1031
-
1032
- const updateFooterOnPageChange = this._getParam('updateFooterOnPageChange', false);
1033
-
1034
- let visibleData;
1035
-
1036
- if (updateFooterOnPageChange) {
1037
- const pageRows = dt.pages?.[dt.currentPage - 1] ?? [];
1038
- visibleData = pageRows
1039
- .map(row => rawData[row['data-index']])
1040
- .filter(Boolean);
1041
- } else {
1042
- // this._filteredData è popolato in onFilter dagli argomenti dell'evento
1043
- // datatable.search / datatable.multisearch (che passano searchData direttamente).
1044
- // null = nessun filtro attivo (o reset) → usiamo rawData completo.
1045
- visibleData = this._filteredData ?? rawData;
1046
- }
1047
-
1048
- // Riutilizza tfoot esistente o ne crea uno nuovo
1049
- let tfoot = table.querySelector('tfoot');
1050
- if (!tfoot) {
1051
- tfoot = document.createElement('tfoot');
1052
- table.appendChild(tfoot);
1053
- }
1054
-
1055
- const tr = document.createElement('tr');
1056
-
1057
- cols.forEach(col => {
1058
- const td = document.createElement('td');
1059
-
1060
- if (col._footerRender != null) {
1061
- if (typeof col._footerRender === 'function') {
1062
- td.innerHTML = col._footerRender(visibleData) ?? '';
1063
- } else {
1064
- // Stringa statica (es. etichetta fissa "Totale:")
1065
- td.innerHTML = col._footerRender;
1066
- }
1067
- }
1068
-
1069
- // Propaga le classi di allineamento della colonna per coerenza visiva
1070
- const cellClass = col.cellClass ?? col.headerClass ?? '';
1071
- if (cellClass) td.className = cellClass;
1072
-
1073
- tr.appendChild(td);
1074
- });
1075
-
1076
- tfoot.replaceChildren(tr);
1077
- }
1078
-
1079
-
1080
- // ─── Collapse valori ripetuti ────────────────────────────────────────────────
1081
-
1082
- /**
1083
- * Sostituisce, nelle colonne con `_collapseKey`, il valore delle celle che
1084
- * ripetono la chiave di raggruppamento della riga precedente con un
1085
- * segnaposto (`_collapsePlaceholder`) e/o una classe CSS (`_collapseClass`).
1086
- *
1087
- * Opera sulle sole righe effettivamente renderizzate nel <tbody> (già
1088
- * filtrate/ordinate/paginate da simple-datatables), risalendo al dato raw
1089
- * di ciascuna tramite l'attributo `data-jidx`. La prima riga di ogni
1090
- * pagina non viene mai collassata, anche se continua un gruppo iniziato
1091
- * nella pagina precedente.
1092
- *
1093
- * Va richiamato ad ogni evento che rirenderizza il <tbody>: datatable.init,
1094
- * datatable.page, datatable.sort, datatable.search, datatable.multisearch.
1095
- *
1096
- * IMPORTANTE — perché serve una cache e non basta ricalcolare "a fresco":
1097
- * il motore di render di simple-datatables (_renderTable) confronta due
1098
- * *virtual dom* interni (quello dell'ultimo render e quello nuovo) e applica
1099
- * al DOM reale solo le differenze fra i due. Non guarda mai il DOM reale in
1100
- * lettura. Se una cella che abbiamo collassato manualmente (fuori da questo
1101
- * ciclo) risulta testualmente identica fra vecchio e nuovo virtual dom — cosa
1102
- * tutt'altro che rara dopo un sort, perché è esattamente la condizione che fa
1103
- * scattare il collapse — la libreria non genera alcuna patch per quella cella
1104
- * e il nostro `»` resta lì, sbagliato, anche se la riga non è più un duplicato.
1105
- * Inoltre i nodi <td> non sono associati in modo stabile a una riga: dopo un
1106
- * sort lo stesso nodo fisico può ospitare dati di una riga diversa. Per questo
1107
- * il valore originale va cachato per identità di riga (`data-jidx`), non per
1108
- * nodo DOM: `this._collapseOrigCache`, chiave `${jidx}:${colIdx}`, popolata al
1109
- * primo incontro di ogni cella (sempre prima di un'eventuale collapse, quindi
1110
- * il valore catturato è garantito genuino) e usata per ripristinare il
1111
- * contenuto pieno quando la riga non è (più) un duplicato.
1112
- */
1113
- _applyCollapse() {
1114
- const cols = this._footerCols;
1115
- if (!cols?.some(col => col._collapseKey)) return;
1116
-
1117
- const rawData = this._rawData;
1118
- const table = this.querySelector('table');
1119
- if (!table || !rawData) return;
1120
-
1121
- const rows = [...table.querySelectorAll('tbody tr')];
1122
- if (!rows.length) return;
1123
-
1124
- const cache = this._collapseOrigCache;
1125
-
1126
- const rowRaw = rows.map(tr => {
1127
- const jidx = tr.dataset.jidx;
1128
- return jidx != null ? rawData[jidx] : null;
1129
- });
1130
-
1131
- cols.forEach((col, colIdx) => {
1132
- if (!col._collapseKey) return;
1133
- const keyPath = col._collapseKey === true ? col._field : col._collapseKey;
1134
- const collapseClass = col._collapseClass ?? null;
1135
-
1136
- rows.forEach((tr, i) => {
1137
- const jidx = tr.dataset.jidx;
1138
- const cur = rowRaw[i];
1139
- if (jidx == null || cur == null) return;
1140
-
1141
- const cell = tr.children[colIdx];
1142
- if (!cell) return;
1143
-
1144
- // Prima cattura per questa riga/colonna: il contenuto attuale è per
1145
- // forza quello vero, perché non lo abbiamo ancora toccato noi.
1146
- const cacheKey = `${jidx}:${colIdx}`;
1147
- if (!cache.has(cacheKey)) cache.set(cacheKey, cell.innerHTML);
1148
-
1149
- const prev = i > 0 ? rowRaw[i - 1] : null;
1150
- const curVal = getNestedValue(cur, keyPath);
1151
- const prevVal = prev != null ? getNestedValue(prev, keyPath) : null;
1152
- // i === 0: mai collassata, anche se continua un gruppo di pagina precedente
1153
- const shouldCollapse = i > 0 && curVal != null && curVal === prevVal;
1154
-
1155
- if (shouldCollapse) {
1156
- // _collapsePlaceholder: stringa, mustache-like ([[key]]) o funzione (row) => string,
1157
- // stesso ventaglio di resolveTemplate già usato per _cellRender/_cellTitle.
1158
- // null (anche di ritorno da funzione) = lascia il testo della cella invariato.
1159
- const placeholder = resolveTemplate(col._collapsePlaceholder, cur);
1160
- if (placeholder != null && cell.innerHTML !== placeholder) cell.innerHTML = placeholder;
1161
- if (collapseClass) cell.classList.add(collapseClass);
1162
- } else {
1163
- const orig = cache.get(cacheKey);
1164
- if (cell.innerHTML !== orig) cell.innerHTML = orig;
1165
- if (collapseClass) cell.classList.remove(collapseClass);
1166
- }
1167
- });
1168
- });
1169
- }
1170
-
1171
-
1172
- } // end component
1173
-
1174
-
1175
- if (!customElements.get('s-datatable')) {
1176
- customElements.define('s-datatable', SimpleDatatableAdapter);
1177
- }