@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.
- package/package.json +1 -1
- package/src/web-components/s-datatable-component/docs.md +0 -484
- package/src/web-components/s-datatable-component/s-datatable-component.js +0 -1177
- package/src/web-components/s-datatable-component/s-datatable-component.module.css +0 -220
- package/src/web-components/s-datatable-component/simple-datatable.minimo.tokens.mjs +0 -47
- package/src/web-components/s-datatable-component/src/parse-cols.js +0 -158
- package/src/web-components/s-datatable-component/src/search-method.js +0 -84
|
@@ -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: `'—'` (trattino em).
|
|
93
|
-
* Applicato al valore della cella e ai segnaposto
|
|
94
|
-
* di `_cellRender` che si risolvono a null. (default: '—')
|
|
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 > '—'
|
|
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
|
-
}
|