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